zudoku 0.82.2 → 0.82.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +254 -200
- package/dist/cli/worker.js +4 -0
- package/dist/declarations/app/wrapProtectedRoutes.d.ts +5 -0
- package/dist/declarations/config/plugin-versions.d.ts +5 -0
- package/dist/declarations/vite/package-root.d.ts +1 -0
- package/docs/components/landing-page.mdx +1 -1
- package/package.json +12 -11
- package/src/app/entry.server.tsx +9 -5
- package/src/app/wrapProtectedRoutes.ts +25 -0
- package/src/config/loader.ts +15 -0
- package/src/config/plugin-versions.ts +38 -0
- package/src/lib/components/navigation/NavigationCategory.tsx +3 -2
- package/src/lib/components/navigation/StackRows.tsx +1 -1
- package/src/vite/api/SchemaManager.ts +11 -25
- package/src/vite/plugin-mdx.ts +3 -0
- package/src/vite/prerender/prerender.ts +28 -3
- package/src/vite/prerender/utils.ts +33 -0
- package/src/vite/prerender/worker.ts +8 -1
package/dist/cli/worker.js
CHANGED
|
@@ -2921,6 +2921,7 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2921
2921
|
outputPath,
|
|
2922
2922
|
redirect: { from: pathname, to: redirectTo },
|
|
2923
2923
|
statusCode: response.status,
|
|
2924
|
+
indexStatusCode: response.status,
|
|
2924
2925
|
html: ""
|
|
2925
2926
|
};
|
|
2926
2927
|
}
|
|
@@ -2931,6 +2932,7 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2931
2932
|
}
|
|
2932
2933
|
const fileContent = response.body ? await response.text() : "";
|
|
2933
2934
|
let html = fileContent;
|
|
2935
|
+
let indexStatusCode = response.status;
|
|
2934
2936
|
if (isProtectedRoute) {
|
|
2935
2937
|
const bypassRequest = new Request(url);
|
|
2936
2938
|
const bypassResponse = await server.handleRequest({
|
|
@@ -2946,12 +2948,14 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2946
2948
|
);
|
|
2947
2949
|
}
|
|
2948
2950
|
html = bypassResponse.body ? await bypassResponse.text() : "";
|
|
2951
|
+
indexStatusCode = bypassResponse.status;
|
|
2949
2952
|
}
|
|
2950
2953
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
2951
2954
|
await fs.writeFile(outputPath, fileContent);
|
|
2952
2955
|
return {
|
|
2953
2956
|
outputPath,
|
|
2954
2957
|
statusCode: response.status,
|
|
2958
|
+
indexStatusCode,
|
|
2955
2959
|
redirect: void 0,
|
|
2956
2960
|
html
|
|
2957
2961
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { RouteObject } from "react-router";
|
|
2
2
|
import type { ProtectedRoutesInput } from "../config/validators/ProtectedRoutesSchema.js";
|
|
3
3
|
export declare const wrapProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, isAuthenticated: boolean, basePath?: string) => RouteObject[];
|
|
4
|
+
export declare const wrapProtectedRoutesForRender: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, { isAuthenticated, bypassProtection, basePath, }: {
|
|
5
|
+
isAuthenticated: boolean;
|
|
6
|
+
bypassProtection?: boolean;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
}) => RouteObject[];
|
|
4
9
|
export declare const warnInlineProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, basePath?: string) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const findPackageRoot: (startDir: string) => Promise<string | undefined>;
|
|
@@ -10,7 +10,7 @@ A ready-made, customizable landing page for your developer portal. It comes in t
|
|
|
10
10
|
covering the most common layouts: a centered hero, a two-column split hero, and a compact
|
|
11
11
|
documentation hub.
|
|
12
12
|
|
|
13
|
-
Use it as the `element` of a [custom page](/docs/custom-pages) — typically your home page:
|
|
13
|
+
Use it as the `element` of a [custom page](/docs/guides/custom-pages) — typically your home page:
|
|
14
14
|
|
|
15
15
|
```tsx title=zudoku.config.tsx
|
|
16
16
|
import { LandingPage } from "zudoku/components";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zudoku",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0 <21.0.0 || >=22.12.0"
|
|
@@ -177,8 +177,9 @@
|
|
|
177
177
|
"@envelop/core": "5.5.1",
|
|
178
178
|
"@graphiql/toolkit": "0.12.1",
|
|
179
179
|
"@graphql-typed-document-node/core": "3.2.0",
|
|
180
|
-
"@hono/node-server": "2.0.
|
|
180
|
+
"@hono/node-server": "2.0.8",
|
|
181
181
|
"@lekoarts/rehype-meta-as-attributes": "3.0.3",
|
|
182
|
+
"@mdx-js/mdx": "3.1.0",
|
|
182
183
|
"@mdx-js/react": "3.1.1",
|
|
183
184
|
"@mdx-js/rollup": "3.1.1",
|
|
184
185
|
"@pothos/core": "4.12.0",
|
|
@@ -206,14 +207,14 @@
|
|
|
206
207
|
"@radix-ui/react-toggle-group": "1.1.11",
|
|
207
208
|
"@radix-ui/react-tooltip": "1.2.8",
|
|
208
209
|
"@radix-ui/react-visually-hidden": "1.2.4",
|
|
209
|
-
"@scalar/openapi-parser": "0.
|
|
210
|
+
"@scalar/openapi-parser": "0.28.4",
|
|
210
211
|
"@scalar/snippetz": "0.9.11",
|
|
211
|
-
"@sentry/node": "10.
|
|
212
|
+
"@sentry/node": "10.62.0",
|
|
212
213
|
"@shikijs/langs": "4.2.0",
|
|
213
214
|
"@shikijs/rehype": "4.2.0",
|
|
214
215
|
"@shikijs/themes": "4.2.0",
|
|
215
216
|
"@shikijs/transformers": "4.2.0",
|
|
216
|
-
"@tailwindcss/typography": "0.5.
|
|
217
|
+
"@tailwindcss/typography": "0.5.20",
|
|
217
218
|
"@tailwindcss/vite": "4.3.0",
|
|
218
219
|
"@tanem/react-nprogress": "6.0.3",
|
|
219
220
|
"@tanstack/react-query": "5.101.0",
|
|
@@ -242,7 +243,7 @@
|
|
|
242
243
|
"hast-util-heading-rank": "3.0.0",
|
|
243
244
|
"hast-util-to-jsx-runtime": "2.3.6",
|
|
244
245
|
"hast-util-to-string": "3.0.1",
|
|
245
|
-
"hono": "4.12.
|
|
246
|
+
"hono": "4.12.27",
|
|
246
247
|
"http-terminator": "3.2.0",
|
|
247
248
|
"javascript-stringify": "2.1.0",
|
|
248
249
|
"jose": "6.2.3",
|
|
@@ -277,7 +278,7 @@
|
|
|
277
278
|
"remark-frontmatter": "5.0.0",
|
|
278
279
|
"remark-gfm": "4.0.1",
|
|
279
280
|
"remark-mdx-frontmatter": "5.2.0",
|
|
280
|
-
"semver": "7.8.
|
|
281
|
+
"semver": "7.8.5",
|
|
281
282
|
"shiki": "4.2.0",
|
|
282
283
|
"sitemap": "9.0.1",
|
|
283
284
|
"strip-ansi": "7.2.0",
|
|
@@ -295,7 +296,7 @@
|
|
|
295
296
|
"zustand": "5.0.14"
|
|
296
297
|
},
|
|
297
298
|
"devDependencies": {
|
|
298
|
-
"@graphql-codegen/cli": "7.1.
|
|
299
|
+
"@graphql-codegen/cli": "7.1.3",
|
|
299
300
|
"@inkeep/cxkit-types": "0.5.119",
|
|
300
301
|
"@testing-library/dom": "10.4.1",
|
|
301
302
|
"@testing-library/jest-dom": "6.9.1",
|
|
@@ -307,14 +308,14 @@
|
|
|
307
308
|
"@types/hast": "3.0.4",
|
|
308
309
|
"@types/json-schema": "7.0.15",
|
|
309
310
|
"@types/mdast": "4.0.4",
|
|
310
|
-
"@types/mdx": "2.0.
|
|
311
|
+
"@types/mdx": "2.0.14",
|
|
311
312
|
"@types/node": "22.19.1",
|
|
312
313
|
"@types/react-is": "19.2.0",
|
|
313
314
|
"@types/semver": "7.7.1",
|
|
314
315
|
"@types/unist": "3.0.3",
|
|
315
316
|
"@types/yargs": "17.0.35",
|
|
316
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
317
|
-
"@vitest/coverage-v8": "4.1.
|
|
317
|
+
"@typescript/native-preview": "7.0.0-dev.20260624.1",
|
|
318
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
318
319
|
"happy-dom": "20.10.2",
|
|
319
320
|
"oxc-parser": "^0.135.0",
|
|
320
321
|
"react": "19.2.7",
|
package/src/app/entry.server.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import type { Adapter } from "./adapter.js";
|
|
|
26
26
|
import { getRoutesByConfig } from "./main.js";
|
|
27
27
|
import { protectChunks as rawProtectChunks } from "./protectChunks.js";
|
|
28
28
|
import { getSsrCacheControl } from "./ssrCacheControl.js";
|
|
29
|
-
import {
|
|
29
|
+
import { wrapProtectedRoutesForRender } from "./wrapProtectedRoutes.js";
|
|
30
30
|
|
|
31
31
|
export { getRoutesByConfig };
|
|
32
32
|
export type { Adapter, AdapterContext } from "./adapter.js";
|
|
@@ -104,12 +104,16 @@ export const handleRequest = async ({
|
|
|
104
104
|
const ssrAuth = await resolveSsrAuth(request);
|
|
105
105
|
|
|
106
106
|
// No-op lazy() on protected subtrees for unauthed requests so loaders
|
|
107
|
-
// don't run for a 401 render.
|
|
108
|
-
|
|
107
|
+
// don't run for a 401 render. A bypass render (search-index pass) keeps the
|
|
108
|
+
// real content so Pagefind can index protected routes.
|
|
109
|
+
const effectiveRoutes = wrapProtectedRoutesForRender(
|
|
109
110
|
routes,
|
|
110
111
|
config.protectedRoutes,
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
{
|
|
113
|
+
isAuthenticated: !!ssrAuth?.profile,
|
|
114
|
+
bypassProtection,
|
|
115
|
+
basePath,
|
|
116
|
+
},
|
|
113
117
|
);
|
|
114
118
|
|
|
115
119
|
const { query, dataRoutes } = createStaticHandler(effectiveRoutes, {
|
|
@@ -47,6 +47,31 @@ export const wrapProtectedRoutes = (
|
|
|
47
47
|
});
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// Route-wrapping decision for an SSR/SSG render. The prerender does a second
|
|
51
|
+
// "bypass" pass over protected routes to build the search index; that pass must
|
|
52
|
+
// render the real (unstubbed) protected content, so it is treated as
|
|
53
|
+
// authenticated here. Without this, the bypass pass renders an empty shell and
|
|
54
|
+
// the Pagefind index for protected routes ends up empty (issue #2672).
|
|
55
|
+
export const wrapProtectedRoutesForRender = (
|
|
56
|
+
routes: RouteObject[],
|
|
57
|
+
protectedRoutes: ProtectedRoutesInput,
|
|
58
|
+
{
|
|
59
|
+
isAuthenticated,
|
|
60
|
+
bypassProtection,
|
|
61
|
+
basePath,
|
|
62
|
+
}: {
|
|
63
|
+
isAuthenticated: boolean;
|
|
64
|
+
bypassProtection?: boolean;
|
|
65
|
+
basePath?: string;
|
|
66
|
+
},
|
|
67
|
+
): RouteObject[] =>
|
|
68
|
+
wrapProtectedRoutes(
|
|
69
|
+
routes,
|
|
70
|
+
protectedRoutes,
|
|
71
|
+
isAuthenticated || bypassProtection === true,
|
|
72
|
+
basePath,
|
|
73
|
+
);
|
|
74
|
+
|
|
50
75
|
// Inline elements can't be chunk-isolated; RouteGuard still blocks render,
|
|
51
76
|
// but the JS ships in the main bundle. Only meaningful in dev.
|
|
52
77
|
export const warnInlineProtectedRoutes = (
|
package/src/config/loader.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getZudokuRootDir } from "../cli/common/package-json.js";
|
|
|
12
12
|
import { runPluginTransformConfig } from "../lib/core/transform-config.js";
|
|
13
13
|
import invariant from "../lib/util/invariant.js";
|
|
14
14
|
import { fileExists } from "./file-exists.js";
|
|
15
|
+
import { getPluginVersions } from "./plugin-versions.js";
|
|
15
16
|
import type {
|
|
16
17
|
ResolvedZudokuConfig,
|
|
17
18
|
ZudokuConfig,
|
|
@@ -240,6 +241,20 @@ export async function loadZudokuConfig(
|
|
|
240
241
|
{ timestamp: true },
|
|
241
242
|
);
|
|
242
243
|
|
|
244
|
+
// Surface which versions of external plugins (created via `createPlugin`)
|
|
245
|
+
// the build is running, so build logs can pin down e.g. the monetization
|
|
246
|
+
// plugin version without each plugin having to log it itself.
|
|
247
|
+
const pluginVersions = await getPluginVersions(config.__pluginDirs ?? []);
|
|
248
|
+
if (pluginVersions.length > 0) {
|
|
249
|
+
logger.info(
|
|
250
|
+
colors.cyan(`loaded plugins `) +
|
|
251
|
+
colors.dim(
|
|
252
|
+
pluginVersions.map((p) => `${p.name}@${p.version}`).join(", "),
|
|
253
|
+
),
|
|
254
|
+
{ timestamp: true },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
243
258
|
return { config, envPrefix, publicEnv };
|
|
244
259
|
} catch (error) {
|
|
245
260
|
const lastValid = getConfig();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findPackageRoot } from "../vite/package-root.js";
|
|
4
|
+
|
|
5
|
+
export type PluginVersion = { name: string; version: string };
|
|
6
|
+
|
|
7
|
+
// Reads the `name`@`version` from each plugin dir's nearest package.json,
|
|
8
|
+
// deduped by package name.
|
|
9
|
+
export const getPluginVersions = async (
|
|
10
|
+
pluginDirs: readonly string[],
|
|
11
|
+
): Promise<PluginVersion[]> => {
|
|
12
|
+
const roots = await Promise.all(
|
|
13
|
+
[...new Set(pluginDirs)].map(findPackageRoot),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const resolved = await Promise.all(
|
|
17
|
+
roots.map(async (root): Promise<PluginVersion | undefined> => {
|
|
18
|
+
if (!root) return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(
|
|
21
|
+
await readFile(path.join(root, "package.json"), "utf-8"),
|
|
22
|
+
) as { name?: string; version?: string };
|
|
23
|
+
if (!pkg.name) return undefined;
|
|
24
|
+
return { name: pkg.name, version: pkg.version ?? "unknown" };
|
|
25
|
+
} catch {
|
|
26
|
+
// A plugin without a readable package.json simply isn't reported.
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const byName = new Map<string, PluginVersion>();
|
|
33
|
+
for (const version of resolved) {
|
|
34
|
+
if (version && !byName.has(version.name)) byName.set(version.name, version);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [...byName.values()];
|
|
38
|
+
};
|
|
@@ -63,11 +63,12 @@ const NavigationCategoryInner = ({
|
|
|
63
63
|
setOpen((prev) => !prev);
|
|
64
64
|
setHasInteracted(true);
|
|
65
65
|
}}
|
|
66
|
-
variant="
|
|
66
|
+
variant="none"
|
|
67
67
|
size="icon"
|
|
68
68
|
aria-label={open ? "Collapse section" : "Expand section"}
|
|
69
69
|
aria-expanded={open}
|
|
70
|
-
|
|
70
|
+
// -my-0.5 keeps the chevron from making folder rows taller than plain ones
|
|
71
|
+
className="size-6 -my-0.5"
|
|
71
72
|
>
|
|
72
73
|
<ChevronRightIcon
|
|
73
74
|
size={16}
|
|
@@ -24,7 +24,7 @@ export const StackDrillRow = ({
|
|
|
24
24
|
className={cn(navigationListItem(), "group justify-between", className)}
|
|
25
25
|
>
|
|
26
26
|
<span className="flex items-center gap-2 truncate">{children}</span>
|
|
27
|
-
<span className="grid size-6 shrink-0 place-items-center">
|
|
27
|
+
<span className="grid size-6 -my-0.5 shrink-0 place-items-center">
|
|
28
28
|
<ChevronRightIcon size={16} />
|
|
29
29
|
</span>
|
|
30
30
|
</Link>
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type JSONSchema,
|
|
6
|
-
} from "@apidevtools/json-schema-ref-parser";
|
|
7
|
-
import { upgrade, validate } from "@scalar/openapi-parser";
|
|
3
|
+
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
|
|
4
|
+
import { upgrade } from "@scalar/openapi-parser";
|
|
8
5
|
import { deepEqual } from "fast-equals";
|
|
9
6
|
import type { LoadedConfig } from "../../config/config.js";
|
|
10
7
|
import type { Processor } from "../../config/validators/BuildSchema.js";
|
|
@@ -98,7 +95,14 @@ export class SchemaManager {
|
|
|
98
95
|
this.storeDir = storeDir;
|
|
99
96
|
this.config = config;
|
|
100
97
|
this.processors = [
|
|
101
|
-
({ schema }) =>
|
|
98
|
+
({ schema }) => {
|
|
99
|
+
const upgraded = upgrade(schema as unknown as Record<string, unknown>)
|
|
100
|
+
.specification as OpenAPIDocument;
|
|
101
|
+
if (upgraded?.info && typeof upgraded.info.version !== "string") {
|
|
102
|
+
upgraded.info.version = "0.0.1";
|
|
103
|
+
}
|
|
104
|
+
return upgraded;
|
|
105
|
+
},
|
|
102
106
|
flattenAllOfProcessor,
|
|
103
107
|
...processors,
|
|
104
108
|
];
|
|
@@ -147,7 +151,6 @@ export class SchemaManager {
|
|
|
147
151
|
this.referencedBy.get(file)?.add(filePath);
|
|
148
152
|
});
|
|
149
153
|
|
|
150
|
-
const validatedSchema = await this.validateSchema(schema, filePath);
|
|
151
154
|
const processedSchema = await this.processors.reduce(
|
|
152
155
|
async (schema, processor) =>
|
|
153
156
|
processor({
|
|
@@ -162,7 +165,7 @@ export class SchemaManager {
|
|
|
162
165
|
},
|
|
163
166
|
}),
|
|
164
167
|
}),
|
|
165
|
-
Promise.resolve(
|
|
168
|
+
Promise.resolve(schema as OpenAPIDocument),
|
|
166
169
|
);
|
|
167
170
|
|
|
168
171
|
const processedTime = Date.now();
|
|
@@ -365,21 +368,4 @@ export class SchemaManager {
|
|
|
365
368
|
`${fileName}${suffix}${extension}`,
|
|
366
369
|
);
|
|
367
370
|
};
|
|
368
|
-
|
|
369
|
-
private validateSchema = async (
|
|
370
|
-
schema: JSONSchema,
|
|
371
|
-
filePath: string,
|
|
372
|
-
): Promise<OpenAPIDocument> => {
|
|
373
|
-
const validated = await validate(schema);
|
|
374
|
-
if (validated.errors?.length) {
|
|
375
|
-
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
376
|
-
console.warn(`Schema warnings in ${filePath}:`);
|
|
377
|
-
for (const error of validated.errors) {
|
|
378
|
-
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
379
|
-
console.warn(error);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return schema as OpenAPIDocument;
|
|
384
|
-
};
|
|
385
371
|
}
|
package/src/vite/plugin-mdx.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import rehypeMetaAsAttributes from "@lekoarts/rehype-meta-as-attributes";
|
|
2
|
+
import { nodeTypes } from "@mdx-js/mdx";
|
|
2
3
|
import mdx from "@mdx-js/rollup";
|
|
3
4
|
import type { Root as HastRoot } from "hast";
|
|
4
5
|
import { toString as hastToString } from "hast-util-to-string";
|
|
5
6
|
import rehypeMdxImportMedia from "rehype-mdx-import-media";
|
|
7
|
+
import rehypeRaw from "rehype-raw";
|
|
6
8
|
import rehypeSlug from "rehype-slug";
|
|
7
9
|
import remarkComment from "remark-comment";
|
|
8
10
|
import remarkDirective from "remark-directive";
|
|
@@ -128,6 +130,7 @@ const viteMdxPlugin = async (): Promise<Plugin> => {
|
|
|
128
130
|
: [...defaultRemarkPlugins, ...(buildConfig?.remarkPlugins ?? [])];
|
|
129
131
|
|
|
130
132
|
const defaultRehypePlugins = [
|
|
133
|
+
[rehypeRaw, { passThrough: nodeTypes }],
|
|
131
134
|
rehypeSlug,
|
|
132
135
|
rehypeExtractTocWithJsx,
|
|
133
136
|
rehypeExtractTocWithJsxExport,
|
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
} from "../plugin-markdown-export.js";
|
|
21
21
|
import { isTTY, throttle, writeLine } from "../reporter.js";
|
|
22
22
|
import { generateSitemap } from "../sitemap.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
routesToPaths,
|
|
25
|
+
routesToRewrites,
|
|
26
|
+
selectPagesToIndex,
|
|
27
|
+
} from "./utils.js";
|
|
24
28
|
import type { StaticWorkerData, WorkerData } from "./worker.js";
|
|
25
29
|
|
|
26
30
|
const Piscina = PiscinaImport as unknown as typeof PiscinaImport.default;
|
|
@@ -28,7 +32,13 @@ const Piscina = PiscinaImport as unknown as typeof PiscinaImport.default;
|
|
|
28
32
|
export type WorkerResult = {
|
|
29
33
|
outputPath: string;
|
|
30
34
|
html: string;
|
|
35
|
+
// Status of the page as served statically (the file written to disk). For a
|
|
36
|
+
// protected route during SSG this is 401 (the sign-in page).
|
|
31
37
|
statusCode: number;
|
|
38
|
+
// Status of the render whose HTML is fed to the search index. Equals
|
|
39
|
+
// `statusCode` for normal routes; for protected routes it's the bypass
|
|
40
|
+
// render (200) so the full content gets indexed (issue #2672).
|
|
41
|
+
indexStatusCode: number;
|
|
32
42
|
redirect?: { from: string; to: string };
|
|
33
43
|
};
|
|
34
44
|
|
|
@@ -173,9 +183,24 @@ export const prerender = async ({
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
if (pagefindIndex) {
|
|
176
|
-
const
|
|
177
|
-
|
|
186
|
+
const { include: pagesToIndex, exclude } = selectPagesToIndex(
|
|
187
|
+
workerResults,
|
|
188
|
+
paths,
|
|
178
189
|
);
|
|
190
|
+
|
|
191
|
+
// Surface anything dropped from the index (e.g. a protected route whose
|
|
192
|
+
// bypass render returned >= 400). A page silently missing from the index
|
|
193
|
+
// is the exact symptom of #2672, so make it visible rather than quiet.
|
|
194
|
+
if (exclude.length > 0) {
|
|
195
|
+
const details = exclude
|
|
196
|
+
.map(({ url, status }) => `${url} (${status})`)
|
|
197
|
+
.join(", ");
|
|
198
|
+
logger.warn(
|
|
199
|
+
colors.yellow(
|
|
200
|
+
`⚠ ${exclude.length} route(s) excluded from the search index (render status >= 400): ${details}`,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
179
204
|
// Batch size caps concurrent IPC writes to the pagefind child process;
|
|
180
205
|
// higher values can overflow its pipe buffer and trigger ENOBUFS.
|
|
181
206
|
const BATCH_SIZE = 40;
|
|
@@ -74,3 +74,36 @@ export const routesToPaths = (routes: RouteObject[]): string[] =>
|
|
|
74
74
|
|
|
75
75
|
export const routesToRewrites = (routes: RouteObject[]): RouteRewrite[] =>
|
|
76
76
|
collectRewrites(resolveRoutes(routes));
|
|
77
|
+
|
|
78
|
+
type IndexablePage = { indexStatusCode: number; html: string };
|
|
79
|
+
|
|
80
|
+
type IndexSelection = {
|
|
81
|
+
include: { url: string; html: string }[];
|
|
82
|
+
exclude: { url: string; status: number }[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Splits prerendered pages into those added to the search index and those
|
|
86
|
+
// skipped because their indexed render failed. `indexStatusCode` is the status
|
|
87
|
+
// of the render whose HTML is indexed: for protected routes that's the bypass
|
|
88
|
+
// render (200), not the gated main render (401), so protected pages aren't
|
|
89
|
+
// silently dropped (issue #2672). Excluded pages are returned rather than
|
|
90
|
+
// dropped in silence so the caller can warn about them, since a protected page
|
|
91
|
+
// missing from the index is exactly the symptom this fix targets.
|
|
92
|
+
export const selectPagesToIndex = (
|
|
93
|
+
pages: IndexablePage[],
|
|
94
|
+
paths: string[],
|
|
95
|
+
): IndexSelection => {
|
|
96
|
+
const withUrl = pages.flatMap(({ indexStatusCode, html }, i) => {
|
|
97
|
+
const url = paths[i];
|
|
98
|
+
return url === undefined ? [] : [{ url, html, indexStatusCode }];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
include: withUrl
|
|
103
|
+
.filter((p) => p.indexStatusCode < 400)
|
|
104
|
+
.map(({ url, html }) => ({ url, html })),
|
|
105
|
+
exclude: withUrl
|
|
106
|
+
.filter((p) => p.indexStatusCode >= 400)
|
|
107
|
+
.map(({ url, indexStatusCode }) => ({ url, status: indexStatusCode })),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -64,6 +64,7 @@ const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
|
|
|
64
64
|
outputPath,
|
|
65
65
|
redirect: { from: pathname, to: redirectTo },
|
|
66
66
|
statusCode: response.status,
|
|
67
|
+
indexStatusCode: response.status,
|
|
67
68
|
html: "",
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -77,8 +78,12 @@ const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
|
|
|
77
78
|
// Get HTML content for file write
|
|
78
79
|
const fileContent = response.body ? await response.text() : "";
|
|
79
80
|
|
|
80
|
-
// For protected routes, do a second render with protection bypassed
|
|
81
|
+
// For protected routes, do a second render with protection bypassed so the
|
|
82
|
+
// search index gets the full content instead of the gated sign-in page. The
|
|
83
|
+
// file on disk stays the gated (401) render; only `html`/`indexStatusCode`
|
|
84
|
+
// reflect the bypass render.
|
|
81
85
|
let html = fileContent;
|
|
86
|
+
let indexStatusCode = response.status;
|
|
82
87
|
if (isProtectedRoute) {
|
|
83
88
|
const bypassRequest = new Request(url);
|
|
84
89
|
const bypassResponse = await server.handleRequest({
|
|
@@ -96,6 +101,7 @@ const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
|
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
html = bypassResponse.body ? await bypassResponse.text() : "";
|
|
104
|
+
indexStatusCode = bypassResponse.status;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// Write the file
|
|
@@ -105,6 +111,7 @@ const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
|
|
|
105
111
|
return {
|
|
106
112
|
outputPath,
|
|
107
113
|
statusCode: response.status,
|
|
114
|
+
indexStatusCode,
|
|
108
115
|
redirect: undefined,
|
|
109
116
|
html,
|
|
110
117
|
};
|