create-zudo-doc 0.2.0 → 0.2.2

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.
Files changed (83) hide show
  1. package/dist/api.js +4 -1
  2. package/dist/cli.js +4 -6
  3. package/dist/compose.d.ts +2 -3
  4. package/dist/compose.js +7 -4
  5. package/dist/features/tauri.d.ts +10 -5
  6. package/dist/features/tauri.js +49 -6
  7. package/dist/preset.js +11 -0
  8. package/dist/prompts.js +2 -6
  9. package/dist/scaffold.js +15 -9
  10. package/dist/settings-gen.js +9 -6
  11. package/dist/utils.d.ts +8 -0
  12. package/dist/utils.js +25 -0
  13. package/dist/zfb-config-gen.js +11 -50
  14. package/package.json +1 -1
  15. package/templates/base/pages/_data.ts +10 -23
  16. package/templates/base/pages/docs/[[...slug]].tsx +27 -168
  17. package/templates/base/pages/lib/_body-end-islands.tsx +3 -0
  18. package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
  19. package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
  20. package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
  21. package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
  22. package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
  23. package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
  24. package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
  25. package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
  26. package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
  27. package/templates/base/pages/lib/_header-with-defaults.tsx +54 -89
  28. package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
  29. package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
  30. package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
  31. package/templates/base/pages/lib/_search-widget-script.ts +32 -9
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
  33. package/templates/base/pages/lib/locale-merge.ts +1 -1
  34. package/templates/base/pages/lib/route-enumerators.ts +11 -7
  35. package/templates/base/plugins/connect-adapter.mjs +30 -1
  36. package/templates/base/plugins/copy-public-plugin.mjs +10 -2
  37. package/templates/base/plugins/search-index-plugin.mjs +20 -8
  38. package/templates/base/src/components/ai-chat-modal.tsx +2 -0
  39. package/templates/base/src/components/doc-history.tsx +2 -0
  40. package/templates/base/src/components/image-enlarge.tsx +2 -0
  41. package/templates/base/src/components/sidebar-toggle.tsx +1 -1
  42. package/templates/base/src/components/sidebar-tree.tsx +11 -5
  43. package/templates/base/src/components/theme-toggle.tsx +18 -102
  44. package/templates/base/src/config/color-schemes.ts +4 -0
  45. package/templates/base/src/config/docs-schema.ts +94 -0
  46. package/templates/base/src/config/i18n.ts +10 -3
  47. package/templates/base/src/styles/global.css +14 -0
  48. package/templates/base/src/types/docs-entry.ts +8 -26
  49. package/templates/base/src/utils/base.ts +5 -3
  50. package/templates/base/src/utils/docs.ts +144 -169
  51. package/templates/base/zfb-shim.d.ts +167 -0
  52. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
  53. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
  54. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
  55. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
  56. package/templates/features/docHistory/files/src/components/doc-history.tsx +30 -8
  57. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
  58. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
  59. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
  60. package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
  61. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
  62. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
  63. package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
  64. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
  65. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
  66. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  67. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +9 -3
  68. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
  69. package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
  70. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
  71. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
  72. package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
  73. package/templates/base/src/components/content/heading-h3.tsx +0 -20
  74. package/templates/base/src/hooks/use-active-heading.ts +0 -133
  75. package/templates/base/src/plugins/docs-source-map.ts +0 -103
  76. package/templates/base/src/plugins/hast-utils.ts +0 -10
  77. package/templates/base/src/plugins/rehype-code-title.ts +0 -50
  78. package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
  79. package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
  80. package/templates/base/src/plugins/url-utils.ts +0 -4
  81. package/templates/base/src/utils/dedent.ts +0 -24
  82. package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
  83. 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
