@takazudo/zudo-doc 0.2.0-next.6 → 0.2.0-next.7

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.
@@ -6,6 +6,7 @@ function buildBreadcrumbItems(tree, currentId, homeHref) {
6
6
  const path = findPath(tree, currentId);
7
7
  for (let i = 0; i < path.length; i++) {
8
8
  const node = path[i];
9
+ if (!node) continue;
9
10
  const isLast = i === path.length - 1;
10
11
  items.push({
11
12
  label: node.label,
@@ -20,7 +21,7 @@ function SmartLabel({ label }) {
20
21
  const nodes = [];
21
22
  for (let i = 0; i < parts.length; i++) {
22
23
  const part = parts[i];
23
- if (part === "") continue;
24
+ if (!part) continue;
24
25
  nodes.push(part);
25
26
  if (i % 2 === 1) nodes.push(/* @__PURE__ */ jsx("wbr", {}, `wbr-${i}`));
26
27
  }
@@ -3,7 +3,7 @@ function dedent(text) {
3
3
  let minIndent = Infinity;
4
4
  for (const line of lines) {
5
5
  if (line.trim().length === 0) continue;
6
- const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
6
+ const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
7
7
  if (indent < minIndent) minIndent = indent;
8
8
  }
9
9
  if (minIndent === 0 || minIndent === Infinity) {
@@ -53,7 +53,7 @@ function PreviewBase({
53
53
  const id = setTimeout(syncHeight, 150);
54
54
  return () => clearTimeout(id);
55
55
  }, [activeViewport, syncHeight, height]);
56
- const containerWidth = VIEWPORTS[activeViewport].width;
56
+ const containerWidth = VIEWPORTS[activeViewport]?.width ?? "100%";
57
57
  return /* @__PURE__ */ jsxs("div", { class: "border border-muted rounded-lg overflow-hidden my-vsp-md", children: [
58
58
  /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-between px-hsp-md py-hsp-sm bg-surface border-b border-muted gap-hsp-sm flex-wrap", children: [
59
59
  title && /* @__PURE__ */ jsx("span", { class: "text-caption font-semibold text-fg", children: title }),
@@ -121,7 +121,7 @@ function escapeForMdx(content) {
121
121
  ).replace(/<(-+|=+)/g, "&lt;$1").replace(/<(\d)/g, "&lt;$1").replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
122
122
  escaped = escaped.replace(
123
123
  new RegExp(`${inlinePlaceholder}(\\d+)\0`, "g"),
124
- (_, idx) => inlineCodes[Number(idx)]
124
+ (_, idx) => inlineCodes[Number(idx)] ?? ""
125
125
  );
126
126
  return escaped;
127
127
  }).join("");
@@ -153,6 +153,7 @@ function getSkillFileTree(skillDir, subDirs) {
153
153
  }
154
154
  for (let i = 0; i < entries.length; i++) {
155
155
  const entry = entries[i];
156
+ if (!entry) continue;
156
157
  const isLast = i === entries.length - 1;
157
158
  const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
158
159
  if (!entry.isDir) {
@@ -161,6 +162,7 @@ function getSkillFileTree(skillDir, subDirs) {
161
162
  lines.push(`${prefix}${entry.name}/`);
162
163
  for (let j = 0; j < entry.children.length; j++) {
163
164
  const child = entry.children[j];
165
+ if (!child) continue;
164
166
  const childIsLast = j === entry.children.length - 1;
165
167
  const continuation = isLast ? " " : "\u2502 ";
166
168
  const childPrefix = childIsLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
@@ -173,7 +175,8 @@ function getSkillFileTree(skillDir, subDirs) {
173
175
  function getScriptDescription(filePath) {
174
176
  try {
175
177
  const topLines = fs.readFileSync(filePath, "utf8").split("\n", 2);
176
- const commentLine = topLines[0].startsWith("#!") ? topLines[1] || "" : topLines[0];
178
+ const firstLine = topLines[0] ?? "";
179
+ const commentLine = firstLine.startsWith("#!") ? topLines[1] ?? "" : firstLine;
177
180
  const match = commentLine.match(/^(?:#|\/\/)\s*(.+)/);
178
181
  return match ? ` \u2014 ${match[1]}` : "";
179
182
  } catch {
@@ -187,7 +190,7 @@ function getSkillReferences(skillsDir, skillDir) {
187
190
  const content = fs.readFileSync(path.join(refsDir, f), "utf8");
188
191
  const name = f.replace(/\.md$/, "");
189
192
  const h1Match = content.match(/^#\s+(.+)$/m);
190
- const title = h1Match ? h1Match[1] : name;
193
+ const title = h1Match?.[1] ?? name;
191
194
  return { name, title, content };
192
195
  }).sort((a, b) => a.name.localeCompare(b.name));
193
196
  }
@@ -283,7 +286,7 @@ ${escapeForMdx(ref.content.trim())}
283
286
  "utf8"
284
287
  );
285
288
  const h1Match = raw.match(/^#\s+(.+)$/m);
286
- const title = h1Match ? h1Match[1] : slug;
289
+ const title = h1Match?.[1] ?? slug;
287
290
  fs.writeFileSync(
288
291
  path.join(outputDir, `${dir}--script-${slug}.mdx`),
289
292
  `---
@@ -305,7 +308,7 @@ ${escapeForMdx(raw.trim())}
305
308
  "utf8"
306
309
  );
307
310
  const h1Match = raw.match(/^#\s+(.+)$/m);
308
- const title = h1Match ? h1Match[1] : slug;
311
+ const title = h1Match?.[1] ?? slug;
309
312
  fs.writeFileSync(
310
313
  path.join(outputDir, `${dir}--asset-${slug}.mdx`),
311
314
  `---
@@ -42,6 +42,12 @@ interface DocHistoryMetaEntry {
42
42
  }
43
43
  /** Manifest shape — keyed by composedSlug. */
44
44
  type DocHistoryMetaManifest = Record<string, DocHistoryMetaEntry>;
45
+ /**
46
+ * Tiny in-file semaphore for bounded parallelism — avoids a p-limit dependency.
47
+ * Limits concurrent async tasks to `concurrency` at a time.
48
+ * Ported from packages/doc-history-server/src/cli.ts.
49
+ */
50
+ declare function makeSemaphore(concurrency: number): () => Promise<() => void>;
45
51
  /**
46
52
  * Emit `<projectRoot>/.zfb/doc-history-meta.json` from git history.
47
53
  *
@@ -55,4 +61,4 @@ type DocHistoryMetaManifest = Record<string, DocHistoryMetaEntry>;
55
61
  */
56
62
  declare function runDocHistoryMetaStep(options: RunDocHistoryMetaOptions): Promise<void>;
57
63
 
58
- export { type DocHistoryMetaEntry, type DocHistoryMetaLocaleConfig, type DocHistoryMetaManifest, type DocHistoryMetaVersionConfig, type RunDocHistoryMetaOptions, runDocHistoryMetaStep };
64
+ export { type DocHistoryMetaEntry, type DocHistoryMetaLocaleConfig, type DocHistoryMetaManifest, type DocHistoryMetaVersionConfig, type RunDocHistoryMetaOptions, makeSemaphore, runDocHistoryMetaStep };
@@ -1,11 +1,37 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { cpus } from "node:os";
3
4
  import {
4
5
  collectContentFiles,
5
- getFileCommitsMeta
6
+ getFileCommitsMetaAsync
6
7
  } from "@takazudo/zudo-doc-history-server/git-history";
7
8
  const META_OUT_RELATIVE_DIR = ".zfb";
8
9
  const META_OUT_FILENAME = "doc-history-meta.json";
10
+ function makeSemaphore(concurrency) {
11
+ let running = 0;
12
+ const queue = [];
13
+ function next() {
14
+ if (queue.length > 0 && running < concurrency) {
15
+ queue.shift()();
16
+ }
17
+ }
18
+ return function acquire() {
19
+ return new Promise((resolve) => {
20
+ function tryRun() {
21
+ if (running < concurrency) {
22
+ running++;
23
+ resolve(() => {
24
+ running--;
25
+ next();
26
+ });
27
+ } else {
28
+ queue.push(tryRun);
29
+ }
30
+ }
31
+ tryRun();
32
+ });
33
+ };
34
+ }
9
35
  async function runDocHistoryMetaStep(options) {
10
36
  const projectRoot = path.resolve(options.projectRoot);
11
37
  const zfbDir = path.join(projectRoot, META_OUT_RELATIVE_DIR);
@@ -27,24 +53,46 @@ async function runDocHistoryMetaStep(options) {
27
53
  dirEntries.push([code, path.resolve(projectRoot, locale.dir)]);
28
54
  }
29
55
  }
30
- const meta = {};
56
+ const concurrency = Math.min(8, Math.max(2, cpus().length));
57
+ const acquire = makeSemaphore(concurrency);
58
+ const jobs = [];
31
59
  for (const [localeKey, contentDir] of dirEntries) {
32
60
  const files = collectContentFiles(contentDir);
33
61
  for (const { filePath, slug } of files) {
34
- const allCommits = getFileCommitsMeta(filePath);
35
- if (allCommits.length === 0) continue;
36
- const newestInfo = allCommits[0];
37
- const oldestInfo = allCommits[allCommits.length - 1] ?? newestInfo;
38
62
  const composedSlug = localeKey ? `${localeKey}/${slug}` : slug;
39
- meta[composedSlug] = {
40
- // Author comes from the FIRST (oldest) commit.
41
- author: oldestInfo.author,
42
- // createdDate = oldest commit; updatedDate = newest commit.
43
- createdDate: oldestInfo.date,
44
- updatedDate: newestInfo.date
45
- };
63
+ jobs.push({ composedSlug, filePath });
46
64
  }
47
65
  }
66
+ const results = new Array(jobs.length).fill(null);
67
+ await Promise.all(
68
+ jobs.map(async ({ filePath }, i) => {
69
+ const release = await acquire();
70
+ try {
71
+ const allCommits = await getFileCommitsMetaAsync(filePath);
72
+ if (allCommits.length === 0) return;
73
+ const newestInfo = allCommits[0];
74
+ const oldestInfo = allCommits[allCommits.length - 1] ?? newestInfo;
75
+ results[i] = {
76
+ newest: { author: newestInfo.author, date: newestInfo.date },
77
+ oldest: { author: oldestInfo.author, date: oldestInfo.date }
78
+ };
79
+ } finally {
80
+ release();
81
+ }
82
+ })
83
+ );
84
+ const meta = {};
85
+ for (let i = 0; i < jobs.length; i++) {
86
+ const result = results[i];
87
+ if (result == null) continue;
88
+ meta[jobs[i].composedSlug] = {
89
+ // Author comes from the FIRST (oldest) commit.
90
+ author: result.oldest.author,
91
+ // createdDate = oldest commit; updatedDate = newest commit.
92
+ createdDate: result.oldest.date,
93
+ updatedDate: result.newest.date
94
+ };
95
+ }
48
96
  fs.mkdirSync(zfbDir, { recursive: true });
49
97
  fs.writeFileSync(outPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
50
98
  logger.info(
@@ -60,5 +108,6 @@ const defaultLogger = {
60
108
  }
61
109
  };
62
110
  export {
111
+ makeSemaphore,
63
112
  runDocHistoryMetaStep
64
113
  };
@@ -53,7 +53,7 @@ function createLlmsTxtDevMiddleware(options, logger) {
53
53
  };
54
54
  }
55
55
  function matchLlmsRoute(url, localeCodes) {
56
- const pathname = url.split("?")[0];
56
+ const pathname = url.split("?")[0] ?? url;
57
57
  const m = pathname.match(LLMS_KIND_PATTERN);
58
58
  if (!m) return null;
59
59
  const prefix = m[1];
@@ -20,7 +20,7 @@ function smartBreak(text) {
20
20
  const nodes = [];
21
21
  for (let i = 0; i < parts.length; i++) {
22
22
  const part = parts[i];
23
- if (part === "") continue;
23
+ if (!part) continue;
24
24
  nodes.push(part);
25
25
  if (i % 2 === 1) nodes.push(/* @__PURE__ */ jsx("wbr", {}, `wbr-${i}`));
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "0.2.0-next.6",
3
+ "version": "0.2.0-next.7",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -158,10 +158,10 @@
158
158
  ],
159
159
  "peerDependencies": {
160
160
  "preact": "^10.29.1",
161
- "@takazudo/zfb": "^0.1.0-next.31",
162
- "@takazudo/zfb-runtime": "^0.1.0-next.31",
161
+ "@takazudo/zfb": "^0.1.0-next.35",
162
+ "@takazudo/zfb-runtime": "^0.1.0-next.35",
163
163
  "@takazudo/zdtp": "^0.2.0-next.2",
164
- "@takazudo/zudo-doc-history-server": "^0.2.0-next.6"
164
+ "@takazudo/zudo-doc-history-server": "^0.2.0-next.7"
165
165
  },
166
166
  "peerDependenciesMeta": {
167
167
  "@takazudo/zudo-doc-history-server": {
@@ -182,8 +182,8 @@
182
182
  "tsup": "^8.0.0",
183
183
  "typescript": "^5.0.0",
184
184
  "vitest": "^3.0.0",
185
- "@takazudo/zfb": "0.1.0-next.31",
186
- "@takazudo/zfb-runtime": "0.1.0-next.31"
185
+ "@takazudo/zfb": "0.1.0-next.35",
186
+ "@takazudo/zfb-runtime": "0.1.0-next.35"
187
187
  },
188
188
  "scripts": {
189
189
  "build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 tsup",