create-zudo-doc 1.2.0 → 2.0.0
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.d.ts +6 -0
- package/dist/api.js +5 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +26 -1
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +10 -6
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +72 -1
- package/package.json +1 -1
- package/templates/base/pages/index.tsx +6 -4
- package/templates/base/pages/lib/_chrome.ts +166 -0
- package/templates/base/pages/lib/_doc-route-entries.ts +7 -51
- package/templates/base/pages/lib/_nav-source-docs.ts +9 -29
- package/templates/base/pages/lib/_route-context.ts +32 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +4 -39
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +137 -22
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +115 -28
- package/templates/features/i18n/files/pages/[locale]/index.tsx +6 -4
- package/templates/base/pages/_mdx-components.ts +0 -106
- package/templates/base/pages/lib/_category-nav.tsx +0 -34
- package/templates/base/pages/lib/_category-tree-nav.tsx +0 -45
- package/templates/base/pages/lib/_compose-meta-title.ts +0 -25
- package/templates/base/pages/lib/_doc-body-end.tsx +0 -8
- package/templates/base/pages/lib/_doc-content-header.tsx +0 -17
- package/templates/base/pages/lib/_doc-history-area.tsx +0 -34
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +0 -26
- package/templates/base/pages/lib/_doc-page-renderer.tsx +0 -31
- package/templates/base/pages/lib/_doc-page-shell.tsx +0 -29
- package/templates/base/pages/lib/_doc-pager.tsx +0 -9
- package/templates/base/pages/lib/_doc-route-paths.ts +0 -16
- package/templates/base/pages/lib/_doc-tags-area.tsx +0 -30
- package/templates/base/pages/lib/_footer-with-defaults.tsx +0 -58
- package/templates/base/pages/lib/_head-with-defaults.tsx +0 -22
- package/templates/base/pages/lib/_header-with-defaults.tsx +0 -45
- package/templates/base/pages/lib/_inline-version-switcher.tsx +0 -23
- package/templates/base/pages/lib/_math-block.tsx +0 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +0 -118
- package/templates/base/pages/lib/_search-widget-script.ts +0 -2
- package/templates/base/pages/lib/_sidebar-prepaint.tsx +0 -11
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +0 -26
- package/templates/base/pages/lib/_site-tree-nav.tsx +0 -37
- package/templates/base/pages/lib/_toc-title.ts +0 -3
- package/templates/base/pages/lib/route-enumerators.ts +0 -58
- package/templates/base/src/components/tree-nav-shared.tsx +0 -71
- package/templates/base/src/utils/content-files.ts +0 -110
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +0 -47
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +0 -33
|
@@ -111,15 +111,20 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it("generates skill as
|
|
114
|
+
it("generates skill page as <dir>/index.mdx", () => {
|
|
115
115
|
generateClaudeResourcesDocs({
|
|
116
116
|
claudeDir,
|
|
117
117
|
projectRoot: tmpDir,
|
|
118
118
|
docsDir,
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
// The skill page is the directory index so `./ref-<name>` links resolve
|
|
122
|
+
// against the source file path (#2411) — NOT a flat `<dir>.mdx`.
|
|
123
|
+
const indexPath = path.join(docsDir, "claude-skills", "test-skill", "index.mdx");
|
|
124
|
+
expect(fs.existsSync(indexPath)).toBe(true);
|
|
125
|
+
|
|
121
126
|
const flatPath = path.join(docsDir, "claude-skills", "test-skill.mdx");
|
|
122
|
-
expect(fs.existsSync(flatPath)).toBe(
|
|
127
|
+
expect(fs.existsSync(flatPath)).toBe(false);
|
|
123
128
|
});
|
|
124
129
|
});
|
|
125
130
|
|
|
@@ -150,7 +155,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
const skillPage = fs.readFileSync(
|
|
153
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
158
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
154
159
|
"utf8",
|
|
155
160
|
);
|
|
156
161
|
const parsed = matter(skillPage);
|
|
@@ -168,7 +173,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
const skillPage = fs.readFileSync(
|
|
171
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
176
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
172
177
|
"utf8",
|
|
173
178
|
);
|
|
174
179
|
|
|
@@ -179,7 +184,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
179
184
|
expect(skillPage).toContain("SKILL.md");
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
it("skill page has links to sub-files that resolve
|
|
187
|
+
it("skill page has links to sub-files that resolve to sibling nested files", () => {
|
|
183
188
|
generateClaudeResourcesDocs({
|
|
184
189
|
claudeDir,
|
|
185
190
|
projectRoot: tmpDir,
|
|
@@ -187,12 +192,12 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
187
192
|
});
|
|
188
193
|
|
|
189
194
|
const skillPage = fs.readFileSync(
|
|
190
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
195
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
191
196
|
"utf8",
|
|
192
197
|
);
|
|
193
198
|
|
|
194
|
-
// Links use ./<subpage
|
|
195
|
-
//
|
|
199
|
+
// Links use ./<subpage>; the skill page is <dir>/index.mdx so these
|
|
200
|
+
// resolve against the file path to the sibling <dir>/<subpage>.mdx (#2411).
|
|
196
201
|
expect(skillPage).toContain("./ref-guide");
|
|
197
202
|
expect(skillPage).toContain("./asset-template");
|
|
198
203
|
|
|
@@ -200,8 +205,9 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
200
205
|
expect(skillPage).not.toContain("./test-skill/ref-guide");
|
|
201
206
|
expect(skillPage).not.toContain("./test-skill/asset-template");
|
|
202
207
|
|
|
203
|
-
// Each linked sub-page must exist as a
|
|
204
|
-
//
|
|
208
|
+
// Each linked sub-page must exist as a sibling nested file inside <dir>/,
|
|
209
|
+
// i.e. claude-skills/test-skill/<subpage>.mdx — which is exactly where the
|
|
210
|
+
// ./<subpage> relative link resolves to.
|
|
205
211
|
const linkPattern = /\]\(\.\/([\w-]+)\)/g;
|
|
206
212
|
let match;
|
|
207
213
|
while ((match = linkPattern.exec(skillPage)) !== null) {
|
|
@@ -209,11 +215,12 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
209
215
|
const targetFile = path.join(
|
|
210
216
|
docsDir,
|
|
211
217
|
"claude-skills",
|
|
212
|
-
|
|
218
|
+
"test-skill",
|
|
219
|
+
`${subPage}.mdx`,
|
|
213
220
|
);
|
|
214
221
|
expect(
|
|
215
222
|
fs.existsSync(targetFile),
|
|
216
|
-
`Link target "test-skill
|
|
223
|
+
`Link target "test-skill/${subPage}.mdx" should exist`,
|
|
217
224
|
).toBe(true);
|
|
218
225
|
}
|
|
219
226
|
});
|
|
@@ -226,7 +233,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
226
233
|
});
|
|
227
234
|
|
|
228
235
|
const skillPage = fs.readFileSync(
|
|
229
|
-
path.join(docsDir, "claude-skills", "test-skill.mdx"),
|
|
236
|
+
path.join(docsDir, "claude-skills", "test-skill", "index.mdx"),
|
|
230
237
|
"utf8",
|
|
231
238
|
);
|
|
232
239
|
|
|
@@ -255,28 +262,28 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
255
262
|
// ---------------------------------------------------------------------------
|
|
256
263
|
|
|
257
264
|
describe("sub-file pages", () => {
|
|
258
|
-
it("generates unlisted reference page", () => {
|
|
265
|
+
it("generates unlisted reference page as a nested file", () => {
|
|
259
266
|
generateClaudeResourcesDocs({
|
|
260
267
|
claudeDir,
|
|
261
268
|
projectRoot: tmpDir,
|
|
262
269
|
docsDir,
|
|
263
270
|
});
|
|
264
271
|
|
|
265
|
-
const refPage = path.join(docsDir, "claude-skills", "test-skill
|
|
272
|
+
const refPage = path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx");
|
|
266
273
|
expect(fs.existsSync(refPage)).toBe(true);
|
|
267
274
|
|
|
268
275
|
const parsed = matter(fs.readFileSync(refPage, "utf8"));
|
|
269
276
|
expect(parsed.data.unlisted).toBe(true);
|
|
270
277
|
});
|
|
271
278
|
|
|
272
|
-
it("generates unlisted asset page for .md files", () => {
|
|
279
|
+
it("generates unlisted asset page for .md files as a nested file", () => {
|
|
273
280
|
generateClaudeResourcesDocs({
|
|
274
281
|
claudeDir,
|
|
275
282
|
projectRoot: tmpDir,
|
|
276
283
|
docsDir,
|
|
277
284
|
});
|
|
278
285
|
|
|
279
|
-
const assetPage = path.join(docsDir, "claude-skills", "test-skill
|
|
286
|
+
const assetPage = path.join(docsDir, "claude-skills", "test-skill", "asset-template.mdx");
|
|
280
287
|
expect(fs.existsSync(assetPage)).toBe(true);
|
|
281
288
|
|
|
282
289
|
const parsed = matter(fs.readFileSync(assetPage, "utf8"));
|
|
@@ -290,23 +297,26 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
290
297
|
docsDir,
|
|
291
298
|
});
|
|
292
299
|
|
|
293
|
-
const scriptPage = path.join(docsDir, "claude-skills", "test-skill
|
|
300
|
+
const scriptPage = path.join(docsDir, "claude-skills", "test-skill", "script-run.mdx");
|
|
294
301
|
expect(fs.existsSync(scriptPage)).toBe(false);
|
|
295
302
|
});
|
|
296
303
|
|
|
297
|
-
it("sub-pages
|
|
304
|
+
it("sub-pages derive their route from the file path (no explicit slug)", () => {
|
|
298
305
|
generateClaudeResourcesDocs({
|
|
299
306
|
claudeDir,
|
|
300
307
|
projectRoot: tmpDir,
|
|
301
308
|
docsDir,
|
|
302
309
|
});
|
|
303
310
|
|
|
311
|
+
// The page lives at claude-skills/test-skill/ref-guide.mdx, so its route
|
|
312
|
+
// is claude-skills/test-skill/ref-guide — nested breadcrumbs come from the
|
|
313
|
+
// file path, not an explicit slug (which zfb's link resolver ignores, #2411).
|
|
304
314
|
const refPage = fs.readFileSync(
|
|
305
|
-
path.join(docsDir, "claude-skills", "test-skill
|
|
315
|
+
path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
|
|
306
316
|
"utf8",
|
|
307
317
|
);
|
|
308
318
|
const parsed = matter(refPage);
|
|
309
|
-
expect(parsed.data.slug).
|
|
319
|
+
expect(parsed.data.slug).toBeUndefined();
|
|
310
320
|
});
|
|
311
321
|
|
|
312
322
|
it("reference page content is correct", () => {
|
|
@@ -317,7 +327,7 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
317
327
|
});
|
|
318
328
|
|
|
319
329
|
const refPage = fs.readFileSync(
|
|
320
|
-
path.join(docsDir, "claude-skills", "test-skill
|
|
330
|
+
path.join(docsDir, "claude-skills", "test-skill", "ref-guide.mdx"),
|
|
321
331
|
"utf8",
|
|
322
332
|
);
|
|
323
333
|
const parsed = matter(refPage);
|
|
@@ -532,4 +542,109 @@ describe("generateClaudeResourcesDocs", () => {
|
|
|
532
542
|
).toThrow(/reserved name "index"/);
|
|
533
543
|
});
|
|
534
544
|
});
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// projectRoot default scope regression test
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
describe("projectRoot default scope", () => {
|
|
551
|
+
it("walks claudeDir (not its parent) when projectRoot is omitted", () => {
|
|
552
|
+
// The fixture already writes tmpDir/CLAUDE.md (outside claudeDir).
|
|
553
|
+
// With the old bug (projectRoot = path.dirname(claudeDir) = tmpDir), the
|
|
554
|
+
// walk starts at tmpDir and picks up tmpDir/CLAUDE.md. With the fix
|
|
555
|
+
// (projectRoot = claudeDir), the walk starts inside claudeDir and does NOT
|
|
556
|
+
// reach tmpDir/CLAUDE.md (parent dirs are never walked upward).
|
|
557
|
+
//
|
|
558
|
+
// To distinguish, we write a CLAUDE.md inside claudeDir with unique content
|
|
559
|
+
// and verify the generated page body matches IT, not the outside one.
|
|
560
|
+
fs.writeFileSync(
|
|
561
|
+
path.join(claudeDir, "CLAUDE.md"),
|
|
562
|
+
"# Claude dir unique content xyz123",
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Call with NO projectRoot.
|
|
566
|
+
generateClaudeResourcesDocs({ claudeDir, docsDir });
|
|
567
|
+
|
|
568
|
+
// The generated root.mdx body must contain the inside-claudeDir content.
|
|
569
|
+
const claudeMdDir = path.join(docsDir, "claude-md");
|
|
570
|
+
const rootMdx = fs.readFileSync(path.join(claudeMdDir, "root.mdx"), "utf8");
|
|
571
|
+
expect(rootMdx).toContain("xyz123");
|
|
572
|
+
|
|
573
|
+
// And must NOT contain the outside content (from tmpDir/CLAUDE.md).
|
|
574
|
+
expect(rootMdx).not.toContain("Project instructions");
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// CLAUDE.md repo-relative link downgrade (#2411, Class B)
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
describe("CLAUDE.md repo-relative link downgrade", () => {
|
|
583
|
+
const claudeMdBody = [
|
|
584
|
+
"# Project",
|
|
585
|
+
"",
|
|
586
|
+
"See [wrangler.toml](./wrangler.toml) and [schema](../../schema/photos.sql).",
|
|
587
|
+
"Also a [workflow](../../.github/workflows/deploy.yml) and a [bare](docs/guide.md) link.",
|
|
588
|
+
"External [site](https://example.com), [anchor](#section), [site-abs](/docs/getting-started), [mail](mailto:x@y.com).",
|
|
589
|
+
"Image: ",
|
|
590
|
+
"",
|
|
591
|
+
"Inline code stays: `[x](./y)`",
|
|
592
|
+
"",
|
|
593
|
+
"```",
|
|
594
|
+
"[code](./should-not-change.md)",
|
|
595
|
+
"```",
|
|
596
|
+
"",
|
|
597
|
+
"~~~",
|
|
598
|
+
"[tilde-code](./also-should-not-change.md)",
|
|
599
|
+
"~~~",
|
|
600
|
+
"",
|
|
601
|
+
].join("\n");
|
|
602
|
+
|
|
603
|
+
function genRoot() {
|
|
604
|
+
fs.writeFileSync(path.join(tmpDir, "CLAUDE.md"), claudeMdBody);
|
|
605
|
+
generateClaudeResourcesDocs({ claudeDir, projectRoot: tmpDir, docsDir });
|
|
606
|
+
return fs.readFileSync(
|
|
607
|
+
path.join(docsDir, "claude-md", "root.mdx"),
|
|
608
|
+
"utf8",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
it("downgrades repo-relative file links to inline code", () => {
|
|
613
|
+
const rootMdx = genRoot();
|
|
614
|
+
expect(rootMdx).toContain("`wrangler.toml`");
|
|
615
|
+
expect(rootMdx).toContain("`schema`");
|
|
616
|
+
expect(rootMdx).toContain("`workflow`");
|
|
617
|
+
expect(rootMdx).toContain("`bare`");
|
|
618
|
+
// The dangling hrefs must be gone.
|
|
619
|
+
expect(rootMdx).not.toContain("](./wrangler.toml)");
|
|
620
|
+
expect(rootMdx).not.toContain("](../../schema/photos.sql)");
|
|
621
|
+
expect(rootMdx).not.toContain("](../../.github/workflows/deploy.yml)");
|
|
622
|
+
expect(rootMdx).not.toContain("](docs/guide.md)");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("preserves resolvable links (external, anchor, site-absolute, scheme)", () => {
|
|
626
|
+
const rootMdx = genRoot();
|
|
627
|
+
expect(rootMdx).toContain("](https://example.com)");
|
|
628
|
+
expect(rootMdx).toContain("](#section)");
|
|
629
|
+
expect(rootMdx).toContain("](/docs/getting-started)");
|
|
630
|
+
expect(rootMdx).toContain("](mailto:x@y.com)");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("downgrades repo-relative image links without leaving a stray '!'", () => {
|
|
634
|
+
const rootMdx = genRoot();
|
|
635
|
+
expect(rootMdx).toContain("`diagram`");
|
|
636
|
+
expect(rootMdx).not.toContain("](./diagram.png)");
|
|
637
|
+
expect(rootMdx).not.toContain("!`diagram`");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("does not rewrite links inside inline code or fenced code blocks", () => {
|
|
641
|
+
const rootMdx = genRoot();
|
|
642
|
+
// Inline-code span is preserved verbatim.
|
|
643
|
+
expect(rootMdx).toContain("`[x](./y)`");
|
|
644
|
+
// Backtick-fenced code block content is preserved verbatim.
|
|
645
|
+
expect(rootMdx).toContain("[code](./should-not-change.md)");
|
|
646
|
+
// Tilde-fenced code block content is preserved verbatim too.
|
|
647
|
+
expect(rootMdx).toContain("[tilde-code](./also-should-not-change.md)");
|
|
648
|
+
});
|
|
649
|
+
});
|
|
535
650
|
});
|
package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts
CHANGED
|
@@ -96,18 +96,24 @@ generated: true
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Writes an unlisted sub-page MDX file
|
|
100
|
-
*
|
|
99
|
+
* Writes an unlisted sub-page MDX file. Used for skill references, scripts,
|
|
100
|
+
* and assets.
|
|
101
|
+
*
|
|
102
|
+
* The route is derived from the file's path within the content collection —
|
|
103
|
+
* deliberately NOT from an explicit `slug:`. zfb's `resolveMarkdownLinks`
|
|
104
|
+
* resolves relative links against the *source file path*, so the on-disk
|
|
105
|
+
* location of these pages must match the URL the skill page links to. Writing
|
|
106
|
+
* them at `<dir>/ref-<name>.mdx` (siblings of the skill's `index.mdx`) is what
|
|
107
|
+
* makes the `./ref-<name>` links resolve (#2411).
|
|
101
108
|
*/
|
|
102
109
|
function writeUnlistedSubPage(
|
|
103
110
|
outputPath: string,
|
|
104
111
|
title: string,
|
|
105
|
-
slug: string,
|
|
106
112
|
body: string,
|
|
107
113
|
) {
|
|
108
114
|
fs.writeFileSync(
|
|
109
115
|
outputPath,
|
|
110
|
-
`---\ntitle: "${escapeTitle(title)}"\
|
|
116
|
+
`---\ntitle: "${escapeTitle(title)}"\nunlisted: true\ngenerated: true\n---\n\n${body}\n`,
|
|
111
117
|
);
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -124,6 +130,82 @@ function assertNotIndexReserved(
|
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Whether a markdown link target is a repo-relative file reference
|
|
135
|
+
* (`./wrangler.toml`, `../../schema/photos.sql`, `foo/bar.md`) rather than
|
|
136
|
+
* something the doc site can resolve: an absolute URL (`https://…`), a
|
|
137
|
+
* protocol-relative URL (`//…`), a site-absolute path (`/docs/…`), a pure
|
|
138
|
+
* anchor (`#…`), or a scheme (`mailto:`, `tel:`).
|
|
139
|
+
*/
|
|
140
|
+
function isRepoRelativeLink(url: string): boolean {
|
|
141
|
+
const trimmed = url.trim();
|
|
142
|
+
if (trimmed === "") return false;
|
|
143
|
+
if (trimmed.startsWith("#")) return false; // anchor
|
|
144
|
+
if (trimmed.startsWith("/")) return false; // site-absolute or protocol-relative (//host)
|
|
145
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false; // has a scheme (http:, mailto:, …)
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Downgrade repo-relative markdown links in a mirrored `CLAUDE.md` body to
|
|
151
|
+
* inline code so they don't dangle in the flattened mirror tree (#2411).
|
|
152
|
+
*
|
|
153
|
+
* A `CLAUDE.md`'s relative links point at real repo files (correct for an
|
|
154
|
+
* in-repo reader), but the mirror flattens each file into a single
|
|
155
|
+
* `claude-md/<name>.mdx` page with no counterpart for those targets — left as
|
|
156
|
+
* links they surface as `broken link:` warnings on every affected page. Inline
|
|
157
|
+
* code keeps the reference legible (`` `wrangler.toml` ``) without a href.
|
|
158
|
+
*
|
|
159
|
+
* Code spans are preserved verbatim: a `[x](./y)` inside a fenced block or an
|
|
160
|
+
* inline-code span is literal text, not a link, and must not be rewritten.
|
|
161
|
+
*/
|
|
162
|
+
function downgradeRepoRelativeLinks(content: string): string {
|
|
163
|
+
const blockPlaceholder = "\x00CRLINK_BLOCK_";
|
|
164
|
+
const inlinePlaceholder = "\x00CRLINK_INLINE_";
|
|
165
|
+
|
|
166
|
+
// Extract fenced code blocks so their contents are untouched. Both backtick
|
|
167
|
+
// (```) and tilde (~~~) fences are recognised; the `\1` backreference makes
|
|
168
|
+
// the closing fence match the same delimiter the block opened with.
|
|
169
|
+
const codeBlocks: string[] = [];
|
|
170
|
+
const withBlocks = content.replace(/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g, (match) => {
|
|
171
|
+
codeBlocks.push(match);
|
|
172
|
+
return `${blockPlaceholder}${codeBlocks.length - 1}\x00`;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const transformed = withBlocks
|
|
176
|
+
.split(new RegExp(`(${blockPlaceholder}\\d+\x00)`, "g"))
|
|
177
|
+
.map((part) => {
|
|
178
|
+
if (new RegExp(`^${blockPlaceholder}\\d+\x00$`).test(part)) return part;
|
|
179
|
+
|
|
180
|
+
// Preserve inline-code spans, then rewrite links in the remaining text.
|
|
181
|
+
const inlineCodes: string[] = [];
|
|
182
|
+
const withInline = part.replace(
|
|
183
|
+
/(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g,
|
|
184
|
+
(match) => {
|
|
185
|
+
inlineCodes.push(match);
|
|
186
|
+
return `${inlinePlaceholder}${inlineCodes.length - 1}\x00`;
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const rewritten = withInline.replace(
|
|
191
|
+
/!?\[([^\]]*)\]\(([^)]+)\)/g,
|
|
192
|
+
(match, text: string, url: string) =>
|
|
193
|
+
isRepoRelativeLink(url) ? `\`${text}\`` : match,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return rewritten.replace(
|
|
197
|
+
new RegExp(`${inlinePlaceholder}(\\d+)\x00`, "g"),
|
|
198
|
+
(_, idx: string) => inlineCodes[Number(idx)] ?? "",
|
|
199
|
+
);
|
|
200
|
+
})
|
|
201
|
+
.join("");
|
|
202
|
+
|
|
203
|
+
return transformed.replace(
|
|
204
|
+
new RegExp(`${blockPlaceholder}(\\d+)\x00`, "g"),
|
|
205
|
+
(_, idx: string) => codeBlocks[Number(idx)] ?? "",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
127
209
|
// ---------------------------------------------------------------------------
|
|
128
210
|
// CLAUDE.md discovery
|
|
129
211
|
// ---------------------------------------------------------------------------
|
|
@@ -164,7 +246,7 @@ function findClaudeMdFiles(dir: string, excludeDirs: string[]): string[] {
|
|
|
164
246
|
function generateClaudemdDocs(
|
|
165
247
|
config: ClaudeResourcesConfig,
|
|
166
248
|
): ClaudeMdItem[] {
|
|
167
|
-
const projectRoot = config.projectRoot ??
|
|
249
|
+
const projectRoot = config.projectRoot ?? config.claudeDir;
|
|
168
250
|
const outputDir = path.join(config.docsDir, "claude-md");
|
|
169
251
|
|
|
170
252
|
cleanDir(outputDir);
|
|
@@ -229,7 +311,7 @@ generated: true
|
|
|
229
311
|
|
|
230
312
|
**Path:** \`${item.relPath}\`
|
|
231
313
|
|
|
232
|
-
${escapeForMdx(content.trim())}
|
|
314
|
+
${escapeForMdx(downgradeRepoRelativeLinks(content.trim()))}
|
|
233
315
|
`;
|
|
234
316
|
fs.writeFileSync(path.join(outputDir, `${item.slug}.mdx`), mdx);
|
|
235
317
|
});
|
|
@@ -309,6 +391,7 @@ function getSkillFileTree(
|
|
|
309
391
|
|
|
310
392
|
for (let i = 0; i < entries.length; i++) {
|
|
311
393
|
const entry = entries[i];
|
|
394
|
+
if (!entry) continue;
|
|
312
395
|
const isLast = i === entries.length - 1;
|
|
313
396
|
const prefix = isLast ? "└── " : "├── ";
|
|
314
397
|
|
|
@@ -318,6 +401,7 @@ function getSkillFileTree(
|
|
|
318
401
|
lines.push(`${prefix}${entry.name}/`);
|
|
319
402
|
for (let j = 0; j < entry.children.length; j++) {
|
|
320
403
|
const child = entry.children[j];
|
|
404
|
+
if (!child) continue;
|
|
321
405
|
const childIsLast = j === entry.children.length - 1;
|
|
322
406
|
const continuation = isLast ? " " : "│ ";
|
|
323
407
|
const childPrefix = childIsLast ? "└── " : "├── ";
|
|
@@ -333,9 +417,10 @@ function getScriptDescription(filePath: string): string {
|
|
|
333
417
|
try {
|
|
334
418
|
const topLines = fs.readFileSync(filePath, "utf8").split("\n", 2);
|
|
335
419
|
// Skip shebang, use second line if available
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
420
|
+
const firstLine = topLines[0] ?? "";
|
|
421
|
+
const commentLine = firstLine.startsWith("#!")
|
|
422
|
+
? topLines[1] ?? ""
|
|
423
|
+
: firstLine;
|
|
339
424
|
// Match # comments (shell/python) or // comments (JS/TS)
|
|
340
425
|
const match = commentLine.match(/^(?:#|\/\/)\s*(.+)/);
|
|
341
426
|
return match ? ` — ${match[1]}` : "";
|
|
@@ -358,7 +443,7 @@ function getSkillReferences(
|
|
|
358
443
|
const content = fs.readFileSync(path.join(refsDir, f), "utf8");
|
|
359
444
|
const name = f.replace(/\.md$/, "");
|
|
360
445
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
361
|
-
const title = h1Match
|
|
446
|
+
const title = h1Match?.[1] ?? name;
|
|
362
447
|
return { name, title, content };
|
|
363
448
|
})
|
|
364
449
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -418,9 +503,9 @@ function generateSkillsDocs(config: ClaudeResourcesConfig): SkillItem[] {
|
|
|
418
503
|
if (subDirs.length > 0) {
|
|
419
504
|
const tree = `\`\`\`\n${getSkillFileTree(dir, subDirs)}\n\`\`\``;
|
|
420
505
|
|
|
421
|
-
// Collect links to all .md sub-files that get pages
|
|
422
|
-
//
|
|
423
|
-
//
|
|
506
|
+
// Collect links to all .md sub-files that get pages. Links use
|
|
507
|
+
// ./<subpage>; because the skill page is written as `<dir>/index.mdx`,
|
|
508
|
+
// these resolve to the sibling `<dir>/<subpage>.mdx` files (#2411).
|
|
424
509
|
const links: string[] = [];
|
|
425
510
|
for (const ref of references) {
|
|
426
511
|
links.push(`- [references/${ref.name}.md](./ref-${ref.name})`);
|
|
@@ -465,18 +550,22 @@ generated: true
|
|
|
465
550
|
|
|
466
551
|
${body}`;
|
|
467
552
|
|
|
468
|
-
// Write skill page as
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
553
|
+
// Write the skill page as the directory index (`<dir>/index.mdx`) so its
|
|
554
|
+
// route is `claude-skills/<dir>` served at URL `.../claude-skills/<dir>/`.
|
|
555
|
+
// This makes the reference/script/asset pages genuine siblings inside
|
|
556
|
+
// `<dir>/`, which is what lets the `./ref-<name>` links above resolve.
|
|
557
|
+
const skillDirOut = path.join(outputDir, dir);
|
|
558
|
+
ensureDir(skillDirOut);
|
|
559
|
+
fs.writeFileSync(path.join(skillDirOut, "index.mdx"), mdx);
|
|
560
|
+
|
|
561
|
+
// Generate unlisted sub-pages as nested files inside `<dir>/`. Their routes
|
|
562
|
+
// (`claude-skills/<dir>/ref-<name>`, …) are derived from these file paths
|
|
563
|
+
// and therefore match the `./ref-<name>` / `./script-<name>` /
|
|
564
|
+
// `./asset-<name>` links emitted above (#2411).
|
|
475
565
|
for (const ref of references) {
|
|
476
566
|
writeUnlistedSubPage(
|
|
477
|
-
path.join(
|
|
567
|
+
path.join(skillDirOut, `ref-${ref.name}.mdx`),
|
|
478
568
|
ref.title,
|
|
479
|
-
`${skillSlugBase}/ref-${ref.name}`,
|
|
480
569
|
escapeForMdx(ref.content.trim()),
|
|
481
570
|
);
|
|
482
571
|
}
|
|
@@ -488,11 +577,10 @@ ${body}`;
|
|
|
488
577
|
"utf8",
|
|
489
578
|
);
|
|
490
579
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
491
|
-
const title = h1Match
|
|
580
|
+
const title = h1Match?.[1] ?? slug;
|
|
492
581
|
writeUnlistedSubPage(
|
|
493
|
-
path.join(
|
|
582
|
+
path.join(skillDirOut, `script-${slug}.mdx`),
|
|
494
583
|
title,
|
|
495
|
-
`${skillSlugBase}/script-${slug}`,
|
|
496
584
|
escapeForMdx(raw.trim()),
|
|
497
585
|
);
|
|
498
586
|
}
|
|
@@ -504,11 +592,10 @@ ${body}`;
|
|
|
504
592
|
"utf8",
|
|
505
593
|
);
|
|
506
594
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
507
|
-
const title = h1Match
|
|
595
|
+
const title = h1Match?.[1] ?? slug;
|
|
508
596
|
writeUnlistedSubPage(
|
|
509
|
-
path.join(
|
|
597
|
+
path.join(skillDirOut, `asset-${slug}.mdx`),
|
|
510
598
|
title,
|
|
511
|
-
`${skillSlugBase}/asset-${slug}`,
|
|
512
599
|
escapeForMdx(raw.trim()),
|
|
513
600
|
);
|
|
514
601
|
}
|
|
@@ -32,10 +32,12 @@ import type { VNode } from "preact";
|
|
|
32
32
|
import { Island } from "@takazudo/zfb";
|
|
33
33
|
import { SiteTreeNav } from "@takazudo/zudo-doc/site-tree-nav-island";
|
|
34
34
|
import { resolveNavSource } from "../lib/_nav-source-docs";
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
import {
|
|
36
|
+
FooterWithDefaults,
|
|
37
|
+
HeaderWithDefaults,
|
|
38
|
+
HeadWithDefaults,
|
|
39
|
+
composeMetaTitle,
|
|
40
|
+
} from "../lib/_chrome";
|
|
39
41
|
import { BodyEndIslands } from "../lib/_body-end-islands";
|
|
40
42
|
|
|
41
43
|
export const frontmatter = { title: "Home" };
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Project MDX-components assembly — thin wrapper over the package factory.
|
|
2
|
-
//
|
|
3
|
-
// The bulk of the components bag (htmlOverrides, admonitions, CodeGroup,
|
|
4
|
-
// Tabs/TabItem, MathBlock, the img base-rewrite, the enlargeable-`p` override)
|
|
5
|
-
// now ships from `@takazudo/zudo-doc/mdx-components` (package-first migration,
|
|
6
|
-
// epic #2321, S8 #2332). This file keeps ONLY what is genuinely project-bound:
|
|
7
|
-
//
|
|
8
|
-
// - the 4 locale-bound nav wrappers (CategoryNav / CategoryTreeNav /
|
|
9
|
-
// SiteTreeNav / SiteTreeNavDemo) — they depend on the project's content
|
|
10
|
-
// collections + nav-tree utilities, so they cannot live in the package.
|
|
11
|
-
// They are passed into the factory as `navData`; the factory injects the
|
|
12
|
-
// active `locale` so `/ja` resolves the JA collection (the load-bearing
|
|
13
|
-
// locale thread — a static global mdx-components slot would render the EN
|
|
14
|
-
// tree on every `/ja` page, which is why this stays a per-call factory).
|
|
15
|
-
// - project `extras` spread last: HtmlPreview (with the host's global
|
|
16
|
-
// config), Details, the Island SSR pass-through, the PresetGenerator SSR
|
|
17
|
-
// shell, and the pure-showcase stubs (Avatar/Button/Card/MyComponent/
|
|
18
|
-
// PageLayout) that appear only as illustrative MDX prose.
|
|
19
|
-
|
|
20
|
-
import type { ComponentChildren } from "preact";
|
|
21
|
-
import { createMdxComponents as createMdxComponentsBase } from "@takazudo/zudo-doc/mdx-components";
|
|
22
|
-
import { HtmlPreviewWrapper, type HtmlPreviewWrapperProps } from "@takazudo/zudo-doc/html-preview-wrapper";
|
|
23
|
-
import { defaultLocale, type Locale } from "@/config/i18n";
|
|
24
|
-
import { settings } from "@/config/settings";
|
|
25
|
-
import { CategoryNavWrapper } from "./lib/_category-nav";
|
|
26
|
-
import { CategoryTreeNavWrapper } from "./lib/_category-tree-nav";
|
|
27
|
-
import { SiteTreeNavWrapper } from "./lib/_site-tree-nav";
|
|
28
|
-
import { DetailsWrapper } from "./lib/_details";
|
|
29
|
-
import { PresetGeneratorFallback } from "./lib/_preset-generator";
|
|
30
|
-
|
|
31
|
-
const HtmlPreviewWithGlobalConfig = (props: HtmlPreviewWrapperProps) =>
|
|
32
|
-
HtmlPreviewWrapper({ globalConfig: settings.htmlPreview ?? null, ...props });
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* MDX-tag stub: renders nothing. Returning `null` keeps the rendered tree
|
|
36
|
-
* intact (Preact's null-vnode path) without leaking placeholder markup into
|
|
37
|
-
* the SSR output.
|
|
38
|
-
*/
|
|
39
|
-
const MdxStub = (_props: unknown) => null;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* SSR-pass-through wrapper for `<Island when="load|idle|visible">`.
|
|
43
|
-
*
|
|
44
|
-
* In the zfb build the zfb `<Island>` component is unavailable here, so the
|
|
45
|
-
* MDX corpus tag resolves to this binding instead. Rendering the children
|
|
46
|
-
* directly ensures server-renderable content nested inside `<Island>`
|
|
47
|
-
* (headings, paragraphs, etc.) appears in the SSR HTML. The `when` prop is
|
|
48
|
-
* ignored at render time — it is only meaningful to the zfb hydration runtime
|
|
49
|
-
* on the client, which reads `data-when` on the inner SSR-skip placeholder.
|
|
50
|
-
*/
|
|
51
|
-
function IslandWrapper(props: {
|
|
52
|
-
when?: "load" | "idle" | "visible" | "media";
|
|
53
|
-
children?: ComponentChildren;
|
|
54
|
-
}): ComponentChildren {
|
|
55
|
-
return props.children ?? null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Build a locale-aware MDX components map for the given locale.
|
|
60
|
-
*
|
|
61
|
-
* Delegates the package-resident components to the `@takazudo/zudo-doc`
|
|
62
|
-
* factory and supplies the project-bound pieces:
|
|
63
|
-
* - `navData`: the 4 locale-aware nav wrappers (the factory injects `lang`).
|
|
64
|
-
* - `extras`: HtmlPreview (host-configured), Details, Island pass-through,
|
|
65
|
-
* PresetGenerator SSR shell, and the showcase stubs.
|
|
66
|
-
*
|
|
67
|
-
* Page modules should call createMdxComponents(locale) — not the static
|
|
68
|
-
* mdxComponents export — so each render gets the locale-correct map.
|
|
69
|
-
*/
|
|
70
|
-
export function createMdxComponents(lang: Locale | string = defaultLocale) {
|
|
71
|
-
return createMdxComponentsBase({
|
|
72
|
-
settings,
|
|
73
|
-
locale: lang,
|
|
74
|
-
navData: {
|
|
75
|
-
CategoryNav: CategoryNavWrapper as unknown as (props: Record<string, unknown>) => unknown,
|
|
76
|
-
CategoryTreeNav: CategoryTreeNavWrapper as unknown as (props: Record<string, unknown>) => unknown,
|
|
77
|
-
SiteTreeNav: SiteTreeNavWrapper as unknown as (props: Record<string, unknown>) => unknown,
|
|
78
|
-
},
|
|
79
|
-
extras: {
|
|
80
|
-
HtmlPreview: HtmlPreviewWithGlobalConfig,
|
|
81
|
-
Details: DetailsWrapper,
|
|
82
|
-
// SmartBreak: corpus tag with no visual rendering — render nothing.
|
|
83
|
-
SmartBreak: MdxStub,
|
|
84
|
-
// Island: pass children through so server-renderable content nested
|
|
85
|
-
// inside <Island> appears in SSR HTML. See IslandWrapper comment above.
|
|
86
|
-
Island: IslandWrapper,
|
|
87
|
-
// PresetGenerator: SSR fallback shell that renders the 8 section headings;
|
|
88
|
-
// the interactive form hydrates client-side via the SSR-skip placeholder
|
|
89
|
-
// inside PresetGeneratorFallback (see pages/lib/_preset-generator.tsx).
|
|
90
|
-
PresetGenerator: PresetGeneratorFallback,
|
|
91
|
-
// Pure showcase placeholders — appear only inside MDX prose as
|
|
92
|
-
// illustrative examples, never implemented as real components.
|
|
93
|
-
Avatar: MdxStub,
|
|
94
|
-
Button: MdxStub,
|
|
95
|
-
Card: MdxStub,
|
|
96
|
-
MyComponent: MdxStub,
|
|
97
|
-
PageLayout: MdxStub,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Static default-locale components map for backward compatibility.
|
|
104
|
-
* New page modules should call createMdxComponents(locale) instead.
|
|
105
|
-
*/
|
|
106
|
-
export const mdxComponents = createMdxComponents(defaultLocale);
|