Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/pages/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ have you up and running with a modern, customizable site that your developers wi

## Prerequisites

- **Node.js** `22.7.0+` (or `20.19+`) - [Download here](https://nodejs.org/)
- **Node.js** `22.12.0+` (or `20.19+`) - [Download here](https://nodejs.org/)
- A terminal or command prompt
- Your favorite code editor

Expand Down
2 changes: 1 addition & 1 deletion examples/with-vite-config/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { visualizer } from "rollup-plugin-visualizer";
/** @type {import('vite').UserConfig} */
export default {
build: {
rollupOptions: {
rolldownOptions: {
plugins: [visualizer()],
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/zudoku/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import semver from "semver";

if (!semver.satisfies(process.version, ">=20.19.0 <21.0.0 || >=22.7.0")) {
if (!semver.satisfies(process.version, ">=20.19.0 <21.0.0 || >=22.12.0")) {
// biome-ignore lint/suspicious/noConsole: Logging allowed here
console.error(
`⚠️ Zudoku requires Node.js version >=20.19.0 or >=22.7.0. Your version: ${process.version}`,
`⚠️ Zudoku requires Node.js version >=20.19.0 or >=22.12.0. Your version: ${process.version}`,
);
process.exit(1);
}
Expand Down
5 changes: 2 additions & 3 deletions packages/zudoku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
"@tanstack/react-query": "5.90.21",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "5.1.4",
"@vitejs/plugin-react": "6.0.0",
"@x0k/json-schema-merge": "1.0.2",
"@zudoku/httpsnippet": "10.0.9",
"@zudoku/react-helmet-async": "2.0.5",
Expand Down Expand Up @@ -176,7 +176,6 @@
"remark-frontmatter": "5.0.0",
"remark-gfm": "4.0.1",
"remark-mdx-frontmatter": "5.2.0",
"rollup": "4.59.0",
"semver": "7.7.4",
"shiki": "3.23.0",
"sitemap": "9.0.1",
Expand All @@ -188,7 +187,7 @@
"unist-util-visit": "5.1.0",
"vaul": "1.1.2",
"vfile": "6.0.3",
"vite": "7.3.1",
"vite": "8.0.0",
"yaml": "2.8.2",
"yargs": "18.0.0",
"zod": "4.3.6",
Expand Down
18 changes: 0 additions & 18 deletions packages/zudoku/src/config/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { stat } from "node:fs/promises";
import path from "node:path";
import colors from "picocolors";
import type { RollupOutput, RollupWatcher } from "rollup";
import {
type ConfigEnv,
runnerImport,
Expand Down Expand Up @@ -106,23 +105,6 @@ async function loadZudokuConfigWithMeta(
return configWithMetadata;
}

export function findOutputPathOfServerConfig(
output: RollupOutput | RollupOutput[] | RollupWatcher,
) {
if (Array.isArray(output)) {
throw new Error("Expected a single output, but got an array");
}
if ("output" in output) {
const result = output.output.find(
(o) => "isEntry" in o && o.isEntry && o.fileName === "zudoku.config.js",
);
if (result) {
return result.fileName;
}
}
throw new Error("Could not find server config output file");
}

function loadEnv(configEnv: ConfigEnv, rootDir: string) {
const envPrefix = ["ZUPLO_PUBLIC_", "ZUDOKU_PUBLIC_"];
const localEnv = viteLoadEnv(configEnv.mode, rootDir, envPrefix);
Expand Down
15 changes: 15 additions & 0 deletions packages/zudoku/src/config/validators/icon-types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions packages/zudoku/src/lib/authentication/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { ZudokuError, type ZudokuErrorOptions } from "../util/invariant.js";
export class AuthorizationError extends Error {}

export class OAuthAuthorizationError extends ZudokuError {
constructor(
message: string,
public error?: unknown,
options?: ZudokuErrorOptions,
) {
error: unknown;

constructor(message: string, error?: unknown, options?: ZudokuErrorOptions) {
super(message, options);
this.error = error;
}
}

Expand Down
106 changes: 55 additions & 51 deletions packages/zudoku/src/vite/build.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { build as esbuild } from "esbuild";
import type { Rollup } from "vite";
import { build as viteBuild } from "vite";
import { createBuilder, type Rolldown } from "vite";
import { ZuploEnv } from "../app/env.js";
import { getZudokuRootDir } from "../cli/common/package-json.js";
import {
findOutputPathOfServerConfig,
loadZudokuConfig,
} from "../config/loader.js";
import { type ConfigWithMeta, loadZudokuConfig } from "../config/loader.js";
import { getIssuer } from "../lib/auth/issuer.js";
import invariant from "../lib/util/invariant.js";
import { joinUrl } from "../lib/util/joinUrl.js";
Expand All @@ -19,21 +15,6 @@ import { prerender } from "./prerender/prerender.js";

const DIST_DIR = "dist";

const extractAssets = (result: Rollup.RollupOutput) => {
const jsEntry = result.output.find(
(o) => "isEntry" in o && o.isEntry,
)?.fileName;
const cssEntries = result.output
.filter((o) => o.fileName.endsWith(".css"))
.map((o) => o.fileName);

if (!jsEntry || cssEntries.length === 0) {
throw new Error("Build failed. No js or css assets found");
}

return { jsEntry, cssEntries };
};

export type BuildOptions = {
dir: string;
ssr?: boolean;
Expand All @@ -43,49 +24,63 @@ export type BuildOptions = {
export async function runBuild(options: BuildOptions) {
const { dir, ssr, adapter = "node" } = options;

// Build client and server bundles
const viteClientConfig = await getViteConfig(dir, {
const viteConfig = await getViteConfig(dir, {
mode: "production",
command: "build",
});
const viteServerConfig = await getViteConfig(dir, {
mode: "production",
command: "build",
isSsrBuild: true,
});

const clientResult = await viteBuild(viteClientConfig);
const serverResult = await viteBuild({
...viteServerConfig,
logLevel: "silent",
});
const builder = await createBuilder(viteConfig);

if (Array.isArray(clientResult) || !("output" in clientResult)) {
throw new Error("Client build failed");
}
if (Array.isArray(serverResult) || !("output" in serverResult)) {
throw new Error("Server build failed");
}
invariant(builder.environments.client, "Client environment is missing");
invariant(builder.environments.ssr, "SSR environment is missing");

const distDir = path.resolve(path.join(dir, "dist"));
await rm(distDir, { recursive: true, force: true });

const [clientResult, serverResult] = await Promise.all([
builder.build(builder.environments.client),
builder.build(builder.environments.ssr),
]);

invariant(
clientResult && !Array.isArray(clientResult) && "output" in clientResult,
"Client build failed to produce valid output",
);

invariant(
serverResult && !Array.isArray(serverResult) && "output" in serverResult,
"SSR build failed to produce valid output",
);

const { config } = await loadZudokuConfig(
{ mode: "production", command: "build" },
dir,
);

const { jsEntry, cssEntries } = extractAssets(clientResult);
const base = viteConfig.base ?? "/";
const clientOutDir = viteConfig.environments?.client?.build?.outDir;
const serverOutDir = viteConfig.environments?.ssr?.build?.outDir;

invariant(clientOutDir, "Client build outDir is missing");
invariant(serverOutDir, "Server build outDir is missing");

const jsEntry = clientResult.output.find(
(o) => "isEntry" in o && o.isEntry,
)?.fileName;
const cssEntries = clientResult.output
.filter((o) => o.fileName.endsWith(".css"))
.map((o) => o.fileName);

if (!jsEntry || cssEntries.length === 0) {
throw new Error("Build failed. No js or css assets found");
}

const html = getBuildHtml({
jsEntry: joinUrl(viteClientConfig.base, jsEntry),
cssEntries: cssEntries.map((css) => joinUrl(viteClientConfig.base, css)),
jsEntry: joinUrl(base, jsEntry),
cssEntries: cssEntries.map((css) => joinUrl(base, css)),
dir: config.site?.dir,
});

invariant(viteClientConfig.build?.outDir, "Client build outDir is missing");
invariant(viteServerConfig.build?.outDir, "Server build outDir is missing");

const clientOutDir = viteClientConfig.build.outDir;
const serverOutDir = viteServerConfig.build.outDir;

if (ssr) {
// SSR: bundle entry.js and remove index.html
await bundleSSREntry({
Expand All @@ -111,18 +106,27 @@ export async function runBuild(options: BuildOptions) {

type PrerenderOptions = {
dir: string;
config: Awaited<ReturnType<typeof loadZudokuConfig>>["config"];
config: ConfigWithMeta;
html: string;
clientOutDir: string;
serverOutDir: string;
serverResult: Rollup.RollupOutput;
serverResult: Rolldown.RolldownOutput;
};

const findServerConfigFilename = (result: Rolldown.RolldownOutput) => {
const entry = result.output.find(
(o) => o.type === "chunk" && o.isEntry && o.fileName === "zudoku.config.js",
);
invariant(entry, "Could not find zudoku.config entry in server build output");

return entry.fileName;
};

const runPrerender = async (options: PrerenderOptions) => {
const { dir, config, html, clientOutDir, serverOutDir, serverResult } =
options;
const issuer = await getIssuer(config);
const serverConfigFilename = findOutputPathOfServerConfig(serverResult);
const serverConfigFilename = findServerConfigFilename(serverResult);

try {
const { workerResults, rewrites } = await prerender({
Expand Down
59 changes: 37 additions & 22 deletions packages/zudoku/src/vite/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,29 +134,44 @@ export async function getViteConfig(
},
},
build: {
ssr: configEnv.isSsrBuild,
sourcemap: true,
target: "es2022",
outDir: path.resolve(
path.join(
dir,
"dist",
config.basePath ?? "",
configEnv.isSsrBuild ? "server" : "",
),
),
emptyOutDir: true,
rollupOptions: {
input:
configEnv.command === "build"
? configEnv.isSsrBuild
? ["zudoku/app/entry.server.tsx", config.__meta.configPath]
: "zudoku/app/entry.client.tsx"
: undefined,
external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
},
chunkSizeWarningLimit: 1500,
},
environments: {
client: {
build: {
outDir: path.resolve(path.join(dir, "dist", config.basePath ?? "")),
emptyOutDir: false,
rolldownOptions: {
input:
configEnv.command === "build"
? "zudoku/app/entry.client.tsx"
: undefined,
external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
},
},
},
ssr: {
build: {
outDir: path.resolve(
path.join(dir, "dist", config.basePath ?? "", "server"),
),
emptyOutDir: false,
rolldownOptions: {
logLevel: "warn",
checks: {
pluginTimings: process.env.ZUDOKU_ENV === "internal",
},
input:
configEnv.command === "build"
? ["zudoku/app/entry.server.tsx", config.__meta.configPath]
: undefined,
external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
},
},
},
},
experimental: {
renderBuiltUrl(filename) {
if (cdnUrl?.base && [".js", ".css"].includes(path.extname(filename))) {
Expand All @@ -171,12 +186,10 @@ export async function getViteConfig(
},
},
optimizeDeps: {
esbuildOptions: {
target: "es2022",
},
entries: [path.posix.join(getZudokuRootDir(), "src/{app,lib}/**")],
exclude: ["zudoku"],
include: [
"@mdx-js/react",
"react-dom/client",
"zudoku/icons",
...(process.env.SENTRY_DSN ? ["@sentry/react"] : []),
Expand All @@ -195,6 +208,8 @@ export async function getViteConfig(
removePluginHookHandleHotUpdate: "warn",
removePluginHookSsrArgument: "warn",
removeServerHot: "warn",
removeServerPluginContainer: "warn",
removeServerReloadModule: "warn",
},
};

Expand Down
Loading
Loading