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
@@ -1,135 +1,45 @@
1
+ // @ts-check
1
2
  // zfb plugin module: claude-resources.
2
3
  //
3
4
  // Wires `runClaudeResourcesPreStep` (from
4
5
  // `@takazudo/zudo-doc/integrations/claude-resources`) into zfb's
5
- // `preBuild` lifecycle hook. Replaces the npm `prebuild`-script glue
6
- // in `scripts/zfb-prebuild.mjs` for this step (the script remains in
7
- // place during the merge window — T6 retires it).
6
+ // `preBuild` lifecycle hook.
8
7
  //
9
- // Why this shim exists (and is not the v2 integration module directly):
10
- //
11
- // 1. zfb's plugin host runs `node` without any TypeScript loader, so
12
- // it cannot import `.ts` source files. The v2 integration package
13
- // (`@takazudo/zudo-doc/integrations/claude-resources`) currently
14
- // ships only TypeScript source through its `exports` map and has
15
- // no build step.
16
- //
17
- // 2. The `runClaudeResourcesPreStep` runner pulls in `gray-matter`,
18
- // which performs a CJS `require("fs")` that esbuild's
19
- // configuration-loader bundle (ESM-only) cannot satisfy. Loading
20
- // it inline at the top of `zfb.config.ts` therefore breaks config
21
- // parsing entirely.
22
- //
23
- // Both problems are solved by isolating the runner behind a child
24
- // process: this shim spawns `tsx` (the project's existing TS-aware
25
- // Node runner, pinned via `tsx` in `package.json`) on a tiny inline
26
- // script that imports the runner. tsx handles `.ts` resolution and
27
- // Node's CJS↔ESM interop for gray-matter; the parent plugin host stays
28
- // in plain Node and only sees a child-process exit code.
29
- //
30
- // Inline functions are not supported by zfb's plugin runtime — see
31
- // `@takazudo/zfb/plugins` source comment. This file is the plugin-host
32
- // equivalent once the npm script is retired (T6).
33
-
34
- import { spawn } from "node:child_process";
35
- import { fileURLToPath } from "node:url";
36
- import { dirname, resolve } from "node:path";
37
-
38
- const PLUGIN_NAME = "@takazudo/zudo-doc-claude-resources";
8
+ // Previously this shim spawned a `tsx` subprocess because the integration
9
+ // package shipped only TypeScript source (no build step) and `gray-matter`
10
+ // pulled in a CJS `require("fs")` that esbuild's ESM-only config-loader
11
+ // bundle could not satisfy. Both constraints are now lifted: the package
12
+ // ships compiled `dist/` and the plugin host is plain Node (not an esbuild
13
+ // bundle), so the runner can be imported directly.
39
14
 
