create-zudo-doc 0.2.0-next.9 → 0.2.1
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/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +7 -7
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +10 -4
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/components/theme-toggle.tsx +0 -107
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
package/dist/api.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { scaffold } from "./scaffold.js";
|
|
3
|
-
import { installDependencies } from "./utils.js";
|
|
3
|
+
import { installDependencies, validateProjectName } from "./utils.js";
|
|
4
4
|
export async function createZudoDoc(options) {
|
|
5
5
|
const { install = false, ...rest } = options;
|
|
6
|
+
const nameError = validateProjectName(rest.projectName);
|
|
7
|
+
if (nameError)
|
|
8
|
+
throw new Error(`Invalid projectName: ${nameError}`);
|
|
6
9
|
const choices = { ...rest, defaultLang: rest.defaultLang ?? "en" };
|
|
7
10
|
await scaffold(choices);
|
|
8
11
|
const targetDir = path.resolve(process.cwd(), choices.projectName);
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import minimist from "minimist";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { FEATURES, SINGLE_SCHEMES, SUPPORTED_LANGS } from "./constants.js";
|
|
4
|
+
import { validateProjectName } from "./utils.js";
|
|
4
5
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
5
6
|
const raw = minimist(argv, {
|
|
6
7
|
string: [
|
|
@@ -146,12 +147,9 @@ export function validateArgs(args) {
|
|
|
146
147
|
return `--scheme is only valid with --color-scheme-mode single`;
|
|
147
148
|
}
|
|
148
149
|
if (args.name) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (/[<>:"|?*\\]/.test(args.name)) {
|
|
153
|
-
return "Project name contains invalid characters";
|
|
154
|
-
}
|
|
150
|
+
const nameError = validateProjectName(args.name);
|
|
151
|
+
if (nameError)
|
|
152
|
+
return nameError;
|
|
155
153
|
}
|
|
156
154
|
return null;
|
|
157
155
|
}
|
package/dist/preset.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { FEATURES, SINGLE_SCHEMES, SUPPORTED_LANGS } from "./constants.js";
|
|
3
|
+
import { validateProjectName } from "./utils.js";
|
|
3
4
|
const VALID_HEADER_RIGHT_COMPONENTS = new Set([
|
|
4
5
|
"theme-toggle",
|
|
5
6
|
"language-switcher",
|
|
@@ -34,6 +35,16 @@ export function validatePreset(json) {
|
|
|
34
35
|
return "Preset must be a JSON object";
|
|
35
36
|
}
|
|
36
37
|
const p = json;
|
|
38
|
+
if (p.projectName !== undefined) {
|
|
39
|
+
// Untyped JSON — guard the type before the string validator, otherwise
|
|
40
|
+
// RegExp.test would coerce numbers/booleans and could accept them.
|
|
41
|
+
if (typeof p.projectName !== "string") {
|
|
42
|
+
return "Invalid projectName: must be a string";
|
|
43
|
+
}
|
|
44
|
+
const nameError = validateProjectName(p.projectName);
|
|
45
|
+
if (nameError)
|
|
46
|
+
return `Invalid projectName: ${nameError}`;
|
|
47
|
+
}
|
|
37
48
|
if (p.features !== undefined && !Array.isArray(p.features)) {
|
|
38
49
|
return `"features" must be an array in preset`;
|
|
39
50
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { LIGHT_DARK_PAIRINGS, SINGLE_SCHEMES, FEATURES, SUPPORTED_LANGS } from "./constants.js";
|
|
3
|
+
import { validateProjectName } from "./utils.js";
|
|
3
4
|
export async function runPrompts(prefilled = {}) {
|
|
4
5
|
// 1. Project name
|
|
5
6
|
let projectName;
|
|
@@ -12,12 +13,7 @@ export async function runPrompts(prefilled = {}) {
|
|
|
12
13
|
placeholder: "my-docs",
|
|
13
14
|
defaultValue: "my-docs",
|
|
14
15
|
validate(value) {
|
|
15
|
-
|
|
16
|
-
return "Project name is required";
|
|
17
|
-
if (/^[./]|\.\./.test(value))
|
|
18
|
-
return "Project name must not contain path traversal characters";
|
|
19
|
-
if (/[<>:"|?*\\]/.test(value))
|
|
20
|
-
return "Project name contains invalid characters";
|
|
16
|
+
return validateProjectName(value) ?? undefined;
|
|
21
17
|
},
|
|
22
18
|
});
|
|
23
19
|
if (p.isCancel(result))
|
package/dist/scaffold.js
CHANGED
|
@@ -264,16 +264,21 @@ function generatePackageJson(choices) {
|
|
|
264
264
|
// next.35 fixes resolve_links rewriting bare same-page `[text](#anchor)` /
|
|
265
265
|
// `[text](?query)` links to `/<parent-dir>/#anchor` (zudolab/zudo-doc#1948,
|
|
266
266
|
// upstream Takazudo/zudo-front-builder#875).
|
|
267
|
-
|
|
268
|
-
"
|
|
267
|
+
// next.38 adds client scripts (`.client.*` + `clientScript()`), the
|
|
268
|
+
// `when="media"` island strategy, exported VNode types, and stricter
|
|
269
|
+
// cross-file anchor validation. BREAKING upstream: the no-op
|
|
270
|
+
// `linkValidation.allowExternal` knob was removed — neither the host nor
|
|
271
|
+
// the generated config ever emitted it, so no migration is needed here.
|
|
272
|
+
"@takazudo/zfb": "0.1.0-next.38",
|
|
273
|
+
"@takazudo/zfb-runtime": "0.1.0-next.38",
|
|
269
274
|
// zfb-adapter-cloudflare — required for any route with `prerender = false`.
|
|
270
275
|
// Pinned in lockstep with @takazudo/zfb.
|
|
271
|
-
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.
|
|
276
|
+
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.38",
|
|
272
277
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
273
278
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
274
279
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
275
280
|
// version moves, so a fresh scaffold pulls the version we just published.
|
|
276
|
-
"@takazudo/zudo-doc": "^0.2.
|
|
281
|
+
"@takazudo/zudo-doc": "^0.2.1",
|
|
277
282
|
// zod — used by the generated zfb.config.ts. zfb-config-gen emits
|
|
278
283
|
// `import { z } from "zod"` for the content-collection schema +
|
|
279
284
|
// `z.toJSONSchema(...)` conversion. Without this dep, the consumer
|
|
@@ -297,11 +302,9 @@ function generatePackageJson(choices) {
|
|
|
297
302
|
"@shikijs/transformers": "^4.0.0",
|
|
298
303
|
clsx: "^2.1.0",
|
|
299
304
|
"gray-matter": "^4.0.0",
|
|
300
|
-
"github-slugger": "^2.0.0",
|
|
301
305
|
mermaid: "^11.12.3",
|
|
302
306
|
"remark-cjk-friendly": "^2.0.1",
|
|
303
307
|
"remark-directive": "^3.0.0",
|
|
304
|
-
"unist-util-visit": "^5.1.0",
|
|
305
308
|
// katex — server-side LaTeX renderer used by the always-on
|
|
306
309
|
// pages/lib/_math-block.tsx (called from pages/_mdx-components.ts
|
|
307
310
|
// for `$…$` and `$$…$$` math nodes). Caught by W6B (#1735)
|
|
@@ -313,8 +316,6 @@ function generatePackageJson(choices) {
|
|
|
313
316
|
"@tailwindcss/vite": "^4.2.0",
|
|
314
317
|
tailwindcss: "^4.2.0",
|
|
315
318
|
typescript: "^5.9.0",
|
|
316
|
-
"@types/hast": "^3.0.4",
|
|
317
|
-
"@types/mdast": "^4.0.4",
|
|
318
319
|
"@types/node": "^22.0.0",
|
|
319
320
|
"@types/react": "^19.2.0", // needed for preact/compat type resolution
|
|
320
321
|
"html-validate": "^10.0.0",
|
|
@@ -331,7 +332,7 @@ function generatePackageJson(choices) {
|
|
|
331
332
|
// @takazudo/zudo-doc/integrations/doc-history which in turn imports
|
|
332
333
|
// @takazudo/zudo-doc-history-server/git-history. Without this dep the
|
|
333
334
|
// plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
|
|
334
|
-
deps["@takazudo/zudo-doc-history-server"] = "^0.2.
|
|
335
|
+
deps["@takazudo/zudo-doc-history-server"] = "^0.2.1";
|
|
335
336
|
// W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
|
|
336
337
|
// run the v2 runtime in a TS-aware Node subprocess; without tsx the
|
|
337
338
|
// plugin's preBuild step exits with ENOENT before zfb finishes config
|
|
@@ -363,6 +364,11 @@ function generatePackageJson(choices) {
|
|
|
363
364
|
build: "zfb build",
|
|
364
365
|
preview: "zfb preview",
|
|
365
366
|
check: "zfb check",
|
|
367
|
+
// NOTE: no `check:pages` here — the host repo's pages/ typecheck
|
|
368
|
+
// (tsconfig.pages.json, #2018) is host-only for now. The base template's
|
|
369
|
+
// no-op feature stubs (e.g. doc-history.tsx) are not type-clean against
|
|
370
|
+
// the pages/lib call sites, so emitting the script would fail on a fresh
|
|
371
|
+
// scaffold. Revisit once the template stubs carry typed props.
|
|
366
372
|
"check:html": "html-validate \"dist/**/*.html\"",
|
|
367
373
|
};
|
|
368
374
|
if (choices.features.includes("tagGovernance")) {
|
package/dist/settings-gen.js
CHANGED
|
@@ -43,7 +43,7 @@ export function generateSettingsFile(choices) {
|
|
|
43
43
|
lines.push(` lightScheme: ${JSON.stringify(choices.lightScheme ?? "GitHub Light")},`);
|
|
44
44
|
lines.push(` darkScheme: ${JSON.stringify(choices.darkScheme ?? "GitHub Dark")},`);
|
|
45
45
|
lines.push(` respectPrefersColorScheme: ${choices.respectPrefersColorScheme ?? true},`);
|
|
46
|
-
lines.push(` } satisfies ColorModeConfig,`);
|
|
46
|
+
lines.push(` } satisfies ColorModeConfig as ColorModeConfig | false,`);
|
|
47
47
|
}
|
|
48
48
|
lines.push(` siteName: ${JSON.stringify(capitalize(choices.projectName.replace(/-/g, " ")))},`);
|
|
49
49
|
lines.push(` siteDescription: "" as string,`);
|
|
@@ -60,16 +60,16 @@ export function generateSettingsFile(choices) {
|
|
|
60
60
|
}
|
|
61
61
|
lines.push(` siteUrl: "" as string,`);
|
|
62
62
|
lines.push(` docsDir: "src/content/docs",`);
|
|
63
|
-
lines.push(` defaultLocale: ${JSON.stringify(choices.defaultLang ?? "en")} as
|
|
63
|
+
lines.push(` defaultLocale: ${JSON.stringify(choices.defaultLang ?? "en")} as const,`);
|
|
64
64
|
if (choices.features.includes("i18n")) {
|
|
65
65
|
const secondaryLang = getSecondaryLang(choices.defaultLang);
|
|
66
66
|
const secondaryLabel = getLangLabel(secondaryLang);
|
|
67
67
|
lines.push(` locales: {`);
|
|
68
68
|
lines.push(` ${secondaryLang}: { label: ${JSON.stringify(secondaryLabel)}, dir: "src/content/docs-${secondaryLang}" },`);
|
|
69
|
-
lines.push(` }
|
|
69
|
+
lines.push(` } satisfies Record<string, LocaleConfig>,`);
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
|
-
lines.push(` locales: {}
|
|
72
|
+
lines.push(` locales: {} satisfies Record<string, LocaleConfig>,`);
|
|
73
73
|
}
|
|
74
74
|
// mermaid is controlled by the markdown.features block in zfb.config.ts
|
|
75
75
|
// (zfb next.12+). This field is retained for compatibility with framework
|
|
@@ -159,7 +159,7 @@ export function generateSettingsFile(choices) {
|
|
|
159
159
|
}
|
|
160
160
|
lines.push(` htmlPreview: undefined as HtmlPreviewConfig | undefined,`);
|
|
161
161
|
if (choices.features.includes("versioning")) {
|
|
162
|
-
lines.push(` versions: [] as VersionConfig[],`);
|
|
162
|
+
lines.push(` versions: [] satisfies VersionConfig[] as VersionConfig[] | false,`);
|
|
163
163
|
}
|
|
164
164
|
else {
|
|
165
165
|
lines.push(` versions: false as VersionConfig[] | false,`);
|
|
@@ -219,7 +219,7 @@ export function generateSettingsFile(choices) {
|
|
|
219
219
|
if (choices.features.includes("changelog")) {
|
|
220
220
|
lines.push(` { label: "Changelog", path: "/docs/changelog", categoryMatch: "changelog" },`);
|
|
221
221
|
}
|
|
222
|
-
lines.push(` ] as HeaderNavItem[],`);
|
|
222
|
+
lines.push(` ] satisfies HeaderNavItem[] as HeaderNavItem[],`);
|
|
223
223
|
lines.push(` headerRightItems: [`);
|
|
224
224
|
if (choices.headerRightItems !== undefined) {
|
|
225
225
|
// User-supplied override (including empty array): emit each entry verbatim,
|
|
@@ -250,7 +250,7 @@ export function generateSettingsFile(choices) {
|
|
|
250
250
|
lines.push(` { type: "component", component: "language-switcher" },`);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
-
lines.push(` ] as HeaderRightItem[],`);
|
|
253
|
+
lines.push(` ] satisfies HeaderRightItem[] as HeaderRightItem[],`);
|
|
254
254
|
lines.push(`};`);
|
|
255
255
|
return lines.join("\n") + "\n";
|
|
256
256
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a project name against the locked grammar.
|
|
3
|
+
*
|
|
4
|
+
* Returns `null` when valid; a human-readable error string when invalid.
|
|
5
|
+
* Apply on every input path: CLI arg, interactive prompt, preset, and
|
|
6
|
+
* programmatic API.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateProjectName(name: string): string | null;
|
|
1
9
|
export declare function installDependencies(dir: string, pm: string): void;
|
|
2
10
|
export declare function capitalize(str: string): string;
|
|
3
11
|
/** Get a short uppercase label for a language code (e.g. "en" → "EN", "zh-cn" → "ZH-CN"). */
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
+
// Project-name grammar (locked by F4 — S4 #2013):
|
|
4
|
+
// /^[a-z0-9][a-z0-9._-]*$/, max 214 chars, unscoped, used as both directory
|
|
5
|
+
// name and package name. Mirrors npm's unscoped-name rules + max path safety.
|
|
6
|
+
const PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
7
|
+
const PROJECT_NAME_MAX = 214;
|
|
8
|
+
/**
|
|
9
|
+
* Validate a project name against the locked grammar.
|
|
10
|
+
*
|
|
11
|
+
* Returns `null` when valid; a human-readable error string when invalid.
|
|
12
|
+
* Apply on every input path: CLI arg, interactive prompt, preset, and
|
|
13
|
+
* programmatic API.
|
|
14
|
+
*/
|
|
15
|
+
export function validateProjectName(name) {
|
|
16
|
+
if (!name || name.length === 0) {
|
|
17
|
+
return "Project name is required";
|
|
18
|
+
}
|
|
19
|
+
if (name.length > PROJECT_NAME_MAX) {
|
|
20
|
+
return `Project name must be ${PROJECT_NAME_MAX} characters or fewer`;
|
|
21
|
+
}
|
|
22
|
+
if (!PROJECT_NAME_RE.test(name)) {
|
|
23
|
+
return ("Project name must start with a lowercase letter or digit and contain " +
|
|
24
|
+
"only lowercase letters, digits, dots, underscores, and hyphens");
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
3
28
|
export function installDependencies(dir, pm) {
|
|
4
29
|
const commands = {
|
|
5
30
|
pnpm: "pnpm install",
|
package/dist/zfb-config-gen.js
CHANGED
|
@@ -19,62 +19,20 @@ export function generateZfbConfig(choices) {
|
|
|
19
19
|
const hasDocHistory = choices.features.includes("docHistory");
|
|
20
20
|
const hasLlmsTxt = choices.features.includes("llmsTxt");
|
|
21
21
|
const hasClaudeResources = choices.features.includes("claudeResources");
|
|
22
|
-
const hasTagGovernance = choices.features.includes("tagGovernance");
|
|
23
22
|
const lines = [];
|
|
24
23
|
// --- Imports ---
|
|
25
24
|
lines.push(`import { z } from "zod";`);
|
|
26
25
|
lines.push(`import { defineConfig } from "zfb/config";`);
|
|
27
26
|
lines.push(`import { settings } from "./src/config/settings";`);
|
|
28
|
-
|
|
29
|
-
lines.push(`import { tagVocabulary } from "./src/config/tag-vocabulary";`);
|
|
30
|
-
}
|
|
27
|
+
lines.push(`import { buildDocsSchema } from "./src/config/docs-schema";`);
|
|
31
28
|
lines.push(``);
|
|
32
|
-
// ---
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
lines.push(` allowed.add(entry.id);`);
|
|
40
|
-
lines.push(` for (const alias of entry.aliases ?? []) allowed.add(alias);`);
|
|
41
|
-
lines.push(` }`);
|
|
42
|
-
lines.push(` const allowedList = [...allowed];`);
|
|
43
|
-
lines.push(` if (allowedList.length === 0) return z.array(z.string()).optional();`);
|
|
44
|
-
lines.push(` const [first, ...rest] = allowedList;`);
|
|
45
|
-
lines.push(` return z.array(z.enum([first, ...rest] as [string, ...string[]])).optional();`);
|
|
46
|
-
lines.push(`}`);
|
|
47
|
-
lines.push(``);
|
|
48
|
-
}
|
|
49
|
-
// --- Schema definition ---
|
|
50
|
-
lines.push(`const docsSchema = z`);
|
|
51
|
-
lines.push(` .object({`);
|
|
52
|
-
lines.push(` title: z.string(),`);
|
|
53
|
-
lines.push(` description: z.string().optional(),`);
|
|
54
|
-
lines.push(` category: z.string().optional(),`);
|
|
55
|
-
lines.push(` sidebar_position: z.number().optional(),`);
|
|
56
|
-
lines.push(` sidebar_label: z.string().optional(),`);
|
|
57
|
-
if (hasTagGovernance) {
|
|
58
|
-
lines.push(` tags: buildTagsSchema(),`);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
lines.push(` tags: z.array(z.string()).optional(),`);
|
|
62
|
-
}
|
|
63
|
-
lines.push(` search_exclude: z.boolean().optional(),`);
|
|
64
|
-
lines.push(` pagination_next: z.string().nullable().optional(),`);
|
|
65
|
-
lines.push(` pagination_prev: z.string().nullable().optional(),`);
|
|
66
|
-
lines.push(` draft: z.boolean().optional(),`);
|
|
67
|
-
lines.push(` unlisted: z.boolean().optional(),`);
|
|
68
|
-
lines.push(` hide_sidebar: z.boolean().optional(),`);
|
|
69
|
-
lines.push(` hide_toc: z.boolean().optional(),`);
|
|
70
|
-
lines.push(` doc_history: z.boolean().optional(),`);
|
|
71
|
-
lines.push(` standalone: z.boolean().optional(),`);
|
|
72
|
-
lines.push(` slug: z.string().optional(),`);
|
|
73
|
-
lines.push(` generated: z.boolean().optional(),`);
|
|
74
|
-
lines.push(` category_no_page: z.boolean().optional(),`);
|
|
75
|
-
lines.push(` category_sort_order: z.enum(["asc", "desc"]).optional(),`);
|
|
76
|
-
lines.push(` })`);
|
|
77
|
-
lines.push(` .passthrough();`);
|
|
29
|
+
// --- Schema definition — delegated to the single source of truth ---
|
|
30
|
+
// buildDocsSchema() lives in src/config/docs-schema.ts and is shared by
|
|
31
|
+
// pages/_data.ts (ZfbDocsData alias) and src/types/docs-entry.ts (DocsData).
|
|
32
|
+
// tagGovernance projects: docs-schema.ts reads settings + tagVocabulary
|
|
33
|
+
// internally, so the generated zfb.config.ts needs no extra import or
|
|
34
|
+
// inline buildTagsSchema — the schema builder encapsulates all of that.
|
|
35
|
+
lines.push(`const docsSchema = buildDocsSchema();`);
|
|
78
36
|
lines.push(``);
|
|
79
37
|
lines.push(`const docsSchemaJson = z.toJSONSchema(docsSchema) as Record<string, unknown>;`);
|
|
80
38
|
lines.push(``);
|
|
@@ -203,6 +161,9 @@ export function generateZfbConfig(choices) {
|
|
|
203
161
|
// --- Export ---
|
|
204
162
|
lines.push(`export default defineConfig({`);
|
|
205
163
|
lines.push(` framework: "preact",`);
|
|
164
|
+
lines.push(` // Pin the dev/preview port — zfb defaults to 3000, but the generated`);
|
|
165
|
+
lines.push(` // CLAUDE.md and the Tauri dev wrappers assume 4321.`);
|
|
166
|
+
lines.push(` port: 4321,`);
|
|
206
167
|
lines.push(` tailwind: { enabled: true },`);
|
|
207
168
|
lines.push(` collections,`);
|
|
208
169
|
lines.push(` stripMdExt: true,`);
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { getCollection } from "zfb/content";
|
|
14
14
|
import type { CollectionEntry } from "zfb/content";
|
|
15
|
+
import type { DocsData } from "@/config/docs-schema";
|
|
15
16
|
import type { DocsEntry } from "@/types/docs-entry";
|
|
16
17
|
import type { DocPageEntry } from "./lib/doc-page-props";
|
|
17
18
|
import { toRouteSlug } from "@/utils/slug";
|
|
@@ -22,30 +23,16 @@ import { toRouteSlug } from "@/utils/slug";
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Frontmatter shape shared by all docs collections (EN, locale, versioned).
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
*
|
|
27
|
+
* Re-exported alias for `DocsData` (the `z.infer`-derived type from
|
|
28
|
+
* `src/config/docs-schema.ts`) so call sites that import `ZfbDocsData` from
|
|
29
|
+
* `pages/_data` continue to work without changes.
|
|
30
|
+
*
|
|
31
|
+
* The `[key: string]: unknown` index signature comes from `.passthrough()` on
|
|
32
|
+
* the zod schema — custom frontmatter keys remain accessible downstream (e.g.
|
|
33
|
+
* for frontmatter-preview) without extra casting.
|
|
28
34
|
*/
|
|
29
|
-
export type ZfbDocsData =
|
|
30
|
-
title: string;
|
|
31
|
-
description?: string;
|
|
32
|
-
category?: string;
|
|
33
|
-
sidebar_position?: number;
|
|
34
|
-
sidebar_label?: string;
|
|
35
|
-
tags?: string[];
|
|
36
|
-
search_exclude?: boolean;
|
|
37
|
-
pagination_next?: string | null;
|
|
38
|
-
pagination_prev?: string | null;
|
|
39
|
-
draft?: boolean;
|
|
40
|
-
unlisted?: boolean;
|
|
41
|
-
hide_sidebar?: boolean;
|
|
42
|
-
hide_toc?: boolean;
|
|
43
|
-
doc_history?: boolean;
|
|
44
|
-
standalone?: boolean;
|
|
45
|
-
slug?: string;
|
|
46
|
-
generated?: boolean;
|
|
47
|
-
[key: string]: unknown;
|
|
48
|
-
};
|
|
35
|
+
export type ZfbDocsData = DocsData;
|
|
49
36
|
|
|
50
37
|
/**
|
|
51
38
|
* zfb collection entry augmented with the `id` and `collection` fields that
|
|
@@ -21,33 +21,19 @@
|
|
|
21
21
|
//
|
|
22
22
|
// Locale: defaultLocale (EN). Non-default locales are handled by
|
|
23
23
|
// pages/[locale]/docs/[[...slug]].tsx.
|
|
24
|
+
//
|
|
25
|
+
// Enumeration + per-entry derived data (breadcrumbs, prev/next, headings) are
|
|
26
|
+
// built by the shared, memoized buildDocRouteEntries (#2010); rendering by the
|
|
27
|
+
// shared renderDocPage. This file owns only the route's nav source and the
|
|
28
|
+
// param/prop shapes.
|
|
24
29
|
|
|
25
30
|
import { settings } from "@/config/settings";
|
|
26
31
|
import { defaultLocale } from "@/config/i18n";
|
|
27
|
-
import { docsUrl, absoluteUrl } from "@/utils/base";
|
|
28
|
-
import {
|
|
29
|
-
buildNavTree,
|
|
30
|
-
buildBreadcrumbs,
|
|
31
|
-
collectAutoIndexNodes,
|
|
32
|
-
type NavNode,
|
|
33
|
-
} from "@/utils/docs";
|
|
34
|
-
import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
|
|
35
|
-
import { toRouteSlug, toSlugParams } from "@/utils/slug";
|
|
36
|
-
// Shared MDX-tag → Preact-component bag. Includes htmlOverrides
|
|
37
|
-
// (native typography), HtmlPreviewWrapper (Island), and stub bindings
|
|
38
|
-
// for every other custom tag the MDX corpus references — see
|
|
39
|
-
// `pages/_mdx-components.ts` for the full list and rationale.
|
|
40
|
-
import { createMdxComponents } from "../_mdx-components";
|
|
41
|
-
import { DocHistoryArea } from "../lib/_doc-history-area";
|
|
42
|
-
import { DocMetainfoArea } from "../lib/_doc-metainfo-area";
|
|
43
|
-
import { buildInlineVersionSwitcher } from "../lib/_inline-version-switcher";
|
|
44
32
|
import type { JSX } from "preact";
|
|
45
33
|
import { resolveNavSource } from "../lib/_nav-source-docs";
|
|
46
|
-
import {
|
|
47
|
-
import
|
|
48
|
-
import {
|
|
49
|
-
import { DocPageShell } from "../lib/_doc-page-shell";
|
|
50
|
-
import { resolveDocPrevNext, flattenSubtree } from "../lib/_doc-route-paths";
|
|
34
|
+
import type { DocPageEntryProps, DocPageAutoIndexProps } from "../lib/doc-page-props";
|
|
35
|
+
import { buildDocRouteEntries } from "../lib/_doc-route-entries";
|
|
36
|
+
import { renderDocPage } from "../lib/_doc-page-renderer";
|
|
51
37
|
|
|
52
38
|
export const frontmatter = { title: "Docs" };
|
|
53
39
|
|
|
@@ -55,8 +41,6 @@ export const frontmatter = { title: "Docs" };
|
|
|
55
41
|
// Props contract
|
|
56
42
|
// ---------------------------------------------------------------------------
|
|
57
43
|
|
|
58
|
-
// DocPageEntry, AutoIndexNode imported from pages/lib/doc-page-props.ts
|
|
59
|
-
|
|
60
44
|
type DocPageProps = DocPageEntryProps | DocPageAutoIndexProps;
|
|
61
45
|
|
|
62
46
|
// ---------------------------------------------------------------------------
|
|
@@ -67,8 +51,8 @@ type DocPageProps = DocPageEntryProps | DocPageAutoIndexProps;
|
|
|
67
51
|
* Enumerate all doc routes for the default locale (EN).
|
|
68
52
|
*
|
|
69
53
|
* Synchronous per ADR-004: getCollection() resolves from the pre-loaded
|
|
70
|
-
* ContentSnapshot. All nav-tree and breadcrumb computation is done
|
|
71
|
-
* so the component is a pure renderer.
|
|
54
|
+
* ContentSnapshot. All nav-tree and breadcrumb computation is done in the
|
|
55
|
+
* shared builder so the component is a pure renderer.
|
|
72
56
|
*/
|
|
73
57
|
export function paths(): Array<{
|
|
74
58
|
params: { slug: string[] };
|
|
@@ -77,66 +61,19 @@ export function paths(): Array<{
|
|
|
77
61
|
const locale = defaultLocale;
|
|
78
62
|
// Identity-stable nav source (draft-filtered, unlisted retained). The same
|
|
79
63
|
// instances are returned across this route's many per-page paths()
|
|
80
|
-
// invocations, so buildNavTree's identity fast-path
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// A `category_no_page` index.mdx carries category metadata only — keep it
|
|
94
|
-
// in the nav tree (built above, used for breadcrumbs) but emit NO route for
|
|
95
|
-
// it. zfb's walker retains every .mdx as a collection entry, so without
|
|
96
|
-
// this explicit skip the metadata file would silently add a route.
|
|
97
|
-
if (entry.data.category_no_page === true) continue;
|
|
98
|
-
const slug = entry.data.slug ?? toRouteSlug(entry.slug);
|
|
99
|
-
const navSection = getNavSectionForSlug(slug);
|
|
100
|
-
const subtree = getNavSubtree(tree, navSection);
|
|
101
|
-
|
|
102
|
-
// Prev/next + frontmatter pagination overrides resolved against THIS
|
|
103
|
-
// route's own `tree`. Latest route — hrefs stay unversioned (no rewrite).
|
|
104
|
-
const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
|
|
105
|
-
tree,
|
|
106
|
-
flattenSubtree(subtree),
|
|
107
|
-
slug,
|
|
108
|
-
entry.data,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
result.push({
|
|
112
|
-
params: { slug: toSlugParams(slug) },
|
|
113
|
-
props: {
|
|
114
|
-
kind: "entry",
|
|
115
|
-
entry,
|
|
116
|
-
breadcrumbs: buildBreadcrumbs(fullTree, slug, locale),
|
|
117
|
-
prev: prevNode,
|
|
118
|
-
next: nextNode,
|
|
119
|
-
headings: extractHeadings(entry.body ?? ""),
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Auto-generated index pages for categories without index.mdx
|
|
125
|
-
for (const node of collectAutoIndexNodes(tree)) {
|
|
126
|
-
result.push({
|
|
127
|
-
params: { slug: toSlugParams(node.slug) },
|
|
128
|
-
props: {
|
|
129
|
-
kind: "autoIndex",
|
|
130
|
-
autoIndex: node as AutoIndexNode,
|
|
131
|
-
breadcrumbs: buildBreadcrumbs(fullTree, node.slug, locale),
|
|
132
|
-
prev: null,
|
|
133
|
-
next: null,
|
|
134
|
-
headings: [],
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
64
|
+
// invocations, so both buildNavTree's identity fast-path and the
|
|
65
|
+
// buildDocRouteEntries memo key on them — see pages/lib/_nav-source-docs.ts
|
|
66
|
+
// (#1902).
|
|
67
|
+
const source = resolveNavSource(locale, undefined);
|
|
68
|
+
|
|
69
|
+
return buildDocRouteEntries({
|
|
70
|
+
source,
|
|
71
|
+
locale,
|
|
72
|
+
routeSig: `docs;${locale}`,
|
|
73
|
+
}).map((item) => ({
|
|
74
|
+
params: { slug: item.slugParams },
|
|
75
|
+
props: item.props,
|
|
76
|
+
}));
|
|
140
77
|
}
|
|
141
78
|
|
|
142
79
|
// ---------------------------------------------------------------------------
|
|
@@ -146,86 +83,8 @@ export function paths(): Array<{
|
|
|
146
83
|
type PageArgs = DocPageProps & { params: { slug: string[] } };
|
|
147
84
|
|
|
148
85
|
export default function DocsPage(props: PageArgs): JSX.Element {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
? props.autoIndex.slug
|
|
154
|
-
: (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
|
|
155
|
-
|
|
156
|
-
const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
|
|
157
|
-
const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
|
|
158
|
-
|
|
159
|
-
// Locale-aware components bag — creates nav wrappers bound to the active
|
|
160
|
-
// locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
|
|
161
|
-
const components = createMdxComponents(locale);
|
|
162
|
-
|
|
163
|
-
// Resolve child hrefs for auto-index pages — latest route keeps the nav
|
|
164
|
-
// node's own docsUrl href (fallback for a noPage parent without an href).
|
|
165
|
-
const autoIndexChildren = props.kind === "autoIndex"
|
|
166
|
-
? props.autoIndex.children
|
|
167
|
-
.filter((c: NavNode) => c.hasPage || c.children.length > 0)
|
|
168
|
-
.map((c: NavNode) => ({
|
|
169
|
-
...c,
|
|
170
|
-
href: c.href ?? docsUrl(c.slug, locale),
|
|
171
|
-
}))
|
|
172
|
-
: [];
|
|
173
|
-
|
|
174
|
-
// Canonical URL — base-prefixed page path, absolutized against siteUrl.
|
|
175
|
-
const currentPath = docsUrl(slug, locale);
|
|
176
|
-
const canonical = absoluteUrl(currentPath);
|
|
177
|
-
|
|
178
|
-
// Persist key: locale + nav-section so the sidebar DOM node is reused
|
|
179
|
-
// across same-locale + same-section navigations only. No sanitizer needed —
|
|
180
|
-
// both lang (BCP-47 locale string) and navSection (filesystem-derived
|
|
181
|
-
// kebab-case slug) come from controlled, trusted sources.
|
|
182
|
-
const navSection = getNavSectionForSlug(slug);
|
|
183
|
-
const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
|
|
184
|
-
const sidebarPersistKey = hideSidebar
|
|
185
|
-
? undefined
|
|
186
|
-
: `sidebar-${locale}-${navSection ?? "default"}`;
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<DocPageShell
|
|
190
|
-
kind={props.kind}
|
|
191
|
-
locale={locale}
|
|
192
|
-
slug={slug}
|
|
193
|
-
title={title}
|
|
194
|
-
description={description}
|
|
195
|
-
canonical={canonical}
|
|
196
|
-
breadcrumbs={breadcrumbs}
|
|
197
|
-
prev={prev}
|
|
198
|
-
next={next}
|
|
199
|
-
headings={headings}
|
|
200
|
-
navSection={navSection}
|
|
201
|
-
sidebarPersistKey={sidebarPersistKey}
|
|
202
|
-
hideSidebar={hideSidebar}
|
|
203
|
-
hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
|
|
204
|
-
currentPath={currentPath}
|
|
205
|
-
versionSwitcher={buildInlineVersionSwitcher(slug, locale)}
|
|
206
|
-
autoIndexLabel={props.kind === "autoIndex" ? props.autoIndex.label : undefined}
|
|
207
|
-
autoIndexChildren={autoIndexChildren}
|
|
208
|
-
metainfoSlot={
|
|
209
|
-
props.kind === "autoIndex" ? <DocMetainfoArea slug={slug} locale={locale} /> : null
|
|
210
|
-
}
|
|
211
|
-
contentHeaderSlot={
|
|
212
|
-
props.kind === "entry" ? (
|
|
213
|
-
<DocContentHeader entry={props.entry} slug={slug} locale={locale} />
|
|
214
|
-
) : undefined
|
|
215
|
-
}
|
|
216
|
-
contentSlot={
|
|
217
|
-
props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
|
|
218
|
-
}
|
|
219
|
-
docHistorySlot={
|
|
220
|
-
props.kind === "entry" && !props.entry.data.unlisted ? (
|
|
221
|
-
<DocHistoryArea
|
|
222
|
-
slug={slug}
|
|
223
|
-
locale={locale}
|
|
224
|
-
entrySlug={props.entry.slug}
|
|
225
|
-
contentDir={settings.docsDir}
|
|
226
|
-
/>
|
|
227
|
-
) : null
|
|
228
|
-
}
|
|
229
|
-
/>
|
|
230
|
-
);
|
|
86
|
+
return renderDocPage(props, {
|
|
87
|
+
locale: defaultLocale,
|
|
88
|
+
docHistoryContentDir: settings.docsDir,
|
|
89
|
+
});
|
|
231
90
|
}
|