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
4 changes: 2 additions & 2 deletions .github/workflows/preview-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:
runs-on: ubuntu-latest
# Run on internal PRs or external PRs with 'approve public build' label
if: |
(github.event_name == 'pull_request_target' && !github.event.pull_request.head.repo.fork) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'approve public build'))
(github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork && contains(github.event.pull_request.labels.*.name, 'approve public build'))
permissions:
contents: read
id-token: write
Expand Down
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"clean": "git clean -Xfde !.env"
},
"engines": {
"node": ">=20.19.0 <21.0.0 || >=22.7.0",
"node": ">=20.19.0 <21.0.0 || >=22.12.0",
"pnpm": ">=10"
},
"devDependencies": {
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 @@ -248,7 +248,7 @@
"@tanstack/react-query": "5.90.12",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "5.1.0",
"@vitejs/plugin-react": "5.1.3",
"@x0k/json-schema-merge": "1.0.2",
"@zudoku/httpsnippet": "10.0.9",
"@zudoku/react-helmet-async": "2.0.5",
Expand Down Expand Up @@ -298,7 +298,6 @@
"remark-frontmatter": "5.0.0",
"remark-gfm": "4.0.1",
"remark-mdx-frontmatter": "5.2.0",
"rollup": "4.52.5",
"semver": "7.7.4",
"shiki": "3.22.0",
"sitemap": "9.0.0",
Expand All @@ -310,7 +309,7 @@
"unist-util-visit": "5.0.0",
"vaul": "1.1.2",
"vfile": "6.0.3",
"vite": "6.4.1",
"vite": "8.0.0-beta.14",
"yaml": "2.8.2",
"yargs": "18.0.0",
"zod": "4.3.6",
Expand Down
7 changes: 2 additions & 5 deletions packages/zudoku/scripts/generate-types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { mkdir, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import icons from "lucide-react/dist/esm/dynamicIconImports.js";
import { format } from "prettier";

const icons = await import("lucide-react/dist/esm/dynamicIconImports.js").then(
(module) => module.default,
);

const iconNames = Object.keys(icons)
const iconNames = Object.keys(icons.default)
.sort()
.map((icon) => `"${icon}"`)
.join(",");
Expand Down
14 changes: 9 additions & 5 deletions packages/zudoku/src/config/loader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
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, loadEnv as viteLoadEnv } from "vite";
import {
type build,
type ConfigEnv,
runnerImport,
loadEnv as viteLoadEnv,
} from "vite";
import { logger } from "../cli/common/logger.js";
import { runPluginTransformConfig } from "../lib/core/transform-config.js";
import invariant from "../lib/util/invariant.js";
Expand Down Expand Up @@ -77,9 +81,9 @@ async function loadZudokuConfigWithMeta(
return configWithMetadata;
}

export function findOutputPathOfServerConfig(
output: RollupOutput | RollupOutput[] | RollupWatcher,
) {
type BuildResult = Awaited<ReturnType<typeof build>>;

export function findOutputPathOfServerConfig(output: BuildResult) {
if (Array.isArray(output)) {
throw new Error("Expected a single output, but got an array");
}
Expand Down
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
12 changes: 8 additions & 4 deletions packages/zudoku/src/lib/plugins/openapi/client/GraphQLClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,24 @@ const throwIfError = (response: GraphQLResponse<unknown>) => {
};

export class GraphQLClient {
constructor(private readonly config: OpenApiPluginOptions) {}
#config: OpenApiPluginOptions;

constructor(config: OpenApiPluginOptions) {
this.#config = config;
}

#getLocalServer = async () => {
if (!localServerPromise) {
localServerPromise = import("./createServer.js").then((m) =>
m.createServer(this.config),
m.createServer(this.#config),
);
}
return localServerPromise;
};

#executeFetch = async (init: RequestInit): Promise<Response> => {
if (this.config.server) {
return fetch(this.config.server, init);
if (this.#config.server) {
return fetch(this.#config.server, init);
}

const localServer = await this.#getLocalServer();
Expand Down
193 changes: 91 additions & 102 deletions packages/zudoku/src/vite/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { build as viteBuild } from "vite";
import { createBuilder } from "vite";
import { ZuploEnv } from "../app/env.js";
import {
findOutputPathOfServerConfig,
Expand All @@ -18,26 +18,24 @@ const DIST_DIR = "dist";

export async function runBuild(options: { dir: string }) {
try {
// Shouldn't run in parallel because it's potentially racy
const viteClientConfig = await getViteConfig(options.dir, {
const viteConfig = await getViteConfig(options.dir, {
mode: "production",
command: "build",
});
const viteServerConfig = await getViteConfig(options.dir, {
mode: "production",
command: "build",
isSsrBuild: true,
});

// Don't run in parallel because it might overwrite itself
const clientResult = await viteBuild(viteClientConfig);
const serverResult = await viteBuild({
...viteServerConfig,
logLevel: "silent",
});
if (Array.isArray(clientResult)) {
throw new Error("Build failed");
}
const builder = await createBuilder(viteConfig);

invariant(builder.environments.client, "Client environment is missing");
invariant(builder.environments.ssr, "SSR environment is missing");

const clientResult = await builder.build(builder.environments.client);
const serverResult = await builder.build(builder.environments.ssr);

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

const { config } = await loadZudokuConfig(
{ mode: "production", command: "build" },
Expand All @@ -46,108 +44,99 @@ export async function runBuild(options: { dir: string }) {

const issuer = await getIssuer(config);

if ("output" in clientResult) {
const [jsEntry, cssEntries] = [
clientResult.output.find((o) => "isEntry" in o && o.isEntry)?.fileName,
clientResult.output
.filter((o) => o.fileName.endsWith(".css"))
.map((o) => o.fileName),
];
const base = viteConfig.base ?? "/";
const clientOutDir = viteConfig.environments?.client?.build?.outDir;
const serverOutDir = viteConfig.environments?.ssr?.build?.outDir;

if (!jsEntry || cssEntries.length === 0) {
throw new Error("Build failed. No js or css assets found");
}
invariant(clientOutDir, "Client build outDir is missing");
invariant(serverOutDir, "Server build outDir is missing");

const html = getBuildHtml({
jsEntry: joinUrl(viteClientConfig.base, jsEntry),
cssEntries: cssEntries.map((css) =>
joinUrl(viteClientConfig.base, css),
),
dir: config.site?.dir,
});
if (!("output" in clientResult)) {
throw new Error("Client build output is missing");
}

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

invariant(
viteClientConfig.build?.outDir,
"Client build outDir is missing",
);
invariant(
viteServerConfig.build?.outDir,
"Server build outDir is missing",
);
if (!jsEntry || cssEntries.length === 0) {
throw new Error("Build failed. No js or css assets found");
}

try {
const results = await prerender({
html,
dir: options.dir,
basePath: config.basePath,
serverConfigFilename,
writeRedirects: process.env.VERCEL === undefined,
});
const html = getBuildHtml({
jsEntry: joinUrl(base, jsEntry),
cssEntries: cssEntries.map((css) => joinUrl(base, css)),
dir: config.site?.dir,
});

const indexHtml = path.join(
viteClientConfig.build.outDir,
"index.html",
);
const serverConfigFilename = findOutputPathOfServerConfig(serverResult);

try {
const results = await prerender({
html,
dir: options.dir,
basePath: config.basePath,
serverConfigFilename,
writeRedirects: process.env.VERCEL === undefined,
});

if (!results.find((r) => r.outputPath === indexHtml)) {
await writeFile(indexHtml, html, "utf-8");
}
const indexHtml = path.join(clientOutDir, "index.html");

if (!results.find((r) => r.outputPath === indexHtml)) {
await writeFile(indexHtml, html, "utf-8");
}

// find 400.html, 404.html, 500.html
const statusPages = results.flatMap((r) =>
/400|404|500\.html$/.test(r.outputPath) ? r.outputPath : [],
// find 400.html, 404.html, 500.html
const statusPages = results.flatMap((r) =>
/400|404|500\.html$/.test(r.outputPath) ? r.outputPath : [],
);

// move status pages to root path (i.e. without base path)
for (const statusPage of statusPages) {
await rename(
statusPage,
path.join(options.dir, DIST_DIR, path.basename(statusPage)),
);
}

// move status pages to root path (i.e. without base path)
for (const statusPage of statusPages) {
await rename(
statusPage,
path.join(options.dir, DIST_DIR, path.basename(statusPage)),
);
}
// Delete the server build output directory because we don't need it anymore
await rm(serverOutDir, {
recursive: true,
force: true,
});

// Delete the server build output directory because we don't need it anymore
await rm(viteServerConfig.build.outDir, {
if (process.env.VERCEL) {
await mkdir(path.join(options.dir, ".vercel/output/static"), {
recursive: true,
force: true,
});
await rename(
path.join(options.dir, DIST_DIR),
path.join(options.dir, ".vercel/output/static"),
);
}

if (process.env.VERCEL) {
await mkdir(path.join(options.dir, ".vercel/output/static"), {
recursive: true,
});
await rename(
path.join(options.dir, DIST_DIR),
path.join(options.dir, ".vercel/output/static"),
);
}

// Write the build output file
await writeOutput(options.dir, {
config,
redirects: results.flatMap((r) => r.redirect ?? []),
});
// Write the build output file
await writeOutput(options.dir, {
config,
redirects: results.flatMap((r) => r.redirect ?? []),
});

if (ZuploEnv.isZuplo && issuer) {
await writeFile(
path.join(options.dir, DIST_DIR, ".output/zuplo.json"),
JSON.stringify({ issuer }, null, 2),
"utf-8",
);
}
} catch (e) {
// dynamic imports in prerender swallow the stack trace, so we log it here
// biome-ignore lint/suspicious/noConsole: Logging allowed here
console.error(e);
throw e;
if (ZuploEnv.isZuplo && issuer) {
await writeFile(
path.join(options.dir, DIST_DIR, ".output/zuplo.json"),
JSON.stringify({ issuer }, null, 2),
"utf-8",
);
}

return;
} catch (e) {
// dynamic imports in prerender swallow the stack trace, so we log it here
// biome-ignore lint/suspicious/noConsole: Logging allowed here
console.error(e);
throw e;
}

throw new Error("Build failed");
} catch (error) {
// biome-ignore lint/suspicious/noConsole: Logging allowed here
console.error("\n❌ [FATAL] Build error in runBuild:");
Expand Down
Loading