40
- // `tsx` is a workspace dependency of the host project and resolves to
41
- // `<projectRoot>/node_modules/.bin/tsx` from this file. We resolve the
42
- // binary explicitly so the shim does not depend on the parent shell's
43
- // `PATH` (the plugin host is spawned by zfb without sourcing the
44
- // user's profile — Node's `PATH` is whatever zfb itself was launched
45
- // with).
46
- const HERE = dirname(fileURLToPath(import.meta.url));
47
- const TSX_BIN = resolve(HERE, "..", "node_modules", ".bin", "tsx");
15
+ /** @import { ZfbBuildHookContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
48
16
 
49
- /**
50
- * Run the v2 claude-resources runner under tsx.
51
- *
52
- * Inlines a minimal ESM script via `tsx -e` so we don't have to ship a
53
- * second `.ts` file just to host the call site. The runner returns a
54
- * `{ claudemd, commands, skills, agents }` summary which we re-emit on
55
- * the child's stdout — the parent (this shim) parses it and forwards
56
- * the summary to the plugin host's logger.
57
- */
58
- function runRunnerUnderTsx({ claudeDir, projectRoot, docsDir }) {
59
- // The runner accepts `projectRoot` and `docsDir` as relative paths
60
- // (resolved against `process.cwd()` in the runner). To insulate the
61
- // child from whatever cwd zfb spawned us with, we set cwd explicitly
62
- // and pass absolute paths through.
63
- // tsx's `-e` flag emits CJS by default (it picks the format from the
64
- // entry's extension, and an inline `-e` script has none), so we wrap
65
- // the body in an `async`-IIFE rather than relying on top-level await.
66
- const childScript = `
67
- (async () => {
68
- const { runClaudeResourcesPreStep } = await import("@takazudo/zudo-doc/integrations/claude-resources");
69
- const result = await runClaudeResourcesPreStep({
70
- claudeDir: ${JSON.stringify(claudeDir)},
71
- projectRoot: ${JSON.stringify(projectRoot)},
72
- docsDir: ${JSON.stringify(docsDir)},
73
- });
74
- process.stdout.write(JSON.stringify(result));
75
- })().catch((err) => {
76
- process.stderr.write(err && err.stack ? err.stack : String(err));
77
- process.exit(1);
78
- });
79
- `;
17
+ import { runClaudeResourcesPreStep } from "@takazudo/zudo-doc/integrations/claude-resources";
80
18
 
81
- return new Promise((resolveCall, reject) => {
82
- const child = spawn(TSX_BIN, ["-e", childScript], {
83
- cwd: projectRoot,
84
- stdio: ["ignore", "pipe", "pipe"],
85
- });
86
- let stdout = "";
87
- let stderr = "";
88
- child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
89
- child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
90
- child.on("error", reject);
91
- child.on("close", (code) => {
92
- if (code !== 0) {
93
- reject(
94
- new Error(
95
- `[${PLUGIN_NAME}] runner exited with code ${code}\n${stderr}`,
96
- ),
97
- );
98
- return;
99
- }
100
- try {
101
- resolveCall(JSON.parse(stdout));
102
- } catch (err) {
103
- reject(
104
- new Error(
105
- `[${PLUGIN_NAME}] failed to parse runner stdout: ${err.message}\nstdout: ${stdout}\nstderr: ${stderr}`,
106
- ),
107
- );
108
- }
109
- });
110
- });
111
- }
19
+ const PLUGIN_NAME = "@takazudo/zudo-doc-claude-resources";
112
20
 
