create-zudo-doc 2.4.0 → 2.5.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.
@@ -11,5 +11,5 @@ export { getSecondaryLang };
11
11
  *
12
12
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
13
13
  */
14
- export declare const ZUDO_DOC_PIN = "^2.4.0";
14
+ export declare const ZUDO_DOC_PIN = "^2.5.0";
15
15
  export declare function scaffold(choices: UserChoices): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -18,7 +18,7 @@ export { getSecondaryLang };
18
18
  *
19
19
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
20
20
  */
21
- export const ZUDO_DOC_PIN = "^2.4.0";
21
+ export const ZUDO_DOC_PIN = "^2.5.0";
22
22
  /**
23
23
  * Files in `templates/base/**` that must never be copied into a generated
24
24
  * project. Each entry is matched against the path relative to `templates/base/`
@@ -489,15 +489,17 @@ function generatePackageJson(choices) {
489
489
  // next.74: routine toolchain bump from next.72 (next.73
490
490
  // skipped), adopted in lockstep with the root package.json pins. No
491
491
  // consumer-facing / CLI change.
492
- // next.75 (current pin): toolchain bump from next.74 restoring Tailwind
492
+ // next.75: toolchain bump from next.74 restoring Tailwind
493
493
  // class-candidate scanning through symlinked project trees
494
494
  // (zudolab/zudo-doc#2511), adopted in lockstep with the root package.json
495
495
  // pins. No consumer-facing / CLI change.
496
- "@takazudo/zfb": "0.1.0-next.75",
497
- "@takazudo/zfb-runtime": "0.1.0-next.75",
496
+ // next.76 (current pin): routine toolchain bump from next.75, adopted in
497
+ // lockstep with the root package.json pins. No consumer-facing / CLI change.
498
+ "@takazudo/zfb": "0.1.0-next.76",
499
+ "@takazudo/zfb-runtime": "0.1.0-next.76",
498
500
  // zfb-adapter-cloudflare — required for any route with `prerender = false`.
499
501
  // Pinned in lockstep with @takazudo/zfb.
500
- "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.75",
502
+ "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.76",
501
503
  // @takazudo/zudo-doc — published from this monorepo via
502
504
  // .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
503
505
  // lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
@@ -580,7 +582,7 @@ function generatePackageJson(choices) {
580
582
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
581
583
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
582
584
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
583
- deps["@takazudo/zudo-doc-history-server"] = "^2.4.0";
585
+ deps["@takazudo/zudo-doc-history-server"] = "^2.5.0";
584
586
  // tsx is no longer needed here: the relocated package plugin imports the
585
587
  // runner directly (no `tsx -e` spawn) since the package ships compiled
586
588
  // dist/ — package-first migration #2321 (#2337).
@@ -212,10 +212,10 @@ export function generateSettingsFile(choices) {
212
212
  if (choices.features.includes("claudeResources")) {
213
213
  lines.push(` claudeResources: {`);
214
214
  lines.push(` claudeDir: ".claude",`);
215
- lines.push(` } as { claudeDir: string; projectRoot?: string } | false,`);
215
+ lines.push(` } as { claudeDir: string; projectRoot?: string; scanRoot?: string } | false,`);
216
216
  }
217
217
  else {
218
- lines.push(` claudeResources: false as { claudeDir: string; projectRoot?: string } | false,`);
218
+ lines.push(` claudeResources: false as { claudeDir: string; projectRoot?: string; scanRoot?: string } | false,`);
219
219
  }
220
220
  if (choices.features.includes("claudeResources")) {
221
221
  lines.push(` defaultLocaleOnlyPrefixes: [`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -575,6 +575,88 @@ describe("generateClaudeResourcesDocs", () => {
575
575
  });
576
576
  });
577
577
 
578
+ // ---------------------------------------------------------------------------
579
+ // excludeDirs boundary matching (#2561)
580
+ // ---------------------------------------------------------------------------
581
+
582
+ describe("excludeDirs boundary matching", () => {
583
+ it("discovers CLAUDE.md in a prefix-colliding sibling of an excluded dir (dist-extra vs dist)", () => {
584
+ fs.mkdirSync(path.join(tmpDir, "dist-extra"), { recursive: true });
585
+ fs.writeFileSync(
586
+ path.join(tmpDir, "dist-extra", "CLAUDE.md"),
587
+ "# dist-extra instructions",
588
+ );
589
+
590
+ const result = generateClaudeResourcesDocs({
591
+ claudeDir,
592
+ projectRoot: tmpDir,
593
+ docsDir,
594
+ });
595
+
596
+ // root/CLAUDE.md (from the fixture) + dist-extra/CLAUDE.md
597
+ expect(result.claudemd).toBe(2);
598
+ const decoyPage = path.join(docsDir, "claude-md", "dist-extra.mdx");
599
+ expect(fs.existsSync(decoyPage)).toBe(true);
600
+ expect(fs.readFileSync(decoyPage, "utf8")).toContain("dist-extra instructions");
601
+ });
602
+
603
+ it("still excludes CLAUDE.md directly inside the excluded dist directory", () => {
604
+ fs.mkdirSync(path.join(tmpDir, "dist"), { recursive: true });
605
+ fs.writeFileSync(
606
+ path.join(tmpDir, "dist", "CLAUDE.md"),
607
+ "# dist instructions — should be excluded",
608
+ );
609
+
610
+ const result = generateClaudeResourcesDocs({
611
+ claudeDir,
612
+ projectRoot: tmpDir,
613
+ docsDir,
614
+ });
615
+
616
+ // Only root/CLAUDE.md (from the fixture) — dist/CLAUDE.md is excluded.
617
+ expect(result.claudemd).toBe(1);
618
+ expect(fs.existsSync(path.join(docsDir, "claude-md", "dist.mdx"))).toBe(false);
619
+ });
620
+
621
+ it("still excludes nested CLAUDE.md files under the excluded dist directory (exact-match boundary holds)", () => {
622
+ fs.mkdirSync(path.join(tmpDir, "dist", "nested"), { recursive: true });
623
+ fs.writeFileSync(
624
+ path.join(tmpDir, "dist", "nested", "CLAUDE.md"),
625
+ "# nested dist instructions — should be excluded",
626
+ );
627
+
628
+ const result = generateClaudeResourcesDocs({
629
+ claudeDir,
630
+ projectRoot: tmpDir,
631
+ docsDir,
632
+ });
633
+
634
+ // Only root/CLAUDE.md (from the fixture) — the whole dist/ subtree,
635
+ // including nested dirs, is excluded once the top-level dist match hits.
636
+ expect(result.claudemd).toBe(1);
637
+ expect(fs.existsSync(path.join(docsDir, "claude-md", "dist--nested.mdx"))).toBe(false);
638
+ });
639
+
640
+ it("still excludes docsDir contents when docsDir carries a trailing separator", () => {
641
+ // path.join preserves a trailing separator on a single argument, so the
642
+ // docsDir exclude entry keeps it — the boundary compare must not break.
643
+ fs.writeFileSync(
644
+ path.join(docsDir, "CLAUDE.md"),
645
+ "# decoy inside docsDir — should be excluded",
646
+ );
647
+
648
+ const result = generateClaudeResourcesDocs({
649
+ claudeDir,
650
+ projectRoot: tmpDir,
651
+ docsDir: docsDir + path.sep,
652
+ });
653
+
654
+ // Only root/CLAUDE.md (from the fixture) — docs/CLAUDE.md is excluded.
655
+ expect(result.claudemd).toBe(1);
656
+ expect(fs.existsSync(path.join(docsDir, "claude-md", "docs.mdx"))).toBe(false);
657
+ });
658
+ });
659
+
578
660
  // ---------------------------------------------------------------------------
579
661
  // CLAUDE.md repo-relative link downgrade (#2411, Class B)
580
662
  // ---------------------------------------------------------------------------
@@ -214,11 +214,19 @@ function findClaudeMdFiles(dir: string, excludeDirs: string[]): string[] {
214
214
  const results: string[] = [];
215
215
  if (!fs.existsSync(dir)) return results;
216
216
 
217
+ // Strip trailing separators (path.join preserves one on e.g. "docs/") so the
218
+ // boundary compare below stays exact for such entries too.
219
+ const excludes = excludeDirs.map((d) =>
220
+ d.endsWith(path.sep) ? d.slice(0, -path.sep.length) : d,
221
+ );
222
+
217
223
  for (const item of fs.readdirSync(dir)) {
218
224
  if (item === "node_modules") continue;
219
225
  if (item.startsWith(".")) continue;
220
226
  const itemPath = path.join(dir, item);
221
- if (excludeDirs.some((d) => itemPath.startsWith(d))) continue;
227
+ // Path-segment-boundary-aware: a raw startsWith(d) would also match a
228
+ // sibling like "dist-extra" against an excluded "dist" (#2561).
229
+ if (excludes.some((d) => itemPath === d || itemPath.startsWith(d + path.sep))) continue;
222
230
 
223
231
  // lstat (not stat) so symlinks aren't followed — a symlinked dir can point
224
232
  // back into the project (e.g. e2e fixtures linking to packages/) or out to
@@ -231,7 +239,7 @@ function findClaudeMdFiles(dir: string, excludeDirs: string[]): string[] {
231
239
  continue;
232
240
  }
233
241
  if (stat.isDirectory()) {
234
- results.push(...findClaudeMdFiles(itemPath, excludeDirs));
242
+ results.push(...findClaudeMdFiles(itemPath, excludes));
235
243
  } else if (stat.isFile() && item === "CLAUDE.md") {
236
244
  results.push(itemPath);
237
245
  }