create-zudo-doc 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +7 -7
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +10 -4
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/components/theme-toggle.tsx +0 -107
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
|
@@ -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
|
-
}
|
|
@@ -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.
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
123
|
-
const docsDirOpt = ctx.options
|
|
124
|
-
const result = await
|
|
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
|
|
131
|
-
// observable
|
|
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
|
);
|