@takazudo/zudo-doc 5.14.0 → 5.16.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/asset-components/asset-card.js +3 -1
  3. package/dist/asset-components/asset-code.js +2 -0
  4. package/dist/asset-components/shared.d.ts +5 -0
  5. package/dist/asset-components/shared.js +7 -0
  6. package/dist/asset-index-page/index.d.ts +1 -0
  7. package/dist/asset-index-page/index.js +10 -9
  8. package/dist/asset-page/index.d.ts +20 -2
  9. package/dist/asset-page/index.js +43 -24
  10. package/dist/asset-path/index.d.ts +2 -0
  11. package/dist/asset-path/index.js +11 -1
  12. package/dist/chrome/derive.js +31 -6
  13. package/dist/compiled.css +12 -0
  14. package/dist/config.d.ts +15 -6
  15. package/dist/config.js +2 -0
  16. package/dist/content/content-link.d.ts +5 -1
  17. package/dist/content/content-link.js +4 -1
  18. package/dist/doc-content-header/index.js +1 -1
  19. package/dist/doc-page-shell/index.js +1 -1
  20. package/dist/factory-context/index.d.ts +7 -3
  21. package/dist/header-with-defaults/index.js +2 -0
  22. package/dist/home-page/index.js +1 -1
  23. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +41 -7
  24. package/dist/html-preview-wrapper/html-preview-wrapper.js +86 -2
  25. package/dist/html-preview-wrapper/html-preview.d.ts +29 -4
  26. package/dist/html-preview-wrapper/html-preview.js +64 -6
  27. package/dist/html-preview-wrapper/index.d.ts +1 -1
  28. package/dist/html-preview-wrapper/preview-auto-height.d.ts +24 -0
  29. package/dist/html-preview-wrapper/preview-auto-height.js +120 -0
  30. package/dist/html-preview-wrapper/preview-base.d.ts +21 -1
  31. package/dist/html-preview-wrapper/preview-base.js +99 -50
  32. package/dist/i18n-defaults/index.d.ts +5 -3
  33. package/dist/i18n-defaults/index.js +89 -0
  34. package/dist/i18n-version/language-switcher.d.ts +10 -5
  35. package/dist/i18n-version/language-switcher.js +115 -22
  36. package/dist/i18n-version/types.d.ts +1 -1
  37. package/dist/mdx-components/index.d.ts +4 -0
  38. package/dist/mdx-components/index.js +11 -2
  39. package/dist/nav-indexing/versions-page-content.js +1 -1
  40. package/dist/plugins/claude-resources.js +15 -1
  41. package/dist/plugins/codex-resources.js +15 -1
  42. package/dist/plugins/internal/asset-viewer/asset-pages.d.ts +53 -0
  43. package/dist/plugins/internal/asset-viewer/asset-pages.js +98 -0
  44. package/dist/plugins/internal/asset-viewer/scan.d.ts +18 -1
  45. package/dist/plugins/internal/asset-viewer/scan.js +34 -27
  46. package/dist/plugins/internal/claude-resources/generate.d.ts +9 -0
  47. package/dist/plugins/internal/claude-resources/generate.js +188 -18
  48. package/dist/plugins/internal/claude-resources/index.d.ts +9 -0
  49. package/dist/plugins/internal/claude-resources/index.js +13 -1
  50. package/dist/plugins/internal/codex-resources/agents-md.js +15 -2
  51. package/dist/plugins/internal/codex-resources/agents.js +15 -2
  52. package/dist/plugins/internal/codex-resources/config.js +15 -2
  53. package/dist/plugins/internal/codex-resources/generate.d.ts +9 -0
  54. package/dist/plugins/internal/codex-resources/generate.js +19 -7
  55. package/dist/plugins/internal/codex-resources/hooks.js +15 -2
  56. package/dist/plugins/internal/codex-resources/index.d.ts +9 -0
  57. package/dist/plugins/internal/codex-resources/index.js +13 -1
  58. package/dist/plugins/internal/codex-resources/overview.js +136 -7
  59. package/dist/plugins/internal/codex-resources/rules.js +15 -2
  60. package/dist/plugins/internal/codex-resources/skills.js +16 -3
  61. package/dist/plugins/internal/llms-txt/assets.d.ts +24 -0
  62. package/dist/plugins/internal/llms-txt/assets.js +65 -0
  63. package/dist/plugins/internal/llms-txt/dev-middleware.d.ts +5 -0
  64. package/dist/plugins/internal/llms-txt/dev-middleware.js +9 -1
  65. package/dist/plugins/internal/llms-txt/emit.js +21 -5
  66. package/dist/plugins/internal/llms-txt/generate.d.ts +3 -3
  67. package/dist/plugins/internal/llms-txt/generate.js +55 -2
  68. package/dist/plugins/internal/llms-txt/index.d.ts +4 -1
  69. package/dist/plugins/internal/llms-txt/index.js +4 -0
  70. package/dist/plugins/internal/llms-txt/types.d.ts +24 -0
  71. package/dist/plugins/internal/llms-txt/types.js +4 -0
  72. package/dist/plugins/internal/resource-docs-shared/fs.d.ts +33 -0
  73. package/dist/plugins/internal/resource-docs-shared/fs.js +82 -1
  74. package/dist/plugins/internal/resource-docs-shared/index.d.ts +4 -2
  75. package/dist/plugins/internal/resource-docs-shared/index.js +21 -1
  76. package/dist/plugins/internal/resource-docs-shared/labels.d.ts +24 -0
  77. package/dist/plugins/internal/resource-docs-shared/labels.js +14 -0
  78. package/dist/plugins/internal/resource-docs-shared/locale-routes.d.ts +16 -0
  79. package/dist/plugins/internal/resource-docs-shared/locale-routes.js +14 -0
  80. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +7 -1
  81. package/dist/plugins/internal/resource-docs-shared/mdx.js +2 -2
  82. package/dist/plugins/internal/search-index/collect.js +46 -1
  83. package/dist/plugins/internal/search-index/types.d.ts +6 -1
  84. package/dist/plugins/llms-txt.js +9 -1
  85. package/dist/plugins/routes.js +14 -0
  86. package/dist/plugins/search-index.js +10 -1
  87. package/dist/preset.d.ts +9 -1
  88. package/dist/preset.js +25 -4
  89. package/dist/route-context/index.js +5 -17
  90. package/dist/route-enumerators/index.d.ts +14 -1
  91. package/dist/route-enumerators/index.js +38 -0
  92. package/dist/routes/locale-files-path.d.ts +29 -0
  93. package/dist/routes/locale-files-path.js +52 -0
  94. package/dist/safelist.css +1 -1
  95. package/dist/settings.d.ts +15 -0
  96. package/dist/tag-pages/index.js +2 -2
  97. package/eject/asset-components/asset-card.tsx +2 -0
  98. package/eject/asset-components/asset-code.tsx +2 -0
  99. package/eject/asset-components/shared.tsx +16 -0
  100. package/package.json +13 -9
  101. package/routes-src/locale-files-path.tsx +73 -0