21
+ /** @type {ZfbPlugin} */
113
22
  export default {
114
23
  name: PLUGIN_NAME,
24
+
25
+ /** @param {ZfbBuildHookContext} ctx */
115
26
  async preBuild(ctx) {
116
- const claudeDir = ctx.options.claudeDir;
27
+ const claudeDir = ctx.options["claudeDir"];
117
28
  if (typeof claudeDir !== "string" || claudeDir.length === 0) {
118
29
  throw new Error(
119
30
  `[${PLUGIN_NAME}] preBuild: options.claudeDir must be a non-empty string (got ${JSON.stringify(claudeDir)})`,
120
31
  );
121
32
  }
122
- const projectRootOpt = ctx.options.projectRoot;
123
- const docsDirOpt = ctx.options.docsDir;
124
- const result = await runRunnerUnderTsx({
33
+ const projectRootOpt = ctx.options["projectRoot"];
34
+ const docsDirOpt = ctx.options["docsDir"];
35
+ const result = await runClaudeResourcesPreStep({
125
36
  claudeDir,
126
37
  projectRoot:
127
38
  typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
128
39
  docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
129
40
  });
130
- // Surface a one-line summary so build logs make the migration
131
- // observable (mirrors the `[zfb-prebuild]` lines from the legacy
132
- // npm-script glue).
41
+ // Surface a one-line summary so build logs make the generation
42
+ // observable.
133
43
  ctx.logger.info(
134
44
  `claude-resources: ${result.claudemd} CLAUDE.md, ${result.commands} commands, ${result.skills} skills, ${result.agents} agents`,
135
45
  );
@@ -64,7 +64,9 @@ function parseFrontmatter(content: string) {
64
64
  }
65
65
 
66
66
  function escapeTitle(s: string): string {
67
- return s.replace(/"/g, '\\"');
67
+ // Backslashes must be escaped first — the value is embedded in
68
+ // double-quoted YAML frontmatter where `\d` or `C:\path` is invalid.
69
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
68
70
  }
69
71
 
70
72
  function listFiles(dir: string): string[] {
@@ -93,6 +95,35 @@ generated: true
93
95
  fs.writeFileSync(path.join(outputDir, "index.mdx"), mdx);
94
96
  }
95
97
 
98
+ /**
99
+ * Writes an unlisted sub-page MDX file (flat file with a custom nested slug).
100
+ * Used for skill references, scripts, and assets.
101
+ */
102
+ function writeUnlistedSubPage(
103
+ outputPath: string,
104
+ title: string,
105
+ slug: string,
106
+ body: string,
107
+ ) {
108
+ fs.writeFileSync(
109
+ outputPath,
110
+ `---\ntitle: "${escapeTitle(title)}"\nslug: "${slug}"\nunlisted: true\ngenerated: true\n---\n\n${body}\n`,
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Guards that the given name/slug is not the reserved "index" value.
116
+ * Throws with a contextual message if it is.
117
+ */
118
+ function assertNotIndexReserved(
119
+ nameOrSlug: string,
120
+ errorMessage: string,
121
+ ) {
122
+ if (nameOrSlug === "index") {
123
+ throw new Error(errorMessage);
124
+ }
125
+ }
126
+
96
127
  // ---------------------------------------------------------------------------
97
128
  // CLAUDE.md discovery
98
129
  // ---------------------------------------------------------------------------
@@ -176,11 +207,10 @@ function generateClaudemdDocs(
176
207
 
177
208
  const emittedSlugs = new Map<string, string>();
178
209
  items.forEach((item, index) => {
179
- if (item.slug === "index") {
180
- throw new Error(
181
- `claude-resources: "${item.relPath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the directory to resolve the conflict.`,
182
- );
183
- }
210
+ assertNotIndexReserved(
211
+ item.slug,
212
+ `claude-resources: "${item.relPath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the directory to resolve the conflict.`,
213
+ );
184
214
  const previous = emittedSlugs.get(item.slug);
185
215
  if (previous !== undefined) {
186
216
  throw new Error(
@@ -232,11 +262,10 @@ function generateCommandsDocs(config: ClaudeResourcesConfig): CommandItem[] {
232
262
  if (!parsed) continue;
233
263
 
234
264
  const name = file.replace(/\.md$/, "");
235
- if (name === "index") {
236
- throw new Error(
237
- `claude-resources: ".claude/commands/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the command file to resolve the conflict.`,
238
- );
239
- }
265
+ assertNotIndexReserved(
266
+ name,
267
+ `claude-resources: ".claude/commands/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the command file to resolve the conflict.`,
268
+ );
240
269
  const description = (parsed.data.description as string) || "";
241
270
 
242
271
  items.push({ name, description });
@@ -357,11 +386,10 @@ function generateSkillsDocs(config: ClaudeResourcesConfig): SkillItem[] {
357
386
  const items: SkillItem[] = [];
358
387
 
359
388
  for (const dir of dirs) {
360
- if (dir === "index") {
361
- throw new Error(
362
- `claude-resources: skill directory ".claude/skills/index/" uses the reserved name "index", which is used for the category metadata file. Rename the skill directory to resolve the conflict.`,
363
- );
364
- }
389
+ assertNotIndexReserved(
390
+ dir,
391
+ `claude-resources: skill directory ".claude/skills/index/" uses the reserved name "index", which is used for the category metadata file. Rename the skill directory to resolve the conflict.`,
392
+ );
365
393
  const content = fs.readFileSync(
366
394
  path.join(skillsDir, dir, "SKILL.md"),
367
395
  "utf8",
@@ -445,46 +473,43 @@ ${body}`;
445
473
  const skillSlugBase = `claude-skills/${dir}`;
446
474
 
447
475
  for (const ref of references) {
448
- const subSlug = `${skillSlugBase}/ref-${ref.name}`;
449
- const refMdx = `---
450
- title: "${escapeTitle(ref.title)}"
451
- slug: "${subSlug}"
452
- unlisted: true
453
- generated: true
454
- ---
455
-
456
- ${escapeForMdx(ref.content.trim())}
457
- `;
458
- fs.writeFileSync(path.join(outputDir, `${dir}--ref-${ref.name}.mdx`), refMdx);
476
+ writeUnlistedSubPage(
477
+ path.join(outputDir, `${dir}--ref-${ref.name}.mdx`),
478
+ ref.title,
479
+ `${skillSlugBase}/ref-${ref.name}`,
480
+ escapeForMdx(ref.content.trim()),
481
+ );
459
482
  }
460
483
 
461
484
  for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
462
485
  const slug = f.replace(/\.md$/, "");
463
- const subSlug = `${skillSlugBase}/script-${slug}`;
464
486
  const raw = fs.readFileSync(
465
487
  path.join(skillsDir, dir, "scripts", f),
466
488
  "utf8",
467
489
  );
468
490
  const h1Match = raw.match(/^#\s+(.+)$/m);
469
491
  const title = h1Match ? h1Match[1] : slug;
470
- fs.writeFileSync(
492
+ writeUnlistedSubPage(
471
493
  path.join(outputDir, `${dir}--script-${slug}.mdx`),
472
- `---\ntitle: "${escapeTitle(title)}"\nslug: "${subSlug}"\nunlisted: true\ngenerated: true\n---\n\n${escapeForMdx(raw.trim())}\n`,
494
+ title,
495
+ `${skillSlugBase}/script-${slug}`,
496
+ escapeForMdx(raw.trim()),
473
497
  );
474
498
  }
475
499
 
476
500
  for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
477
501
  const slug = f.replace(/\.md$/, "");
478
- const subSlug = `${skillSlugBase}/asset-${slug}`;
479
502
  const raw = fs.readFileSync(
480
503
  path.join(skillsDir, dir, "assets", f),
481
504
  "utf8",
482
505
  );
483
506
  const h1Match = raw.match(/^#\s+(.+)$/m);
484
507
  const title = h1Match ? h1Match[1] : slug;
485
- fs.writeFileSync(
508
+ writeUnlistedSubPage(
486
509
  path.join(outputDir, `${dir}--asset-${slug}.mdx`),
487
- `---\ntitle: "${escapeTitle(title)}"\nslug: "${subSlug}"\nunlisted: true\ngenerated: true\n---\n\n${escapeForMdx(raw.trim())}\n`,
510
+ title,
511
+ `${skillSlugBase}/asset-${slug}`,
512
+ escapeForMdx(raw.trim()),
488
513
  );
489
514
  }
490
515
  }
@@ -522,11 +547,10 @@ function generateAgentsDocs(config: ClaudeResourcesConfig): AgentItem[] {
522
547
  const description = (parsed.data.description as string) || "";
523
548
  const model = (parsed.data.model as string) || "";
524
549
  const fileSlug = file.replace(/\.md$/, "");
525
- if (fileSlug === "index") {
526
- throw new Error(
527
- `claude-resources: ".claude/agents/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the agent file to resolve the conflict.`,
528
- );
529
- }
550
+ assertNotIndexReserved(
551
+ fileSlug,
552
+ `claude-resources: ".claude/agents/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the agent file to resolve the conflict.`,
553
+ );
530
554
 
531
555
  items.push({ name, file: fileSlug, description, model });
532
556
 
@@ -6,11 +6,12 @@
6
6
  *
7
7
  * Type notes:
8
8
  * - zdtp's `ColorScheme` requires a `shikiTheme: string` field that is not
9
- * present in zudo-doc's local `ColorScheme` type or data. The cast below
10
- * (`as unknown as Record<string, ZdtpColorScheme>`) is intentional: zdtp
11
- * uses `shikiTheme` only for the code-block preview inside the panel; when
12
- * absent at runtime it falls back to `colorExtras.defaultShikiTheme`. No
13
- * user-visible regression results from the missing field.
9
+ * present in this project's local `ColorScheme` type or data (zdtp uses it
10
+ * only for the panel's client-side code-block preview). Rather than an unsafe
11
+ * `as unknown as Record<string, ZdtpColorScheme>` double-cast, every local
12
+ * scheme map is run through `toZdtpColorSchemes()` below, which supplies
13
+ * `DEFAULT_SHIKI_THEME` as the fallback so the result satisfies zdtp's
14
+ * required-field shape with an ordinary type-checked assignment.
14
15
  */
15
16
 
16
17
  import type {
@@ -28,6 +29,7 @@ import {
28
29
  SIZE_TOKENS,
29
30
  } from "./design-tokens-manifest";
30
31
  import { colorSchemes } from "./color-schemes";
32
+ import type { ColorScheme as LocalColorScheme } from "./color-schemes";
31
33
  import { SEMANTIC_DEFAULTS, SEMANTIC_CSS_NAMES } from "./color-scheme-utils";
32
34
  import { settings } from "./settings";
33
35
  import { DESIGN_TOKEN_SCHEMA } from "@takazudo/zudo-doc/theme";
@@ -49,6 +51,30 @@ const BASE_DEFAULTS = {
49
51
  */
50
52
  const DEFAULT_SHIKI_THEME = "github-dark";
51
53
 
54
+ /**
55
+ * Normalize this project's local `ColorScheme` records into zdtp's
56
+ * `ColorScheme` shape. zdtp's type requires `shikiTheme: string`; the local
57
+ * scheme type makes it optional. This helper fills `DEFAULT_SHIKI_THEME` only
58
+ * when a scheme doesn't declare its own, so the result is assignable to
59
+ * `Record<string, ZdtpColorScheme>` via an ordinary type-checked assignment —
60
+ * replacing the previous `as unknown as` double-cast that bypassed every field
61
+ * check. Tracked upstream at Takazudo/zudo-design-token-panel#342 (shikiTheme
62
+ * should be optional in zdtp's `ColorScheme` type); drop this helper once that
63
+ * lands.
64
+ */
65
+ function toZdtpColorSchemes(
66
+ schemes: Record<string, LocalColorScheme>,
67
+ ): Record<string, ZdtpColorScheme> {
68
+ const normalized: Record<string, ZdtpColorScheme> = {};
69
+ for (const [name, scheme] of Object.entries(schemes)) {
70
+ normalized[name] = {
71
+ ...scheme,
72
+ shikiTheme: scheme.shikiTheme ?? DEFAULT_SHIKI_THEME,
73
+ };
74
+ }
75
+ return normalized;
76
+ }
77
+
52
78
  /**
53
79
  * Initial palette taken from the configured active scheme.
54
80
  */
@@ -178,9 +204,9 @@ const COLOR_EXTRAS: ColorClusterExtras = {
178
204
  },
179
205
  baseDefaults: BASE_DEFAULTS,
180
206
  defaultShikiTheme: DEFAULT_SHIKI_THEME,
181
- // Local ColorScheme lacks shikiTheme; cast is safe zdtp falls back to
182
- // defaultShikiTheme when shikiTheme is absent at runtime.
183
- colorSchemes: colorSchemes as unknown as Record<string, ZdtpColorScheme>,
207
+ // toZdtpColorSchemes fills the fallback only for schemes without their own
208
+ // shikiTheme, so this is a type-checked assignment rather than an unsafe cast.
209
+ colorSchemes: toZdtpColorSchemes(colorSchemes),
184
210
  panelSettings: {
185
211
  colorScheme: settings.colorScheme,
186
212
  // colorMode: strip off respectPrefersColorScheme (not in zdtp's shape).
@@ -1,12 +1,12 @@
1
+ // @ts-check
1
2
  // zfb plugin module: doc-history.
2
3
  //
3
4
  // Wires three lifecycle hooks for the doc-history integration:
4
5
  //
5
6
  // preBuild — emits `.zfb/doc-history-meta.json` (per-page git metadata
6
- // consumed at bundle time). Uses the tsx-e shim because the
7
- // runner imports Node-only modules (`fs`, `child_process`) and
8
- // TypeScript source that the plain-Node plugin host cannot load
9
- // directly. Honours `SKIP_DOC_HISTORY=1` via `env: process.env`.
7
+ // consumed at bundle time). Calls `runDocHistoryMetaStep`
8
+ // directly; honours `SKIP_DOC_HISTORY=1` via the runner's
9
+ // own env check.
10
10
  //
11
11
  // postBuild — invokes `runDocHistoryPostBuild` to write
12
12
  // `<outDir>/doc-history/<slug>.json` files. Skipped by default
@@ -20,69 +20,45 @@
20
20
  // Inline functions are not supported by zfb's plugin runtime — see
21
21
  // `@takazudo/zfb/plugins` source comment. Plugins must be authored as
22
22
  // standalone modules and referenced from `zfb.config.ts` by `name`.
23
- //
24
- // The legacy `scripts/zfb-{pre,post}build.mjs` npm-script glue and
25
- // `scripts/dev-sidecar.mjs` stay in place during the merge window;
26
- // T6 retires them once all lifecycle epics land.
27
23
 
28
- import { spawnSync } from "node:child_process";
29
- import { fileURLToPath } from "node:url";
30
- import { dirname, resolve } from "node:path";
24
+ /** @import { ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
31
25
 
26
+ import { runDocHistoryMetaStep } from "@takazudo/zudo-doc/integrations/doc-history";
32
27
  import { runDocHistoryPostBuild } from "@takazudo/zudo-doc/integrations/doc-history";
33
28
  import { createDocHistoryDevMiddleware } from "@takazudo/zudo-doc/integrations/doc-history";
34
29
  import { connectToZfbHandler } from "./connect-adapter.mjs";
35
30
 
36
- const HERE = dirname(fileURLToPath(import.meta.url));
37
- // tsx is a workspace dep; resolving the binary explicitly avoids PATH
38
- // dependency — the plugin host is spawned by zfb without the user's shell profile.
39
- const TSX_BIN = resolve(HERE, "..", "node_modules", ".bin", "tsx");
40
-
31
+ /** @type {ZfbPlugin} */
41
32
  export default {
42
33
  name: "doc-history",
43
34
 
35
+ /** @param {ZfbBuildHookContext} ctx */
44
36
  async preBuild(ctx) {
45
37
  const { docsDir, locales } = ctx.options;
46
- // Serialize options as JSON for the tsx-e inline script. The runner
47
- // (`runDocHistoryMetaStep`) honours SKIP_DOC_HISTORY=1 internally;
48
- // passing `env: process.env` propagates the flag to the child process.
49
- const optsJson = JSON.stringify({
38
+ await runDocHistoryMetaStep({
50
39
  projectRoot: ctx.projectRoot,
51
40
  docsDir: typeof docsDir === "string" ? docsDir : "src/content/docs",
52
- locales: locales ?? {},
53
- });
54
- const script = `
55
- (async () => {
56
- const { runDocHistoryMetaStep } = await import("@takazudo/zudo-doc/integrations/doc-history");
57
- const opts = ${optsJson};
58
- await runDocHistoryMetaStep(opts);
59
- })().catch((err) => {
60
- process.stderr.write(err && err.stack ? err.stack : String(err));
61
- process.exit(1);
62
- });
63
- `;
64
- const result = spawnSync(TSX_BIN, ["-e", script], {
65
- cwd: ctx.projectRoot,
66
- stdio: "inherit",
67
- env: process.env,
41
+ locales:
42
+ locales != null
43
+ ? /** @type {Record<string,{dir:string}>} */ (locales)
44
+ : undefined,
68
45
  });
69
- if (result.status !== 0) {
70
- throw new Error(
71
- `doc-history-meta preBuild failed (exit ${result.status})`,
72
- );
73
- }
74
46
  },
75
47
 
48
+ /** @param {ZfbBuildHookContext} ctx */
76
49
  async postBuild(ctx) {
77
- const { docsDir, locales } = ctx.options;
78
50
  await runDocHistoryPostBuild(
79
- { docsDir, locales },
51
+ /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
80
52
  { outDir: ctx.outDir, logger: ctx.logger },
81
53
  );
82
54
  },
83
55
 
56
+ /** @param {ZfbDevMiddlewareContext} ctx */
84
57
  devMiddleware(ctx) {
85
- const middleware = createDocHistoryDevMiddleware(ctx.options, ctx.logger);
58
+ const middleware = createDocHistoryDevMiddleware(
59
+ /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
60
+ ctx.logger,
61
+ );
86
62
  // zfb's `register(path, handler)` matches against the FULL request
87
63
  // URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
88
64
  // requests arrive as `/my-docs/doc-history/foo.json`, so we register
@@ -90,11 +66,17 @@ export default {
90
66
  // and the route is `/doc-history` as expected. The v2 middleware
91
67
  // itself is base-tolerant (matches via `url.includes("/doc-history/")`)
92
68
  // and slices from `/doc-history/` onward when proxying upstream.
93
- const basePrefix = stripTrailingSlash(ctx.options.base ?? "");
69
+ const basePrefix = stripTrailingSlash(
70
+ typeof ctx.options["base"] === "string" ? ctx.options["base"] : "",
71
+ );
94
72
  ctx.register(`${basePrefix}/doc-history`, connectToZfbHandler(middleware));
95
73
  },
96
74
  };
97
75
 
76
+ /**
77
+ * @param {string} s
78
+ * @returns {string}
79
+ */
98
80
  function stripTrailingSlash(s) {
99
81
  if (typeof s !== "string" || s.length === 0) return "";
100
82
  return s.endsWith("/") ? s.slice(0, -1) : s;
@@ -1,5 +1,7 @@
1
+ "use client";
2
+
1
3
  import { useState, useEffect, useCallback, useMemo, useRef } from "preact/compat";
2
- import { diffLines } from "diff";
4
+ import type { Change } from "diff";
3
5
  import type { DocHistoryData, DocHistoryEntry } from "@/types/doc-history";
4
6
  import { SmartBreak } from "@/utils/smart-break";
5
7
 
@@ -104,8 +106,10 @@ interface DiffRow {
104
106
  type: "context" | "removed" | "added" | "changed";
105
107
  }
106
108
 
109
+ type DiffChanges = Change[];
110
+
107
111
  function buildSideBySideRows(
108
- changes: ReturnType<typeof diffLines>,
112
+ changes: DiffChanges,
109
113
  ): DiffRow[] {
110
114
  const rows: DiffRow[] = [];
111
115
  let leftNum = 0;
@@ -177,11 +181,24 @@ function DiffViewer({
177
181
  onBack: () => void;
178
182
  showBackButton: boolean;
179
183
  }) {
180
- const changes = useMemo(
181
- () => diffLines(selection.older.content, selection.newer.content),
182
- [selection.older.content, selection.newer.content],
184
+ const [changes, setChanges] = useState<DiffChanges | null>(null);
185
+
186
+ useEffect(() => {
187
+ let cancelled = false;
188
+ // Lazy-load diff — only needed after History → Compare. This keeps the
189
+ // module out of the eager islands bundle.
190
+ import("diff").then(({ diffLines }) => {
191
+ if (!cancelled) {
192
+ setChanges(diffLines(selection.older.content, selection.newer.content));
193
+ }
194
+ });
195
+ return () => { cancelled = true; };
196
+ }, [selection.older.content, selection.newer.content]);
197
+
198
+ const rows = useMemo(
199
+ () => (changes ? buildSideBySideRows(changes) : []),
200
+ [changes],
183
201
  );
184
- const rows = useMemo(() => buildSideBySideRows(changes), [changes]);
185
202
 
186
203
  return (
187
204
  <div className="flex flex-col h-full">
@@ -207,8 +224,13 @@ function DiffViewer({
207
224
  </div>
208
225
  </div>
209
226
 
210
- {/* Side-by-side diff */}
211
- <div className="flex-1 overflow-auto">
227
+ {/* Side-by-side diff — shows a loading state while the diff module lazy-loads */}
228
+ {!changes && (
229
+ <div className="flex-1 flex items-center justify-center py-vsp-xl">
230
+ <p className="text-small text-muted">Loading diff…</p>
231
+ </div>
232
+ )}
233
+ <div className={`flex-1 overflow-auto${!changes ? " hidden" : ""}`}>
212
234
  <table className="w-full border-collapse" style={{ tableLayout: "fixed" }}>
213
235
  <colgroup>
214
236
  <col style={{ width: "2.5rem" }} />