- if (/^[./]|\.\./.test(args.name)) {
150
- return "Project name must not contain path traversal characters";
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/compose.d.ts CHANGED
@@ -78,9 +78,8 @@ export declare function validateDependencies(features: FeatureDefinition[], allS
78
78
  * - `pages/_mdx-components.ts` — image-enlarge.ts injects the
79
79
  * EnlargeableParagraph p-override (ENLARGE_SVG, EnlargeableParagraph def,
80
80
  * `p:` map entry) when imageEnlarge is enabled.
81
- *
82
- * The .tsx anchor form is still supported in `ANCHOR_LINE_RE` for forward
83
- * compatibility.
81
+ * - `pages/lib/_body-end-islands.tsx` — tauri.ts injects the FindInPageInit
82
+ * island (import, displayName, Island mount) when tauri is enabled.
84
83
  */
85
84
  export declare const ANCHOR_FILES: string[];
86
85
  /**
package/dist/compose.js CHANGED
@@ -164,11 +164,14 @@ export function validateDependencies(features, allSelectedNames) {
164
164
  * - `pages/_mdx-components.ts` — image-enlarge.ts injects the
165
165
  * EnlargeableParagraph p-override (ENLARGE_SVG, EnlargeableParagraph def,
166
166
  * `p:` map entry) when imageEnlarge is enabled.
167
- *
168
- * The .tsx anchor form is still supported in `ANCHOR_LINE_RE` for forward
169
- * compatibility.
167
+ * - `pages/lib/_body-end-islands.tsx` — tauri.ts injects the FindInPageInit
168
+ * island (import, displayName, Island mount) when tauri is enabled.
170
169
  */
171
- export const ANCHOR_FILES = ["src/styles/global.css", "pages/_mdx-components.ts"];
170
+ export const ANCHOR_FILES = [
171
+ "src/styles/global.css",
172
+ "pages/_mdx-components.ts",
173
+ "pages/lib/_body-end-islands.tsx",
174
+ ];
172
175
  /**
173
176
  * Main composition entry point. Orchestrates the full feature composition
174
177
  * pipeline for a generated project.
@@ -2,10 +2,15 @@ import type { FeatureModule } from "../compose.js";
2
2
  /**
3
3
  * Tauri feature.
4
4
  *
5
- * W7A (#1736): post-cutover, the FindInPage island is mounted by the
6
- * pages/lib body-end wrapper. The find-match highlight CSS is unconditional
7
- * in `templates/base/src/styles/global.css` (matches host). Only the
8
- * postProcess hooks (Cargo.toml / tauri.conf.json / .gitignore patches)
9
- * remain feature-scoped.
5
+ * #2052: the FindInPageInit island (Cmd/Ctrl+F find bar for the Tauri
6
+ * WebView, where the browser-native find UI is unavailable) is wired into
7
+ * `pages/lib/_body-end-islands.tsx` via the three injections below — import,
8
+ * displayName, and Island mount. zfb's island scanner only registers
9
+ * components reachable through static import chains (page → wrapper →
10
+ * component), so without this injection the feature-copied component files
11
+ * are orphaned dead code that never hydrates. The find-match highlight CSS
12
+ * is unconditional in `templates/base/src/styles/global.css` (matches host);
13
+ * the component runtime-gates itself (renders null unless
14
+ * `window.__TAURI_INTERNALS__` exists), so no settings field is needed.
10
15
  */
11
16
  export declare const tauriFeature: FeatureModule;
@@ -3,15 +3,58 @@ import path from "path";
3
3
  /**
4
4
  * Tauri feature.
5
5
  *
6
- * W7A (#1736): post-cutover, the FindInPage island is mounted by the
7
- * pages/lib body-end wrapper. The find-match highlight CSS is unconditional
8
- * in `templates/base/src/styles/global.css` (matches host). Only the
9
- * postProcess hooks (Cargo.toml / tauri.conf.json / .gitignore patches)
10
- * remain feature-scoped.
6
+ * #2052: the FindInPageInit island (Cmd/Ctrl+F find bar for the Tauri
7
+ * WebView, where the browser-native find UI is unavailable) is wired into
8
+ * `pages/lib/_body-end-islands.tsx` via the three injections below — import,
9
+ * displayName, and Island mount. zfb's island scanner only registers
10
+ * components reachable through static import chains (page → wrapper →
11
+ * component), so without this injection the feature-copied component files
12
+ * are orphaned dead code that never hydrates. The find-match highlight CSS
13
+ * is unconditional in `templates/base/src/styles/global.css` (matches host);
14
+ * the component runtime-gates itself (renders null unless
15
+ * `window.__TAURI_INTERNALS__` exists), so no settings field is needed.
11
16
  */
12
17
  export const tauriFeature = (choices) => ({
13
18
  name: "tauri",
14
- injections: [],
19
+ injections: [
20
+ // 1. Import the island entry. Inserted AFTER the
21
+ // `// @slot:body-end-islands:imports` anchor.
22
+ {
23
+ file: "pages/lib/_body-end-islands.tsx",
24
+ anchor: "// @slot:body-end-islands:imports",
25
+ position: "after",
26
+ content: `import FindInPageInit from "@/components/find-in-page-init";`,
27
+ },
28
+ // 2. Stable island marker name (same belt-and-braces guard as the
29
+ // sibling islands in the file). Inserted AFTER the
30
+ // `// @slot:body-end-islands:display-names` anchor.
31
+ {
32
+ file: "pages/lib/_body-end-islands.tsx",
33
+ anchor: "// @slot:body-end-islands:display-names",
34
+ position: "after",
35
+ content: `(FindInPageInit as { displayName?: string }).displayName = "FindInPageInit";`,
36
+ },
37
+ // 3. Island mount. Inserted AFTER the
38
+ // `{/* @slot:body-end-islands:extra-islands */}` anchor.
39
+ // when="load" (not "idle"): the island's job is to intercept
40
+ // Cmd/Ctrl+F via a keydown listener, so it must hydrate as soon as
41
+ // the islands runtime mounts — same rationale as the
42
+ // clientRouterBootstrap click intercept above it. Deferring to idle
43
+ // would leave a post-load window where Cmd+F does nothing, which is
44
+ // the very bug this injection fixes.
45
+ {
46
+ file: "pages/lib/_body-end-islands.tsx",
47
+ anchor: "{/* @slot:body-end-islands:extra-islands */}",
48
+ position: "after",
49
+ content: ` {/* Tauri-only find-in-page (Cmd/Ctrl+F) bar. Renders null outside
50
+ a Tauri WebView, so the island is inert in plain browser builds
51
+ of the same scaffold. */}
52
+ {Island({
53
+ when: "load",
54
+ children: <FindInPageInit />,
55
+ }) as unknown as VNode}`,
56
+ },
57
+ ],
15
58
  postProcess: async (targetDir) => {
16
59
  // Patch Cargo.toml package name
17
60
  const cargoPath = path.join(targetDir, "src-tauri/Cargo.toml");
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
- if (!value.trim())
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
- "@takazudo/zfb": "0.1.0-next.35",
268
- "@takazudo/zfb-runtime": "0.1.0-next.35",
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.35",
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.0",
281
+ "@takazudo/zudo-doc": "^0.2.2",
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.0";
335
+ deps["@takazudo/zudo-doc-history-server"] = "^0.2.2";
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")) {
@@ -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,15 +60,18 @@ 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 string,`);
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(` } as Record<string, LocaleConfig>,`);
69
+ lines.push(` } satisfies Record<string, LocaleConfig>,`);
70
70
  }
71
71
  else {
72
+ // `as`, not `satisfies`: satisfies keeps the inferred type at literal {},
73
+ // so Object.entries(settings.locales) in the generated zfb.config.ts
74
+ // yields unknown values and `zfb check` fails with TS18046 (#2053).
72
75
  lines.push(` locales: {} as Record<string, LocaleConfig>,`);
73
76
  }
74
77
  // mermaid is controlled by the markdown.features block in zfb.config.ts
@@ -159,7 +162,7 @@ export function generateSettingsFile(choices) {
159
162
  }
160
163
  lines.push(` htmlPreview: undefined as HtmlPreviewConfig | undefined,`);
161
164
  if (choices.features.includes("versioning")) {
162
- lines.push(` versions: [] as VersionConfig[],`);
165
+ lines.push(` versions: [] satisfies VersionConfig[] as VersionConfig[] | false,`);
163
166
  }
164
167
  else {
165
168
  lines.push(` versions: false as VersionConfig[] | false,`);
@@ -219,7 +222,7 @@ export function generateSettingsFile(choices) {
219
222
  if (choices.features.includes("changelog")) {
220
223
  lines.push(` { label: "Changelog", path: "/docs/changelog", categoryMatch: "changelog" },`);
221
224
  }
222
- lines.push(` ] as HeaderNavItem[],`);
225
+ lines.push(` ] satisfies HeaderNavItem[] as HeaderNavItem[],`);
223
226
  lines.push(` headerRightItems: [`);
224
227
  if (choices.headerRightItems !== undefined) {
225
228
  // User-supplied override (including empty array): emit each entry verbatim,
@@ -250,7 +253,7 @@ export function generateSettingsFile(choices) {
250
253
  lines.push(` { type: "component", component: "language-switcher" },`);
251
254
  }
252
255
  }
253
- lines.push(` ] as HeaderRightItem[],`);
256
+ lines.push(` ] satisfies HeaderRightItem[] as HeaderRightItem[],`);
254
257
  lines.push(`};`);
255
258
  return lines.join("\n") + "\n";
256
259
  }
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",
@@ -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
- if (hasTagGovernance) {
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
- // --- Tags schema builder (only when tagGovernance is selected) ---
33
- if (hasTagGovernance) {
34
- lines.push(`function buildTagsSchema() {`);
35
- lines.push(` const vocabularyActive = settings.tagVocabulary && settings.tagGovernance === "strict";`);
36
- lines.push(` if (!vocabularyActive) return z.array(z.string()).optional();`);
37
- lines.push(` const allowed = new Set<string>();`);
38
- lines.push(` for (const entry of tagVocabulary) {`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -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
- * Matches the zod schema in zfb.config.ts field-for-field.
26
- * `.passthrough()` equivalent: the index signature [key: string]: unknown
27
- * keeps custom frontmatter keys available (e.g. for frontmatter-preview).
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