@@ -128,7 +128,10 @@ function makeEnlargeableParagraph(imageEnlarge, ContentImg, assetOptions) {
128
128
  href: assetViewerHref({
129
129
  base: assetOptions.base,
130
130
  routePrefix: assetOptions.routePrefix,
131
- path: imageEntry.path
131
+ path: imageEntry.path,
132
+ locale: assetOptions.isDefaultLocaleOnlyPath?.(
133
+ `/${assetOptions.routePrefix}/${imageEntry.path}`
134
+ ) ? void 0 : assetOptions.locale
132
135
  }),
133
136
  children: `\u2922 Open asset page${dimensions}`
134
137
  },
@@ -160,6 +163,8 @@ function createMdxComponents(options) {
160
163
  const {
161
164
  settings,
162
165
  assetManifest = null,
166
+ assetViewerLocale,
167
+ isDefaultLocaleOnlyPath,
163
168
  locale,
164
169
  currentVersion,
165
170
  currentSlug,
@@ -176,6 +181,8 @@ function createMdxComponents(options) {
176
181
  base: settings.base,
177
182
  dir: assetDir,
178
183
  routePrefix: assetRoutePrefix,
184
+ locale: assetViewerLocale,
185
+ isDefaultLocaleOnlyPath,
179
186
  manifest: assetManifest
180
187
  }
181
188
  );
@@ -183,7 +190,9 @@ function createMdxComponents(options) {
183
190
  base: settings.base,
184
191
  assetManifest,
185
192
  routePrefix: assetRoutePrefix,
186
- dir: assetDir
193
+ dir: assetDir,
194
+ locale: assetViewerLocale,
195
+ isDefaultLocaleOnlyPath
187
196
  }) : defaultComponents.a;
188
197
  const CategoryNavBound = (props) => navData.CategoryNav({ ...props, lang: locale, currentVersion });
189
198
  const CategoryTreeNavBound = (props) => navData.CategoryTreeNav({ ...props, lang: locale, currentVersion });
