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,22 +1,24 @@
1
1
  import type { DocsEntry } from "@/types/docs-entry";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { toTitleCase, toRouteSlug } from "@/utils/slug";
5
2
  import { docsUrl, withBase } from "@/utils/base";
6
3
  import { defaultLocale, type Locale } from "@/config/i18n";
4
+ import {
5
+ buildSidebarTree,
6
+ type CategoryMeta,
7
+ type SidebarNode,
8
+ } from "@takazudo/zudo-doc/sidebar-tree";
7
9
 
8
10
  /** Filter predicate: true when a doc should appear in navigation (sidebar, index, sitemap). */
9
11
  export function isNavVisible(doc: DocsEntry): boolean {
10
12
  return !doc.data.unlisted && !doc.data.standalone;
11
13
  }
12
14
 
13
- export interface CategoryMeta {
14
- label?: string;
15
- position?: number;
16
- description?: string;
17
- sortOrder?: "asc" | "desc";
18
- noPage?: boolean;
19
- }
15
+ // `_category_.json` loading + per-directory memoization live in the shared
16
+ // framework package — the host keeps no parallel copy (#2030 dedup). The
17
+ // package memoizes per resolved (absolute) contentDir, so the returned Map
18
+ // instance is stable per directory — which `stableMergeCategoryMeta` and the
19
+ // identity fast-path below rely on.
20
+ export { loadCategoryMeta } from "@takazudo/zudo-doc/sidebar-tree";
21
+ export type { CategoryMeta } from "@takazudo/zudo-doc/sidebar-tree";
20
22
 
