create-zudo-doc 2.4.1 → 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.1";
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.1";
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/`
@@ -582,7 +582,7 @@ function generatePackageJson(choices) {
582
582
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
583
583
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
584
584
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
585
- deps["@takazudo/zudo-doc-history-server"] = "^2.4.1";
585
+ deps["@takazudo/zudo-doc-history-server"] = "^2.5.0";
586
586
  // tsx is no longer needed here: the relocated package plugin imports the
587
587
  // runner directly (no `tsx -e` spawn) since the package ships compiled
588
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.1",
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
  }