@@ -3,7 +3,7 @@ import { ChevronRight } from "../icons/index.js";
3
3
  function VersionsPageContent(props) {
4
4
  const { latestHref, versions, labels } = props;
5
5
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-lg", children: labels.pageTitle }),
6
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-lg break-words", children: labels.pageTitle }),
7
7
  /* @__PURE__ */ jsxs("section", { class: "mb-vsp-xl", children: [
8
8
  /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-xs", children: labels.latestTitle }),
9
9
  /* @__PURE__ */ jsx("p", { class: "text-small text-muted mb-vsp-sm", children: labels.latestDescription }),
@@ -13,17 +13,31 @@ const plugin = {
13
13
  const projectRootOpt = ctx.options["projectRoot"];
14
14
  const scanRootOpt = ctx.options["scanRoot"];
15
15
  const docsDirOpt = ctx.options["docsDir"];
16
+ const localesOpt = ctx.options["locales"];
17
+ const defaultLocaleOpt = ctx.options["defaultLocale"];
18
+ const translationsOpt = ctx.options["translations"];
19
+ const defaultLocaleOnlyPrefixesOpt = ctx.options["defaultLocaleOnlyPrefixes"];
16
20
  const result = await runClaudeResourcesPreStep({
17
21
  claudeDir,
18
22
  projectRoot: typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
19
23
  scanRoot: typeof scanRootOpt === "string" ? scanRootOpt : void 0,
20
- docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs"
24
+ docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
25
+ locales: isRecord(localesOpt) ? localesOpt : void 0,
26
+ defaultLocale: typeof defaultLocaleOpt === "string" ? defaultLocaleOpt : void 0,
27
+ translations: isRecord(translationsOpt) ? translationsOpt : void 0,
28
+ defaultLocaleOnlyPrefixes: isStringArray(defaultLocaleOnlyPrefixesOpt) ? defaultLocaleOnlyPrefixesOpt : void 0
21
29
  });
22
30
  ctx.logger.info(
23
31
  `claude-resources: ${result.claudemd} CLAUDE.md, ${result.commands} commands, ${result.skills} skills, ${result.agents} agents`
24
32
  );
25
33
  }
26
34
  };
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function isStringArray(value) {
39
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
40
+ }
27
41
  var claude_resources_default = plugin;
28
42
  export {
29
43
  claude_resources_default as default,
@@ -13,17 +13,31 @@ const plugin = {
13
13
  const projectRootOpt = ctx.options["projectRoot"];
14
14
  const scanRootOpt = ctx.options["scanRoot"];
15
15
  const docsDirOpt = ctx.options["docsDir"];
16
+ const localesOpt = ctx.options["locales"];
17
+ const defaultLocaleOpt = ctx.options["defaultLocale"];
18
+ const translationsOpt = ctx.options["translations"];
19
+ const defaultLocaleOnlyPrefixesOpt = ctx.options["defaultLocaleOnlyPrefixes"];
16
20
  const result = runCodexResourcesPreStep({
17
21
  codexDir,
18
22
  projectRoot: typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
19
23
  scanRoot: typeof scanRootOpt === "string" ? scanRootOpt : void 0,
20
- docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs"
24
+ docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs",
25
+ locales: isRecord(localesOpt) ? localesOpt : void 0,
26
+ defaultLocale: typeof defaultLocaleOpt === "string" ? defaultLocaleOpt : void 0,
27
+ translations: isRecord(translationsOpt) ? translationsOpt : void 0,
28
+ defaultLocaleOnlyPrefixes: isStringArray(defaultLocaleOnlyPrefixesOpt) ? defaultLocaleOnlyPrefixesOpt : void 0
21
29
  });
22
30
  ctx.logger.info(
23
31
  `codex-resources: ${result.agentsMd} AGENTS.md, ${result.config} config, ${result.agents} agents, ${result.hooks} hooks, ${result.rules} rules, ${result.skills} skills`
24
32
  );
25
33
  }
26
34
  };
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function isStringArray(value) {
39
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
40
+ }
27
41
  var codex_resources_default = plugin;
28
42
  export {
29
43
  codex_resources_default as default,
@@ -0,0 +1,53 @@
1
+ import type { AssetViewerIndexingConfig } from "../../../settings.js";
2
+ /** The serialized asset-viewer settings shared by index consumers. */
3
+ export interface AssetScanProjection {
4
+ assetViewer: boolean;
5
+ assetViewerIndexing: AssetViewerIndexingConfig | false;
6
+ assetViewerDir: string;
7
+ assetViewerRoutePrefix: string;
8
+ assetViewerExclude: readonly string[];
9
+ base: string;
10
+ /** Non-default locales, keyed by locale code. */
11
+ locales: Record<string, {
12
+ dir?: string;
13
+ }>;
14
+ /** Route prefixes whose generated pages exist only in the default locale. */
15
+ defaultLocaleOnlyPrefixes: readonly string[];
16
+ }
17
+ /** The asset-page output currently supported by a consumer. */
18
+ export type AssetIndexConsumer = keyof AssetViewerIndexingConfig;
19
+ /** Minimal normalized descriptor consumed by search, llms.txt, and sitemap. */
20
+ export interface AssetPageDescriptor {
21
+ /** NFC-normalized POSIX path relative to `assetViewerDir`. */
22
+ path: string;
23
+ /** Base-prefixed, trailing-slashed generated asset-viewer URL. */
24
+ url: string;
25
+ /** Non-default locale segment; omitted for the default locale. */
26
+ locale?: string;
27
+ /** Whether the asset has a text-like extension understood by the viewer. */
28
+ isText: boolean;
29
+ /** Current byte size of the public asset. */
30
+ size: number;
31
+ }
32
+ export interface AssetPageDescriptorOptions {
33
+ /** Runtime project root injected by the zfb plugin wrapper. */
34
+ projectRoot: string;
35
+ /** Serialized projection from `zudoDocPreset()`. */
36
+ assetScan?: AssetScanProjection;
37
+ /** The output whose explicit indexing gate must be enabled. */
38
+ consumer: AssetIndexConsumer;
39
+ }
40
+ /**
41
+ * Scan the public asset directory and materialize one descriptor per emitted
42
+ * asset page. The default-locale descriptor is emitted first, followed by
43
+ * configured locales in object iteration order. Every descriptor URL is
44
+ * minted by `assetViewerHref()` so encoding, base, and trailing-slash rules
45
+ * cannot diverge between consumers.
46
+ *
47
+ * The descriptor path is intentionally synchronous. Search and llms.txt are
48
+ * synchronous build/dev paths, and `connect-adapter.ts` does not await a
49
+ * middleware's returned promise. Callers must pass the runtime `projectRoot`
50
+ * supplied by the plugin wrapper; it is never part of the serialized preset
51
+ * projection.
52
+ */
53
+ export declare function collectAssetPageDescriptors(options: AssetPageDescriptorOptions): AssetPageDescriptor[];
@@ -0,0 +1,98 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ import { extname, resolve } from "node:path";
3
+ import { assetViewerHref } from "../../../asset-path/index.js";
4
+ import { scanAssetsSync } from "./scan.js";
5
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6
+ "js",
7
+ "ts",
8
+ "tsx",
9
+ "jsx",
10
+ "mjs",
11
+ "cjs",
12
+ "json",
13
+ "yaml",
14
+ "yml",
15
+ "toml",
16
+ "css",
17
+ "scss",
18
+ "html",
19
+ "md",
20
+ "mdx",
21
+ "sh",
22
+ "bash",
23
+ "py",
24
+ "rb",
25
+ "go",
26
+ "rs",
27
+ "java",
28
+ "kt",
29
+ "swift",
30
+ "c",
31
+ "h",
32
+ "cpp",
33
+ "hpp",
34
+ "cs",
35
+ "php",
36
+ "sql",
37
+ "xml",
38
+ "txt",
39
+ "log",
40
+ "csv",
41
+ "tsv",
42
+ "env",
43
+ "conf",
44
+ "ini"
45
+ ]);
46
+ function hasBinaryPrefix(absPath) {
47
+ const fd = openSync(absPath, "r");
48
+ try {
49
+ const sample = Buffer.allocUnsafe(8 * 1024);
50
+ const bytesRead = readSync(fd, sample, 0, sample.length, 0);
51
+ return sample.subarray(0, bytesRead).includes(0);
52
+ } finally {
53
+ closeSync(fd);
54
+ }
55
+ }
56
+ function isTextAsset(absPath, path) {
57
+ const extension = extname(path).slice(1).toLowerCase();
58
+ if (extension.length !== 0 && !TEXT_EXTENSIONS.has(extension)) return false;
59
+ return !hasBinaryPrefix(absPath);
60
+ }
61
+ function isDefaultLocaleOnlyAsset(routePrefix, path, prefixes) {
62
+ const routePath = assetViewerHref({ base: "/", routePrefix, path });
63
+ return prefixes.some((prefix) => routePath.startsWith(prefix));
64
+ }
65
+ function hasConsumerGate(assetScan, consumer) {
66
+ const indexing = assetScan.assetViewerIndexing;
67
+ return assetScan.assetViewer === true && indexing !== false && typeof indexing === "object" && indexing !== null && indexing[consumer] === true;
68
+ }
69
+ function collectAssetPageDescriptors(options) {
70
+ const { projectRoot, assetScan, consumer } = options;
71
+ if (assetScan === void 0 || !hasConsumerGate(assetScan, consumer)) return [];
72
+ const dir = assetScan.assetViewerDir;
73
+ const routePrefix = assetScan.assetViewerRoutePrefix;
74
+ const base = assetScan.base;
75
+ const exclude = assetScan.assetViewerExclude ?? [];
76
+ const paths = scanAssetsSync(projectRoot, dir, exclude);
77
+ const assetRoot = resolve(projectRoot, "public", dir);
78
+ const descriptorFor = (path, locale) => ({
79
+ path,
80
+ url: assetViewerHref({ base, routePrefix, path, ...locale === void 0 ? {} : { locale } }),
81
+ ...locale === void 0 ? {} : { locale },
82
+ isText: isTextAsset(resolve(assetRoot, path), path),
83
+ size: statSync(resolve(assetRoot, path)).size
84
+ });
85
+ const descriptors = paths.map((path) => descriptorFor(path));
86
+ for (const locale of Object.keys(assetScan.locales ?? {})) {
87
+ for (const path of paths) {
88
+ if (isDefaultLocaleOnlyAsset(routePrefix, path, assetScan.defaultLocaleOnlyPrefixes ?? [])) {
89
+ continue;
90
+ }
91
+ descriptors.push(descriptorFor(path, locale));
92
+ }
93
+ }
94
+ return descriptors;
95
+ }
96
+ export {
97
+ collectAssetPageDescriptors
98
+ };
@@ -1,2 +1,19 @@
1
- /** Recursively enumerate safe, public assets in deterministic path order. */
1
+ /**
2
+ * Recursively enumerate safe, public assets in deterministic path order.
3
+ *
4
+ * This synchronous implementation is the canonical walk. Search and llms.txt
5
+ * build/dev consumers are synchronous, while the routes setup hook already
6
+ * exposes an async `scanAssets()` contract. Keeping one walk here gives both
7
+ * boundaries identical symlink, exclusion, and collision behavior.
8
+ */
9
+ export declare function scanAssetsSync(projectRoot: string, dir: string, exclude?: readonly string[]): string[];
10
+ /**
11
+ * Async-compatible asset scan used by the routes setup hook.
12
+ *
13
+ * The route plugin's public lifecycle contract is async, but its scan itself
14
+ * is only filesystem enumeration. Delegating to the canonical sync walk keeps
15
+ * it behaviorally identical to `scanAssetsSync()` without introducing a
16
+ * second implementation (or a promise that synchronous middleware cannot
17
+ * consume).
18
+ */
2
19
  export declare function scanAssets(projectRoot: string, dir: string, exclude?: readonly string[]): Promise<string[]>;
@@ -1,11 +1,27 @@
1
- import { readdir, realpath, stat } from "node:fs/promises";
1
+ import { readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { basename, relative, resolve, sep } from "node:path";
3
3
  import { matchExclude, normalizeAssetPath } from "../../../asset-path/index.js";
4
4
  function isWithin(root, candidate) {
5
5
  const rel = relative(root, candidate);
6
6
  return rel === "" || !rel.startsWith(`..${sep}`) && rel !== "..";
7
7
  }
8
- async function scanAssets(projectRoot, dir, exclude = []) {
8
+ function finishScan(paths) {
9
+ paths.sort((a, b) => a.localeCompare(b, "en"));
10
+ const folded = /* @__PURE__ */ new Map();
11
+ for (const path of paths) {
12
+ const key = path.toLocaleLowerCase("en-US");
13
+ const existing = folded.get(key);
14
+ if (existing !== void 0) {
15
+ throw new Error(`[asset-viewer] case-insensitive or Unicode-normalized URL collision: ${existing} and ${path}`);
16
+ }
17
+ folded.set(key, path);
18
+ if (path === "client" || path.startsWith("client/")) {
19
+ console.warn(`[asset-viewer] reserved client/ asset path: ${path}`);
20
+ }
21
+ }
22
+ return paths;
23
+ }
24
+ function scanAssetsSync(projectRoot, dir, exclude = []) {
9
25
  const publicRoot = resolve(projectRoot, "public");
10
26
  const assetRoot = resolve(publicRoot, dir);
11
27
  if (!isWithin(publicRoot, assetRoot)) {
@@ -13,8 +29,8 @@ async function scanAssets(projectRoot, dir, exclude = []) {
13
29
  }
14
30
  let canonicalRoot;
15
31
  try {
16
- const canonicalPublicRoot = await realpath(publicRoot);
17
- canonicalRoot = await realpath(assetRoot);
32
+ const canonicalPublicRoot = realpathSync(publicRoot);
33
+ canonicalRoot = realpathSync(assetRoot);
18
34
  if (!isWithin(canonicalPublicRoot, canonicalRoot)) {
19
35
  throw new Error(`[asset-viewer] asset directory symlink escapes public/: ${dir}`);
20
36
  }
@@ -23,29 +39,29 @@ async function scanAssets(projectRoot, dir, exclude = []) {
23
39
  throw error;
24
40
  }
25
41
  const paths = [];
26
- async function walk(absDir, relDir, canonicalAncestors) {
27
- const canonicalDir = await realpath(absDir);
42
+ function walk(absDir, relDir, canonicalAncestors) {
43
+ const canonicalDir = realpathSync(absDir);
28
44
  if (!isWithin(canonicalRoot, canonicalDir)) {
29
45
  throw new Error(`[asset-viewer] symlink escapes public/${dir}: ${relDir || basename(absDir)}`);
30
46
  }
31
47
  if (canonicalAncestors.has(canonicalDir)) return;
32
48
  const nextAncestors = new Set(canonicalAncestors).add(canonicalDir);
33
- const entries = await readdir(absDir, { withFileTypes: true });
49
+ const entries = readdirSync(absDir, { withFileTypes: true });
34
50
  entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
35
51
  for (const entry of entries) {
36
52
  if (entry.name.startsWith(".")) continue;
37
53
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
38
54
  if (entry.name.endsWith(".meta.json")) continue;
39
55
  const absPath = resolve(absDir, entry.name);
40
- const canonical = await realpath(absPath);
56
+ const canonical = realpathSync(absPath);
41
57
  if (!isWithin(canonicalRoot, canonical)) {
42
58
  throw new Error(`[asset-viewer] symlink escapes public/${dir}: ${relPath}`);
43
59
  }
44
60
  if (entry.isDirectory()) {
45
- await walk(absPath, relPath, nextAncestors);
61
+ walk(absPath, relPath, nextAncestors);
46
62
  } else if (entry.isSymbolicLink()) {
47
- const target = await stat(canonical);
48
- if (target.isDirectory()) await walk(absPath, relPath, nextAncestors);
63
+ const target = statSync(canonical);
64
+ if (target.isDirectory()) walk(absPath, relPath, nextAncestors);
49
65
  else {
50
66
  const path = normalizeAssetPath(relPath);
51
67
  if (!matchExclude(path, exclude)) paths.push(path);
@@ -56,22 +72,13 @@ async function scanAssets(projectRoot, dir, exclude = []) {
56
72
  }
57
73
  }
58
74
  }
59
- await walk(assetRoot, "", /* @__PURE__ */ new Set());
60
- paths.sort((a, b) => a.localeCompare(b, "en"));
61
- const folded = /* @__PURE__ */ new Map();
62
- for (const path of paths) {
63
- const key = path.toLocaleLowerCase("en-US");
64
- const existing = folded.get(key);
65
- if (existing !== void 0) {
66
- throw new Error(`[asset-viewer] case-insensitive or Unicode-normalized URL collision: ${existing} and ${path}`);
67
- }
68
- folded.set(key, path);
69
- if (path === "client" || path.startsWith("client/")) {
70
- console.warn(`[asset-viewer] reserved client/ asset path: ${path}`);
71
- }
72
- }
73
- return paths;
75
+ walk(assetRoot, "", /* @__PURE__ */ new Set());
76
+ return finishScan(paths);
77
+ }
78
+ async function scanAssets(projectRoot, dir, exclude = []) {
79
+ return scanAssetsSync(projectRoot, dir, exclude);
74
80
  }
75
81
  export {
76
- scanAssets
82
+ scanAssets,
83
+ scanAssetsSync
77
84
  };
@@ -1,3 +1,4 @@
1
+ import { type ResourceLocaleConfig, type ResourceTranslations } from "../resource-docs-shared/index.js";
1
2
  export interface ClaudeResourcesConfig {
2
3
  claudeDir: string;
3
4
  /**
@@ -13,6 +14,14 @@ export interface ClaudeResourcesConfig {
13
14
  */
14
15
  scanRoot?: string;
15
16
  docsDir: string;
17
+ /** Additional locale content roots, resolved by the runner when possible. */
18
+ locales?: Record<string, ResourceLocaleConfig>;
19
+ /** Default locale code (the unprefixed docs directory). */
20
+ defaultLocale?: string;
21
+ /** UI-string translation table used by localized generated indexes. */
22
+ translations?: ResourceTranslations;
23
+ /** Route prefixes that must not receive non-default-locale indexes. */
24
+ defaultLocaleOnlyPrefixes?: string[];
16
25
  }
17
26
  export declare function generateClaudeResourcesDocs(config: ClaudeResourcesConfig): {
18
27
  claudemd: number;
@@ -8,10 +8,38 @@ import {
8
8
  escapeForMdx,
9
9
  escapeTitle,
10
10
  findNamedFiles,
11
+ formatFrontmatterString,
11
12
  generateSkillsCategory,
12
13
  parseFrontmatter,
13
- writeCategoryIndex
14
+ removeGeneratedIndex,
15
+ resolveLocaleDirs,
16
+ resolveResourceLabel,
17
+ shouldEmitResourceLocaleRoute,
18
+ writeCategoryIndex,
19
+ writeGeneratedIndex
14
20
  } from "../resource-docs-shared/index.js";
21
+ const DEFAULT_RESOURCE_LOCALE = "en";
22
+ function resourceLabel(config, locale, key, fallbackLiteral) {
23
+ return resolveResourceLabel({
24
+ translations: config.translations,
25
+ locale,
26
+ defaultLocale: config.defaultLocale,
27
+ key,
28
+ fallbackLiteral
29
+ });
30
+ }
31
+ function defaultResourceLabel(config, key, fallbackLiteral) {
32
+ return resourceLabel(
33
+ config,
34
+ config.defaultLocale ?? DEFAULT_RESOURCE_LOCALE,
35
+ key,
36
+ fallbackLiteral
37
+ );
38
+ }
39
+ function formatClaudeFrontmatterString(value) {
40
+ const formatted = formatFrontmatterString(value);
41
+ return formatted === value ? JSON.stringify(value) : formatted;
42
+ }
15
43
  function generateClaudemdDocs(config) {
16
44
  const projectRoot = config.projectRoot ?? config.claudeDir;
17
45
  const scanRoot = config.scanRoot ?? projectRoot;
@@ -66,7 +94,16 @@ ${escapeForMdx(downgradeRepoRelativeLinks(content.trim()))}
66
94
  `;
67
95
  fs.writeFileSync(path.join(outputDir, `${item.slug}.mdx`), mdx);
68
96
  });
69
- writeCategoryIndex(outputDir, "CLAUDE.md", 900, "Project-specific instructions");
97
+ writeCategoryIndex(
98
+ outputDir,
99
+ defaultResourceLabel(config, "resource.claudeMd.label", "CLAUDE.md"),
100
+ 900,
101
+ defaultResourceLabel(
102
+ config,
103
+ "resource.claudeMd.description",
104
+ "Project-specific instructions"
105
+ )
106
+ );
70
107
  return items;
71
108
  }
72
109
  function generateCommandsDocs(config) {
@@ -101,16 +138,29 @@ ${escapeForMdx(parsed.content.trim())}
101
138
  fs.writeFileSync(path.join(outputDir, `${name}.mdx`), mdx);
102
139
  }
103
140
  items.sort((a, b) => a.name.localeCompare(b.name));
104
- writeCategoryIndex(outputDir, "Commands", 901, "Custom slash commands");
141
+ writeCategoryIndex(
142
+ outputDir,
143
+ defaultResourceLabel(config, "resource.claudeCommands.label", "Commands"),
144
+ 901,
145
+ defaultResourceLabel(
146
+ config,
147
+ "resource.claudeCommands.description",
148
+ "Custom slash commands"
149
+ )
150
+ );
105
151
  return items;
106
152
  }
107
153
  function generateSkillsDocs(config) {
108
154
  return generateSkillsCategory({
109
155
  skillsDirs: [path.join(config.claudeDir, "skills")],
110
156
  outputDir: path.join(config.docsDir, "claude-skills"),
111
- label: "Skills",
157
+ label: defaultResourceLabel(config, "resource.claudeSkills.label", "Skills"),
112
158
  position: 902,
113
- description: "Skill packages",
159
+ description: defaultResourceLabel(
160
+ config,
161
+ "resource.claudeSkills.description",
162
+ "Skill packages"
163
+ ),
114
164
  sourceLabel: ".claude/skills"
115
165
  });
116
166
  }
@@ -151,7 +201,16 @@ ${escapeForMdx(parsed.content.trim())}
151
201
  fs.writeFileSync(path.join(outputDir, `${fileSlug}.mdx`), mdx);
152
202
  }
153
203
  items.sort((a, b) => a.name.localeCompare(b.name));
154
- writeCategoryIndex(outputDir, "Agents", 903, "Custom subagents");
204
+ writeCategoryIndex(
205
+ outputDir,
206
+ defaultResourceLabel(config, "resource.claudeAgents.label", "Agents"),
207
+ 903,
208
+ defaultResourceLabel(
209
+ config,
210
+ "resource.claudeAgents.description",
211
+ "Custom subagents"
212
+ )
213
+ );
155
214
  return items;
156
215
  }
157
216
  function generateOverviewIndex(config, {
@@ -168,26 +227,137 @@ function generateOverviewIndex(config, {
168
227
  if (hasSkills) categorySlugs.push("claude-skills");
169
228
  if (hasAgents) categorySlugs.push("claude-agents");
170
229
  if (hasCommands) categorySlugs.push("claude-commands");
171
- const categoriesAttr = JSON.stringify(categorySlugs);
172
- const index = `---
173
- title: "Claude"
174
- description: "Claude Code configuration reference."
230
+ const index = renderOverviewIndex(
231
+ config,
232
+ config.defaultLocale ?? DEFAULT_RESOURCE_LOCALE,
233
+ categorySlugs
234
+ );
235
+ fs.writeFileSync(path.join(outputDir, "index.mdx"), index);
236
+ for (const [locale, localeConfig] of Object.entries(config.locales ?? {})) {
237
+ emitLocaleCategoryIndexes(config, locale, localeConfig.dir, {
238
+ hasCommands,
239
+ hasSkills,
240
+ hasAgents,
241
+ hasClaudemd
242
+ });
243
+ const overviewPath = path.join(localeConfig.dir, "claude", "index.mdx");
244
+ if (shouldEmitResourceLocaleRoute({
245
+ slug: "claude",
246
+ locale,
247
+ defaultLocale: config.defaultLocale,
248
+ defaultLocaleOnlyPrefixes: config.defaultLocaleOnlyPrefixes
249
+ })) {
250
+ writeGeneratedIndex(
251
+ overviewPath,
252
+ renderOverviewIndex(config, locale, categorySlugs)
253
+ );
254
+ } else {
255
+ removeGeneratedIndex(overviewPath);
256
+ }
257
+ }
258
+ }
259
+ function renderOverviewIndex(config, locale, categorySlugs) {
260
+ return `---
261
+ title: ${formatClaudeFrontmatterString(resourceLabel(config, locale, "resource.claude.title", "Claude"))}
262
+ description: ${formatClaudeFrontmatterString(resourceLabel(
263
+ config,
264
+ locale,
265
+ "resource.claude.description",
266
+ "Claude Code configuration reference."
267
+ ))}
175
268
  sidebar_position: 899
176
269
  generated: true
177
270
  ---
178
271
 
179
- ## Resources
272
+ ## ${escapeForMdx(resourceLabel(config, locale, "resource.resources", "Resources"))}
180
273
 
181
- <CategoryNav categories={${categoriesAttr}} />
274
+ <CategoryNav categories={${JSON.stringify(categorySlugs)}} />
182
275
  `;
183
- fs.writeFileSync(path.join(outputDir, "index.mdx"), index);
276
+ }
277
+ function emitLocaleCategoryIndexes(config, locale, localeDir, presence) {
278
+ writeLocaleCategoryIndex(
279
+ config,
280
+ locale,
281
+ localeDir,
282
+ "claude-md",
283
+ presence.hasClaudemd,
284
+ "resource.claudeMd.label",
285
+ "CLAUDE.md",
286
+ 900,
287
+ "resource.claudeMd.description",
288
+ "Project-specific instructions"
289
+ );
290
+ writeLocaleCategoryIndex(
291
+ config,
292
+ locale,
293
+ localeDir,
294
+ "claude-commands",
295
+ presence.hasCommands,
296
+ "resource.claudeCommands.label",
297
+ "Commands",
298
+ 901,
299
+ "resource.claudeCommands.description",
300
+ "Custom slash commands"
301
+ );
302
+ writeLocaleCategoryIndex(
303
+ config,
304
+ locale,
305
+ localeDir,
306
+ "claude-skills",
307
+ presence.hasSkills,
308
+ "resource.claudeSkills.label",
309
+ "Skills",
310
+ 902,
311
+ "resource.claudeSkills.description",
312
+ "Skill packages"
313
+ );
314
+ writeLocaleCategoryIndex(
315
+ config,
316
+ locale,
317
+ localeDir,
318
+ "claude-agents",
319
+ presence.hasAgents,
320
+ "resource.claudeAgents.label",
321
+ "Agents",
322
+ 903,
323
+ "resource.claudeAgents.description",
324
+ "Custom subagents"
325
+ );
326
+ }
327
+ function writeLocaleCategoryIndex(config, locale, localeDir, categoryDir, present, labelKey, fallbackLabel, position, descriptionKey, fallbackDescription) {
328
+ const indexPath = path.join(localeDir, categoryDir, "index.mdx");
329
+ if (!present || !shouldEmitResourceLocaleRoute({
330
+ slug: categoryDir,
331
+ locale,
332
+ defaultLocale: config.defaultLocale,
333
+ defaultLocaleOnlyPrefixes: config.defaultLocaleOnlyPrefixes
334
+ })) {
335
+ removeGeneratedIndex(indexPath);
336
+ return;
337
+ }
338
+ writeCategoryIndex(
339
+ path.join(localeDir, categoryDir),
340
+ resourceLabel(config, locale, labelKey, fallbackLabel),
341
+ position,
342
+ resourceLabel(config, locale, descriptionKey, fallbackDescription),
343
+ formatClaudeFrontmatterString,
344
+ writeGeneratedIndex
345
+ );
184
346
  }
185
347
  function generateClaudeResourcesDocs(config) {
186
- const claudemds = generateClaudemdDocs(config);
187
- const commands = generateCommandsDocs(config);
188
- const skills = generateSkillsDocs(config);
189
- const agents = generateAgentsDocs(config);
190
- generateOverviewIndex(config, {
348
+ const normalizedConfig = config.locales === void 0 ? config : {
349
+ ...config,
350
+ locales: resolveLocaleDirs({
351
+ projectRoot: path.resolve(config.projectRoot ?? config.claudeDir),
352
+ docsDir: config.docsDir,
353
+ locales: config.locales
354
+ })
355
+ };
356
+ const claudemds = generateClaudemdDocs(normalizedConfig);
357
+ const commands = generateCommandsDocs(normalizedConfig);
358
+ const skills = generateSkillsDocs(normalizedConfig);
359
+ const agents = generateAgentsDocs(normalizedConfig);
360
+ generateOverviewIndex(normalizedConfig, {
191
361
  hasClaudemd: claudemds.length > 0,
192
362
  hasCommands: commands.length > 0,
193
363
  hasSkills: skills.length > 0,