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.
- package/dist/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/compose.d.ts +2 -3
- package/dist/compose.js +7 -4
- package/dist/features/tauri.d.ts +10 -5
- package/dist/features/tauri.js +49 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +9 -6
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_body-end-islands.tsx +3 -0
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +54 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/ai-chat-modal.tsx +2 -0
- package/templates/base/src/components/doc-history.tsx +2 -0
- package/templates/base/src/components/image-enlarge.tsx +2 -0
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +11 -5
- package/templates/base/src/components/theme-toggle.tsx +18 -102
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/base/zfb-shim.d.ts +167 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +30 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +9 -3
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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 =
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
* `
|
|
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
|
+
}
|