21
23
  export interface NavNode {
22
24
  slug: string;
@@ -30,16 +32,39 @@ export interface NavNode {
30
32
  collapsed?: boolean;
31
33
  }
32
34
 
33
- interface BuildNode {
34
- segment: string;
35
- fullPath: string;
36
- doc?: DocsEntry;
37
- children: Map<string, BuildNode>;
35
+ // Module-level cache — persists across all page renders during a single build.
36
+ //
37
+ // Bounded LRU, cap 64. A production build only ever needs a handful of
38
+ // distinct keys — one per (locale × version × categoryMeta-variant), single
39
+ // digits for this corpus — so 64 never evicts within one build. The cap
40
+ // exists for dev: every content edit under HMR produces a NEW content key
41
+ // (the key embeds nav-affecting frontmatter by design, so edits bust the
42
+ // cache), and before #2030 the Map grew unbounded across a long dev session,
43
+ // each entry holding an O(corpus) key string plus a full tree. With the cap,
44
+ // stale generations age out once 64 fresher keys land. (#1902's WeakMap
45
+ // identity fast-path keeps production hits off this Map entirely.)
46
+ const NAV_TREE_CACHE_MAX = 64;
47
+ const navTreeCache = new Map<string, NavNode[]>();
48
+
49
+ function navTreeCacheGet(key: string): NavNode[] | undefined {
50
+ const hit = navTreeCache.get(key);
51
+ if (hit !== undefined) {
52
+ // Refresh recency — Map preserves insertion order, so delete+set moves
53
+ // the entry to the "most recently used" end.
54
+ navTreeCache.delete(key);
55
+ navTreeCache.set(key, hit);
56
+ }
57
+ return hit;
38
58
  }
39
59
 
40
- // Module-level caches persist across all page renders during a single Astro build.
41
- const categoryMetaCache = new Map<string, Map<string, CategoryMeta>>();
42
- const navTreeCache = new Map<string, NavNode[]>();
60
+ function navTreeCacheSet(key: string, value: NavNode[]): void {
61
+ navTreeCache.delete(key);
62
+ navTreeCache.set(key, value);
63
+ if (navTreeCache.size > NAV_TREE_CACHE_MAX) {
64
+ const oldest = navTreeCache.keys().next().value;
65
+ if (oldest !== undefined) navTreeCache.delete(oldest);
66
+ }
67
+ }
43
68
 
44
69
  // Identity fast-path cache. Keyed on the docs-array reference: when nav-source
45
70
  // loaders hand back the SAME stable array instance across the build's many
@@ -99,12 +124,13 @@ function navTreeCacheKey(
99
124
  }
100
125
 
101
126
  /**
102
- * Build a recursive navigation tree from a flat Astro content collection.
127
+ * Build a recursive navigation tree from a flat content collection.
103
128
  * Mirrors the filesystem: directories become category nodes, files become leaves.
104
129
  *
105
- * Astro 5 glob() strips /index from IDs:
106
- * getting-started/index.mdx ID "getting-started" (category index)
107
- * getting-started/intro.mdx ID "getting-started/intro" (child page)
130
+ * Since #2030 the tree construction itself is delegated to the shared
131
+ * framework builder (`buildSidebarTree` in @takazudo/zudo-doc/sidebar-tree);
132
+ * this function keeps the host-side concerns: the NavNode shape consumed by
133
+ * every host nav surface, the content-key cache, and the identity fast-path.
108
134
  */
109
135
  export function buildNavTree(
110
136
  docs: DocsEntry[],
@@ -123,62 +149,110 @@ export function buildNavTree(
123
149
  }
124
150
 
125
151
  const cacheKey = navTreeCacheKey(docs, lang, categoryMeta);
126
- const cached = navTreeCache.get(cacheKey);
152
+ const cached = navTreeCacheGet(cacheKey);
127
153
  if (cached) {
128
154
  rememberIdentity(docs, lang, categoryMeta, cached);
129
155
  return cached;
130
156
  }
131
157
 
132
- const root: BuildNode = {
133
- segment: "",
134
- fullPath: "",
135
- children: new Map(),
136
- };
137
-
138
- for (const doc of docs) {
139
- const slug = doc.data.slug ?? toRouteSlug(doc.id);
140
- const parts = slug.split("/");
141
-
142
- if (parts.length <= 1) {
143
- // Category index: Astro 5 stripped /index → single segment like "guides".
144
- // Key by the route slug (honors a custom frontmatter `slug`), not the raw
145
- // id otherwise the sidebar/breadcrumb link diverges from the built route.
146
- const segment = parts[0] || doc.id;
147
- if (!root.children.has(segment)) {
148
- root.children.set(segment, {
149
- segment,
150
- fullPath: segment,
151
- children: new Map(),
152
- });
153
- }
154
- root.children.get(segment)!.doc = doc;
155
- } else {
156
- // Multi-segment: walk the tree creating intermediate nodes as needed
157
- let current = root;
158
- for (let i = 0; i < parts.length; i++) {
159
- const segment = parts[i] ?? "";
160
- const fullPath = parts.slice(0, i + 1).join("/");
161
- if (!current.children.has(segment)) {
162
- current.children.set(segment, {
163
- segment,
164
- fullPath,
165
- children: new Map(),
166
- });
167
- }
168
- if (i === parts.length - 1) {
169
- current.children.get(segment)!.doc = doc;
170
- }
171
- current = current.children.get(segment)!;
172
- }
173
- }
158
+ const sidebarTree = buildSidebarTree(
159
+ // Pass `{ id, data }` only — NOT the whole entry. zfb entries carry the
160
+ // raw, un-index-stripped engine slug on the top-level `slug` field
161
+ // (e.g. "getting-started/index"), and the shared builder prefers
162
+ // `entry.slug` over the id-derived form; forwarding it would mint wrong
163
+ // node paths. Omitting it reproduces the legacy host derivation
164
+ // `data.slug ?? toRouteSlug(id)` (ids arrive pre-stripped via _data.ts).
165
+ docs.map((d) => ({ id: d.id, data: d.data })),
166
+ lang,
167
+ {
168
+ categoryMeta,
169
+ buildHref: (slug, locale) => docsUrl(slug, locale),
170
+ // Host call sites own visibility: nav surfaces pre-filter via
171
+ // `stableNavDocs(docs.filter(isNavVisible))`, while the breadcrumb tree
172
+ // intentionally builds from the UNFILTERED list so unlisted pages still
173
+ // get breadcrumbs. Disable the builder's default unlisted/standalone
174
+ // filter so neither path changes behavior.
175
+ isNavVisible: () => true,
176
+ },
177
+ );
178
+ const result = sidebarTree.map(toNavNode);
179
+
180
+ // Root docs-index entry (derived slug "" — a root index.mdx arrives from
181
+ // _data.ts bridging as id ""). The shared builder drops empty slugs, but the
182
+ // legacy host builder minted a top-level node keyed "" (href /docs/) so the
183
+ // root page stayed present in sidebar/breadcrumb/prev-next data. Re-create
184
+ // that node here with the exact legacy field derivation, then re-sort with
185
+ // the same comparator the builder used (stable sort → idempotent for the
186
+ // already-sorted rest).
187
+ const rootDoc = findRootIndexDoc(docs);
188
+ if (rootDoc) {
189
+ result.push(toRootNavNode(rootDoc, lang, categoryMeta));
190
+ result.sort((a, b) => {
191
+ const posCompare = a.position - b.position;
192
+ if (posCompare !== 0) return posCompare;
193
+ return a.slug.localeCompare(b.slug);
194
+ });
174
195
  }
175
196
 
176
- const result = toNavNodes(root, lang, categoryMeta);
177
- navTreeCache.set(cacheKey, result);
197
+ navTreeCacheSet(cacheKey, result);
178
198
  rememberIdentity(docs, lang, categoryMeta, result);
179
199
  return result;
180
200
  }
181
201
 
202
+ /** Last entry whose package-derived slug is empty ("") — i.e. the entry the
203
+ * shared builder skips. Last one wins, mirroring the legacy builder's
204
+ * `node.doc = doc` overwrite. (A bare id "index" is NOT matched here: both
205
+ * the legacy and shared builders resolve it to a node keyed "index".) */
206
+ function findRootIndexDoc(docs: DocsEntry[]): DocsEntry | undefined {
207
+ let found: DocsEntry | undefined;
208
+ for (const d of docs) {
209
+ const slug = d.data.slug ?? d.id.replace(/\/index$/, "");
210
+ if (slug === "") found = d;
211
+ }
212
+ return found;
213
+ }
214
+
215
+ /** Legacy-faithful node for the root docs index (slug ""): no children are
216
+ * possible (a multi-segment slug never has an empty first part), label falls
217
+ * back through the same chain (title is required, so it always resolves),
218
+ * and href is the locale docs root. */
219
+ function toRootNavNode(
220
+ doc: DocsEntry,
221
+ lang: Locale,
222
+ categoryMeta?: Map<string, CategoryMeta>,
223
+ ): NavNode {
224
+ const meta = categoryMeta?.get("");
225
+ const noPage = doc.data.category_no_page ?? meta?.noPage;
226
+ const sortOrder = doc.data.category_sort_order ?? meta?.sortOrder ?? "asc";
227
+ return {
228
+ slug: "",
229
+ label: doc.data.sidebar_label ?? doc.data.title ?? meta?.label ?? "",
230
+ description: doc.data.description ?? meta?.description,
231
+ position: doc.data.sidebar_position ?? meta?.position ?? 999,
232
+ href: noPage ? undefined : docsUrl("", lang),
233
+ hasPage: noPage !== true,
234
+ children: [],
235
+ sortOrder,
236
+ };
237
+ }
238
+
239
+ /** Map the shared builder's SidebarNode shape onto the host NavNode shape.
240
+ * `position` and `sortOrder` defaults mirror the legacy inline builder:
241
+ * position falls back to 999 (ties sort alphabetically) and sortOrder is
242
+ * always materialized ("asc" unless frontmatter/sidecar set it). */
243
+ function toNavNode(node: SidebarNode): NavNode {
244
+ return {
245
+ slug: node.id,
246
+ label: node.label,
247
+ description: node.description,
248
+ position: node.sidebar_position ?? 999,
249
+ href: node.href,
250
+ hasPage: node.hasPage,
251
+ children: node.children.map(toNavNode),
252
+ sortOrder: node.sortOrder ?? "asc",
253
+ };
254
+ }
255
+
182
256
  /** Record a (docs-array identity, lang, categoryMeta) → tree mapping for the
183
257
  * identity fast-path. No-op-safe to call multiple times for the same slot. */
184
258
  function rememberIdentity(
@@ -197,55 +271,6 @@ function rememberIdentity(
197
271
  }
198
272
  }
199
273
 
200
- function toNavNodes(
201
- parent: BuildNode,
202
- lang: Locale,
203
- categoryMeta?: Map<string, CategoryMeta>,
204
- parentSortOrder?: "asc" | "desc",
205
- ): NavNode[] {
206
- const nodes: NavNode[] = [];
207
-
208
- for (const child of parent.children.values()) {
209
- const doc = child.doc;
210
- const meta = categoryMeta?.get(child.fullPath);
211
- // Frontmatter wins over the `_category_.json` sidecar when both exist.
212
- const noPage = doc?.data.category_no_page ?? meta?.noPage;
213
- const sortOrder = doc?.data.category_sort_order ?? meta?.sortOrder ?? "asc";
214
- const children = toNavNodes(child, lang, categoryMeta, sortOrder);
215
-
216
- nodes.push({
217
- slug: child.fullPath,
218
- label:
219
- doc?.data.sidebar_label ?? doc?.data.title ?? meta?.label ?? toTitleCase(child.segment),
220
- description: doc?.data.description ?? meta?.description,
221
- position: doc?.data.sidebar_position ?? meta?.position ?? 999,
222
- href: noPage
223
- ? undefined
224
- : doc || children.length > 0
225
- ? docsUrl(child.fullPath, lang)
226
- : undefined,
227
- // A `category_no_page` index.mdx is metadata-only — force hasPage false so
228
- // it matches a `_category_.json` noPage category (no backing file): a
229
- // non-linked header that flattenTree drops from prev/next. Otherwise the
230
- // route-excluded slug would surface as a 404 pagination target.
231
- hasPage: !!doc && noPage !== true,
232
- children,
233
- sortOrder,
234
- });
235
- }
236
-
237
- // Use the PARENT's sortOrder to sort these sibling nodes
238
- const order = parentSortOrder ?? "asc";
239
- nodes.sort((a, b) => {
240
- const posCompare = a.position - b.position;
241
- if (posCompare !== 0) return order === "desc" ? -posCompare : posCompare;
242
- const slugCompare = a.slug.localeCompare(b.slug);
243
- return order === "desc" ? -slugCompare : slugCompare;
244
- });
245
-
246
- return nodes;
247
- }
248
-
249
274
  /**
250
275
  * Group "satellite" nodes under their primary node based on slug prefixes.
251
276
  * E.g. with prefix "claude", nodes "claude-md", "claude-commands" get moved
@@ -348,8 +373,9 @@ export interface BreadcrumbItem {
348
373
  /**
349
374
  * Build breadcrumb trail by walking the nav tree.
350
375
  *
351
- * Nav-node hrefs are always the LATEST `docsUrl(slug, lang)` values (see
352
- * `toNavNodes`). On versioned routes that would make breadcrumbs link back to
376
+ * Nav-node hrefs are always the LATEST `docsUrl(slug, lang)` values (see the
377
+ * `buildHref` wiring in `buildNavTree`). On versioned routes that would make
378
+ * breadcrumbs link back to
353
379
  * latest content (#1916 #1). Pass an optional `hrefFor(slug)` to remap each
354
380
  * intermediate crumb's href to the route's own URL space (e.g.
355
381
  * `versionedDocsUrl`-bound). The home crumb and the current/last crumb carry no
@@ -387,54 +413,3 @@ export function buildBreadcrumbs(
387
413
 
388
414
  return crumbs;
389
415
  }
390
-
391
- /**
392
- * Scan a content directory for _category_.json files and return a map
393
- * of relative paths to category metadata.
394
- * Results are memoized by contentDir.
395
- */
396
- export function loadCategoryMeta(contentDir: string): Map<string, CategoryMeta> {
397
- const cached = categoryMetaCache.get(contentDir);
398
- if (cached) return cached;
399
- const result = new Map<string, CategoryMeta>();
400
- scanDir(contentDir, contentDir, result);
401
- categoryMetaCache.set(contentDir, result);
402
- return result;
403
- }
404
-
405
- function scanDir(baseDir: string, currentDir: string, result: Map<string, CategoryMeta>): void {
406
- let entries: fs.Dirent[];
407
- try {
408
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
409
- } catch {
410
- return;
411
- }
412
-
413
- for (const entry of entries) {
414
- if (entry.isDirectory()) {
415
- const fullPath = path.join(currentDir, entry.name);
416
- const categoryFile = path.join(fullPath, "_category_.json");
417
- if (fs.existsSync(categoryFile)) {
418
- try {
419
- const raw = fs.readFileSync(categoryFile, "utf-8");
420
- const parsed: unknown = JSON.parse(raw);
421
- if (typeof parsed === "object" && parsed !== null) {
422
- const obj = parsed as Record<string, unknown>;
423
- const meta: CategoryMeta = {
424
- label: typeof obj.label === "string" ? obj.label : undefined,
425
- position: typeof obj.position === "number" ? obj.position : undefined,
426
- description: typeof obj.description === "string" ? obj.description : undefined,
427
- sortOrder: obj.sortOrder === "asc" || obj.sortOrder === "desc" ? obj.sortOrder : undefined,
428
- noPage: obj.noPage === true ? true : undefined,
429
- };
430
- const relativePath = path.relative(baseDir, fullPath);
431
- result.set(relativePath, meta);
432
- }
433
- } catch {
434
- // skip invalid JSON
435
- }
436
- }
437
- scanDir(baseDir, fullPath, result);
438
- }
439
- }
440
- }
@@ -0,0 +1,167 @@
1
+ // Local type shim for the bare `zfb/config` specifier.
2
+ //
3
+ // `@takazudo/zfb` is consumed as a published npm package (version pinned
4
+ // in the root `package.json`). The package exposes its real config types
5
+ // under the *scoped* subpath `@takazudo/zfb/config` → `dist/config.d.ts`.
6
+ // But `zfb.config.ts` imports from the *bare* specifier `zfb/config`,
7
+ // which zfb's build tool aliases to a runtime-only stub at parse time
8
+ // (`zfb-config-stub.mjs` — `defineConfig` is identity, carrying no types).
9
+ // No real file backs `zfb/config` in `node_modules`, so this ambient
10
+ // declaration is what supplies the `ZfbConfig` type to `zfb.config.ts`.
11
+ //
12
+ // IMPORTANT — this block is the source of truth for the type `zfb check`
13
+ // (plain `tsc --noEmit`) binds against the config. An ambient `declare
14
+ // module` wins over node resolution AND over tsconfig `paths`, so it must
15
+ // be kept in sync BY HAND with the published `@takazudo/zfb/config`
16
+ // (`dist/config.d.ts`). When it lags the engine, valid config fields fail
17
+ // `pnpm check` with TS2353 (see Takazudo/zudo-front-builder#678 +
18
+ // zudolab/zudo-doc#1834 — `bundle` was missing here, blocking next.22's
19
+ // `bundle.exclude`).
20
+
21
+ declare module "zfb/config" {
22
+ /** JSX framework runtime. */
23
+ export type Framework = "preact" | "react";
24
+
25
+ /** A content collection registered with the zfb engine. */
26
+ export interface CollectionDef {
27
+ /** Identifier used at the call site (e.g. `"docs"`). */
28
+ name: string;
29
+ /** Directory (relative to the project root) holding the entries. */
30
+ path: string;
31
+ /**
32
+ * Optional schema. Reserved for v1.1 — accepted but not enforced
33
+ * today. Authored as zod and converted to JSON Schema via
34
+ * `z.toJSONSchema()` at the boundary.
35
+ */
36
+ schema?: Record<string, unknown>;
37
+ }
38
+
39
+ /** Tailwind options; absent = defaults. */
40
+ export interface TailwindConfig {
41
+ enabled?: boolean;
42
+ }
43
+
44
+ /** User-supplied plugin configuration entry. */
45
+ export interface PluginConfig {
46
+ name: string;
47
+ options?: Record<string, unknown>;
48
+ }
49
+
50
+ /**
51
+ * Bundler options. Mirrors `BundleConfig` in crates/zfb/src/config.rs
52
+ * and the published `@takazudo/zfb/config` (`dist/config.d.ts`). Added
53
+ * in next.22 (`bundle.exclude`, #664) and extended in next.23
54
+ * (`mainFields` / `external`, #676).
55
+ */
56
+ export interface BundleConfig {
57
+ /**
58
+ * Project-relative, gitignore-style globs for source files the bundler
59
+ * must NOT pull into the esbuild graph (e.g. test fixtures or
60
+ * `*.stories.tsx`). Matched files are skipped from the shadow-tree walk
61
+ * and dropped from any eager `import.meta.glob(...)` expansion.
62
+ */
63
+ exclude?: string[];
64
+ /**
65
+ * Explicit esbuild `main-fields` for the `--platform=neutral` page/SSR
66
+ * pass (empty by default under `neutral`), letting CJS-`main`-only deps
67
+ * resolve. Mirrors `BundleConfig::main_fields`.
68
+ */
69
+ mainFields?: string[];
70
+ /**
71
+ * Bare specifiers to mark external in the `--platform=neutral` pass so
72
+ * esbuild leaves them unbundled. Mirrors `BundleConfig::external`.
73
+ */
74
+ external?: string[];
75
+ }
76
+
77
+ /** Mirrors the Rust `Config` struct one-for-one. */
78
+ export interface ZfbConfig {
79
+ outDir?: string;
80
+ publicDir?: string;
81
+ host?: string;
82
+ port?: number;
83
+ framework?: Framework;
84
+ collections?: CollectionDef[];
85
+ tailwind?: TailwindConfig;
86
+ /**
87
+ * Bundler options. `bundle.exclude` keeps project-relative globs out of
88
+ * the esbuild graph — used here to skip `packages/md-plugins/__fixtures__/**`
89
+ * so the MDX link resolver no longer walks the test fixtures (silences
90
+ * ~15 pre-existing broken-link warnings). Mirrors `Config::bundle`.
91
+ */
92
+ bundle?: BundleConfig;
93
+ plugins?: PluginConfig[];
94
+ adapter?: string;
95
+ /**
96
+ * Strip `.md` / `.mdx` from in-page `<a href>` paths and append a
97
+ * trailing `/` so author-written `[label](other.mdx)` references
98
+ * resolve to the rendered route URL. Mirrors Config::strip_md_ext
99
+ * in crates/zfb/src/config.rs (zudolab/zfb#131).
100
+ */
101
+ stripMdExt?: boolean;
102
+ /**
103
+ * Site base path. Prefixed onto stable HTML asset URLs (CSS / JS
104
+ * `<link>` and `<script>` tags). Normalised to start AND end with
105
+ * `/`; `undefined` / `""` / `"/"` all behave identically (no
106
+ * prefix). Mirrors Config::base in crates/zfb/src/config.rs
107
+ * (Takazudo/zudo-front-builder#154).
108
+ */
109
+ base?: string;
110
+ /**
111
+ * Configures the syntect-based syntax highlighter shipped with zfb.
112
+ * Mirrors `code_highlight` in crates/zfb/src/config.rs (Takazudo/zudo-front-builder#188 / sub #194; landed in commit 339e30f).
113
+ * When omitted, the engine falls back to the hardcoded default theme `base16-ocean.dark`.
114
+ */
115
+ codeHighlight?: {
116
+ theme?: string;
117
+ themesDir?: string;
118
+ };
119
+ /**
120
+ * Markdown link resolver (port of `remarkResolveMarkdownLinks`).
121
+ * Mirrors `Config::resolve_markdown_links` in crates/zfb/src/config.rs
122
+ * (Takazudo/zudo-front-builder PR #234 / zudolab/zudo-doc#1577).
123
+ * When `enabled: true`, the build appends `ResolveLinksPlugin` to the
124
+ * mdast pipeline so author-written `[label](./other.mdx)` links are
125
+ * rewritten to the corresponding rendered route URL — bypassing the
126
+ * file→directory transformation that breaks relative paths in dist
127
+ * HTML when `foo.mdx` becomes `foo/index.html`.
128
+ */
129
+ resolveMarkdownLinks?: {
130
+ enabled?: boolean;
131
+ docsDir?: string;
132
+ dirs?: Array<{ dir: string; routePrefix: string }>;
133
+ onBrokenLinks?: "warn" | "error" | "ignore";
134
+ };
135
+ /**
136
+ * Whether the basePath rewriter should append a trailing `/` to
137
+ * extensionless absolute hrefs. Mirrors `Config::trailing_slash` in
138
+ * crates/zfb/src/config.rs (Takazudo/zudo-front-builder PR #234 /
139
+ * zudolab/zudo-doc#1579). Off by default — preserves byte-for-byte
140
+ * parity with the pre-`trailingSlash` build for projects that
141
+ * haven't opted in.
142
+ */
143
+ trailingSlash?: boolean;
144
+ /**
145
+ * Markdown / MDX pipeline options. Mirrors `Config::markdown` →
146
+ * `MarkdownConfig` in crates/zfb/src/config.rs. zfb next.12 moved the
147
+ * former-Core features under `markdown.features` and next.13 ships the
148
+ * rest as opt-in; zudo-doc uses `markdown.features` to opt back into the
149
+ * former-Core four plus the additional opt-in features (#1804). Each
150
+ * `features` value is per-feature: `true` for boolean-shorthand features,
151
+ * or an options object for object-typed features.
152
+ */
153
+ markdown?: {
154
+ gfm?: boolean | Record<string, boolean>;
155
+ toc?: Record<string, unknown>;
156
+ externalLinks?: Record<string, unknown>;
157
+ cjkFriendly?: boolean;
158
+ features?: Record<string, boolean | Record<string, unknown>>;
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Identity helper: returns the supplied config as-is, but typed
164
+ * against `ZfbConfig`. Use as the default export of `zfb.config.ts`.
165
+ */
166
+ export function defineConfig(config: ZfbConfig): ZfbConfig;
167
+ }