@suzumiyaaoba/mdxr 0.15.0 → 0.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 (38) hide show
  1. package/dist/cli.mjs +421 -459
  2. package/dist/components.d.mts +4 -4
  3. package/dist/components.mjs +2 -2
  4. package/dist/{config-DSZfMOIO.mjs → config-By0YyXJs.mjs} +1 -1
  5. package/dist/{doc-context-Cye5isVT.d.mts → doc-context-Cz2pR4Vn.d.mts} +10 -2
  6. package/dist/index.d.mts +1 -1
  7. package/dist/index.mjs +2 -2
  8. package/dist/{ui-CyLil0AF.mjs → ui-CawgRB1q.mjs} +853 -6
  9. package/package.json +2 -1
  10. package/skill/SKILL.md +1 -1
  11. package/skill/references/components/output.md +34 -2
  12. package/skill/references/components.md +1 -0
  13. package/src/agent-http.ts +13 -33
  14. package/src/ascii/output.ts +25 -1
  15. package/src/document-history-http.ts +3 -33
  16. package/src/file-preview-http.ts +11 -27
  17. package/src/library-app.tsx +7 -601
  18. package/src/library-controls.tsx +186 -0
  19. package/src/library-document.ts +242 -0
  20. package/src/library-files.ts +162 -0
  21. package/src/library-index.ts +38 -439
  22. package/src/library-language.ts +182 -0
  23. package/src/library-results.tsx +261 -0
  24. package/src/library.ts +24 -42
  25. package/src/local-http.ts +43 -0
  26. package/src/preview-watch.ts +197 -0
  27. package/src/serve.ts +13 -181
  28. package/src/styles/globals.css +1 -0
  29. package/src/styles/interactive-graph.css +345 -0
  30. package/src/ui/graph-node-inspector.tsx +232 -0
  31. package/src/ui/graph-specs.tsx +16 -2
  32. package/src/ui/graph-view-rail.tsx +195 -0
  33. package/src/ui/graph.tsx +13 -5
  34. package/src/ui/index.ts +13 -0
  35. package/src/ui/interactive-graph-model.ts +212 -0
  36. package/src/ui/interactive-graph-specs.tsx +149 -0
  37. package/src/ui/interactive-graph.tsx +462 -0
  38. package/src/workspace-export-data.ts +2 -25
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { Bv as fenceFilename, Dv as attrTrue, Fv as takeUsedIcons, Ha as shadcnModules, Hv as parseLineRange, Iv as DocContext, Jf as dependencyMatrixModel, Jv as isRecord, Kv as langForPath, M_ as parseDiff, Mh as parseSyntaxLines, Ov as numOf, Qg as calculate, Qv as urlScheme, Rv as parseProps, S_ as tableRecords, Uv as splitPathLines, Vv as fenceLang, Wv as SKIP_LANGS, Xf as diagramModel, Xv as own, Yf as queryPlanModel, Yv as nonEmpty, Zf as flameModel, Zv as safeHref, __ as isDataRecord, _v as textList, av as isStatus, b_ as readRecords, ch as PERFORMANCE_LABELS, dh as performanceDetails, fh as performanceResult, g_ as display, gm as PlanHeader, gv as numList, h_ as columnsOf, i_ as schemaRows, lh as PERFORMANCE_TARGET_SCHEMA, n_ as reportModel, nm as PLOT_NAMES, qf as bundleModel, qv as isComponent, r_ as comparisonModel, rm as plotModel, rv as STATUSES, sh as CLAIM_LABELS, t as builtinComponents, t_ as REPORT_SPECS, uh as RESEARCH_CLAIM_SCHEMA, v_ as numberValue, x_ as records, y_ as parseJson } from "./ui-CyLil0AF.mjs";
3
- import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-DSZfMOIO.mjs";
2
+ import { $v as own, Av as attrTrue, C_ as readRecords, F_ as parseDiff, Fh as parseSyntaxLines, Gv as parseLineRange, Ha as shadcnModules, Jf as dependencyMatrixModel, Kv as splitPathLines, Qv as nonEmpty, Rv as takeUsedIcons, S_ as parseJson, T_ as tableRecords, Uv as fenceFilename, Vv as parseProps, Wv as fenceLang, Xf as diagramModel, Xv as isComponent, Yf as queryPlanModel, Yv as langForPath, Zf as flameModel, Zv as isRecord, a_ as reportModel, b_ as isDataRecord, bv as textList, cv as isStatus, dh as PERFORMANCE_LABELS, ey as safeHref, fh as PERFORMANCE_TARGET_SCHEMA, gm as PlanHeader, hh as performanceResult, i_ as REPORT_SPECS, jv as numOf, mh as performanceDetails, nm as PLOT_NAMES, o_ as comparisonModel, ov as STATUSES, ph as RESEARCH_CLAIM_SCHEMA, qf as bundleModel, qv as SKIP_LANGS, rm as plotModel, s_ as schemaRows, t as builtinComponents, t_ as calculate, ty as urlScheme, uh as CLAIM_LABELS, v_ as columnsOf, w_ as records, x_ as numberValue, y_ as display, yv as numList, zv as DocContext } from "./ui-CawgRB1q.mjs";
3
+ import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-By0YyXJs.mjs";
4
4
  import module from "node:module";
5
5
  import fsp, { access, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
@@ -46,7 +46,7 @@ import { compile as compile$1 } from "@mdx-js/mdx";
46
46
  import { renderToStaticMarkup, renderToString } from "react-dom/server";
47
47
  import rehypeKatex from "rehype-katex";
48
48
  //#region package.json
49
- var version = "0.15.0";
49
+ var version = "0.16.0";
50
50
  //#endregion
51
51
  //#region src/agent-options.ts
52
52
  const validateAgentOptions = (agent, session, server) => {
@@ -3147,18 +3147,25 @@ const outputRenderers = {
3147
3147
  ])] },
3148
3148
  Endpoints: { flow: endpoints },
3149
3149
  Graph: { flow: graph },
3150
+ GraphGroup: { flow: (node, ctx) => [...caption(attr(node, "label") ?? attr(node, "id")), ...ctx.children(node)] },
3151
+ GraphView: { flow: (node, ctx) => [
3152
+ ...caption(attr(node, "label") ?? attr(node, "id")),
3153
+ para([txt(`nodes: ${attr(node, "nodes") ?? ""}; edges: ${attr(node, "edges") ?? ""}`)]),
3154
+ ...ctx.children(node)
3155
+ ] },
3150
3156
  Ins: { text: (n, ctx) => [
3151
3157
  html("<ins>"),
3152
3158
  ...ctx.inline(n),
3153
3159
  html("</ins>")
3154
3160
  ] },
3161
+ InteractiveGraph: { flow: (node, ctx) => [...caption(attr(node, "title")), ...ctx.children(node)] },
3155
3162
  Json: { flow: json },
3156
3163
  Node: {
3157
- flow: (n) => [para([
3164
+ flow: (n, ctx) => [para([
3158
3165
  icode(attr(n, "id") ?? ""),
3159
3166
  ...nonEmpty(attr(n, "label")) ? [txt(` = ${attr(n, "label")}`)] : [],
3160
3167
  ...suffix([attr(n, "note"), attr(n, "status")])
3161
- ])],
3168
+ ]), ...ctx.children(n)],
3162
3169
  text: (n) => [icode(attr(n, "id") ?? "")]
3163
3170
  },
3164
3171
  Span: { flow: (n) => [para([icode(attr(n, "name") ?? ""), ...suffix([attr(n, "duration"), attr(n, "note")])])] },
@@ -5797,6 +5804,217 @@ const installInstructions = async (opts) => {
5797
5804
  return await Promise.all(prepared.map(async (target) => await installTarget(target, opts.force === true)));
5798
5805
  };
5799
5806
  //#endregion
5807
+ //#region src/library-document.ts
5808
+ const ignoredNodeTypes = /* @__PURE__ */ new Set([
5809
+ "definition",
5810
+ "mdxFlowExpression",
5811
+ "mdxjsEsm",
5812
+ "mdxTextExpression",
5813
+ "toml",
5814
+ "yaml"
5815
+ ]);
5816
+ const searchableMdxAttributes = /* @__PURE__ */ new Set([
5817
+ "alt",
5818
+ "aria-label",
5819
+ "caption",
5820
+ "description",
5821
+ "heading",
5822
+ "label",
5823
+ "name",
5824
+ "placeholder",
5825
+ "summary",
5826
+ "text",
5827
+ "title",
5828
+ "tooltip",
5829
+ "value"
5830
+ ]);
5831
+ const blockNodeTypes = /* @__PURE__ */ new Set([
5832
+ "blockquote",
5833
+ "code",
5834
+ "heading",
5835
+ "listItem",
5836
+ "paragraph",
5837
+ "tableCell",
5838
+ "tableRow",
5839
+ "thematicBreak"
5840
+ ]);
5841
+ const mdxParser = unified().use(remarkParse).use(remarkMdx).use(remarkFrontmatter).use(remarkGfm);
5842
+ const markdownParser = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm);
5843
+ const scalarText = (value) => {
5844
+ if (typeof value === "string" || typeof value === "number") return String(value).trim();
5845
+ if (typeof value === "boolean" || typeof value === "bigint") return String(value);
5846
+ if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString();
5847
+ return "";
5848
+ };
5849
+ const rawNode = (node) => node;
5850
+ const appendLeafNodeText = (node, current, chunks) => {
5851
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "code") {
5852
+ if (typeof current.value === "string") chunks.push(current.value);
5853
+ return true;
5854
+ }
5855
+ if (node.type === "image" || node.type === "imageReference") {
5856
+ if (typeof current.alt === "string") chunks.push(current.alt);
5857
+ return true;
5858
+ }
5859
+ if (node.type === "break" || node.type === "thematicBreak") {
5860
+ chunks.push("\n");
5861
+ return true;
5862
+ }
5863
+ if (node.type === "html" && typeof current.value === "string") {
5864
+ chunks.push(current.value.replaceAll(/<script\b[^>]*>[\s\S]*?<\/script\s*>/giu, " ").replaceAll(/<style\b[^>]*>[\s\S]*?<\/style\s*>/giu, " ").replaceAll(/<!--[\s\S]*?-->/gu, " ").replaceAll(/<[^>]*>/gu, " "));
5865
+ return true;
5866
+ }
5867
+ return false;
5868
+ };
5869
+ const appendMdxAttributes = (current, chunks) => {
5870
+ for (const rawAttribute of current.attributes ?? []) {
5871
+ if (!isRecord(rawAttribute)) continue;
5872
+ const { name, value } = rawAttribute;
5873
+ if (typeof name === "string" && searchableMdxAttributes.has(name.toLowerCase()) && typeof value === "string") chunks.push(value);
5874
+ }
5875
+ };
5876
+ const appendNodeText = (node, chunks) => {
5877
+ if (ignoredNodeTypes.has(node.type)) return;
5878
+ const current = rawNode(node);
5879
+ if (appendLeafNodeText(node, current, chunks)) return;
5880
+ if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") appendMdxAttributes(current, chunks);
5881
+ const isBlock = blockNodeTypes.has(node.type);
5882
+ if (isBlock) chunks.push("\n");
5883
+ for (const child of current.children ?? []) appendNodeText(child, chunks);
5884
+ if (isBlock) chunks.push("\n");
5885
+ };
5886
+ const plainText = (node) => {
5887
+ const chunks = [];
5888
+ appendNodeText(node, chunks);
5889
+ return chunks.join("").replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll(/[ \t]+\n/gu, "\n").replaceAll(/\n{3,}/gu, "\n\n").trim();
5890
+ };
5891
+ const firstHeading = (tree) => {
5892
+ let heading = "";
5893
+ visit(tree, "heading", (node) => {
5894
+ if (heading === "") heading = plainText(node);
5895
+ });
5896
+ return heading;
5897
+ };
5898
+ const withoutFrontmatter = (source) => {
5899
+ const lines = source.replace(/^\uFEFF/u, "").split(/\r?\n/u);
5900
+ if (lines[0]?.trim() !== "---") return source;
5901
+ const closing = lines.findIndex((line, index) => index > 0 && (line.trim() === "---" || line.trim() === "..."));
5902
+ return closing === -1 ? source : lines.slice(closing + 1).join("\n");
5903
+ };
5904
+ const parseLibraryDocument = (source, relativePath) => {
5905
+ let metadata = {};
5906
+ let parseWarning = false;
5907
+ try {
5908
+ const file = new VFile({ value: source });
5909
+ matter(file);
5910
+ metadata = isRecord(file.data.matter) ? file.data.matter : {};
5911
+ } catch {
5912
+ parseWarning = true;
5913
+ }
5914
+ let tree;
5915
+ try {
5916
+ tree = (path.extname(relativePath).toLowerCase() === ".mdx" ? mdxParser : markdownParser).parse(source);
5917
+ } catch {
5918
+ parseWarning = true;
5919
+ tree = markdownParser.parse(withoutFrontmatter(source));
5920
+ }
5921
+ const title = scalarText(metadata.title) || firstHeading(tree) || path.basename(relativePath, path.extname(relativePath));
5922
+ return {
5923
+ body: plainText(tree),
5924
+ parseWarning,
5925
+ status: scalarText(metadata.status),
5926
+ title
5927
+ };
5928
+ };
5929
+ //#endregion
5930
+ //#region src/library-files.ts
5931
+ const MAX_STABLE_READ_ATTEMPTS = 3;
5932
+ const READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
5933
+ const isWithinRoot = (root, candidate) => {
5934
+ const relative = path.relative(root, candidate);
5935
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
5936
+ };
5937
+ const filePathForId = (root, id) => {
5938
+ if (id === "" || id.includes("\\") || id.includes("\0") || path.posix.isAbsolute(id) || path.win32.parse(id).root !== "") return;
5939
+ const parts = id.split("/");
5940
+ if (parts.some((part) => part === "" || part === "." || part === "..")) return;
5941
+ const filePath = path.resolve(root, ...parts);
5942
+ return isWithinRoot(root, filePath) && filePath !== root ? filePath : void 0;
5943
+ };
5944
+ const isMissingPathError = (error) => isRecord(error) && error.code === "ENOENT";
5945
+ const signatureOf = (stats) => [
5946
+ stats.dev,
5947
+ stats.ino,
5948
+ stats.mode,
5949
+ stats.size,
5950
+ stats.mtimeNs,
5951
+ stats.ctimeNs
5952
+ ].map(String).join(":");
5953
+ var UnsafeLibraryPathError = class extends Error {
5954
+ constructor() {
5955
+ super("Path is not a regular file inside the library root");
5956
+ this.name = "UnsafeLibraryPathError";
5957
+ }
5958
+ };
5959
+ const checkedFileStat = async (root, filePath) => {
5960
+ if (!isWithinRoot(root, filePath) || filePath === root) throw new UnsafeLibraryPathError();
5961
+ const parts = path.relative(root, filePath).split(path.sep).filter(Boolean);
5962
+ let current = root;
5963
+ let currentStats;
5964
+ for (const [index, part] of parts.entries()) {
5965
+ current = path.join(current, part);
5966
+ currentStats = await lstat(current, { bigint: true });
5967
+ const final = index === parts.length - 1;
5968
+ if (currentStats.isSymbolicLink() || (final ? !currentStats.isFile() : !currentStats.isDirectory())) throw new UnsafeLibraryPathError();
5969
+ }
5970
+ const actualPath = await realpath(filePath);
5971
+ if (!isWithinRoot(root, actualPath)) throw new UnsafeLibraryPathError();
5972
+ if (currentStats === void 0) throw new UnsafeLibraryPathError();
5973
+ return currentStats;
5974
+ };
5975
+ const readFileAttempt = async (root, filePath) => {
5976
+ let handle;
5977
+ try {
5978
+ await checkedFileStat(root, filePath);
5979
+ handle = await open(filePath, READ_FLAGS);
5980
+ const opened = await handle.stat({ bigint: true });
5981
+ if (!opened.isFile()) throw new UnsafeLibraryPathError();
5982
+ const source = await handle.readFile({ encoding: "utf-8" });
5983
+ const afterRead = await handle.stat({ bigint: true });
5984
+ await handle.close();
5985
+ handle = void 0;
5986
+ const current = await checkedFileStat(root, filePath);
5987
+ const afterReadSignature = signatureOf(afterRead);
5988
+ const currentSignature = signatureOf(current);
5989
+ if (signatureOf(opened) !== afterReadSignature || afterReadSignature !== currentSignature) return;
5990
+ return {
5991
+ signature: currentSignature,
5992
+ source,
5993
+ stats: current
5994
+ };
5995
+ } finally {
5996
+ await handle?.close();
5997
+ }
5998
+ };
5999
+ const readStableFile$1 = async (root, filePath) => {
6000
+ for (let attempt = 0; attempt < MAX_STABLE_READ_ATTEMPTS; attempt += 1) {
6001
+ const result = await readFileAttempt(root, filePath);
6002
+ if (result !== void 0) return result;
6003
+ }
6004
+ throw new Error("File changed while it was being indexed");
6005
+ };
6006
+ const assertRootDirectory = async (root) => {
6007
+ const requestedRoot = path.resolve(root);
6008
+ let rootStats;
6009
+ try {
6010
+ rootStats = await stat(requestedRoot, { bigint: true });
6011
+ } catch {
6012
+ throw new Error(`Document library root does not exist: ${requestedRoot}`);
6013
+ }
6014
+ if (!rootStats.isDirectory()) throw new Error(`Document library root is not a directory: ${requestedRoot}`);
6015
+ return await realpath(requestedRoot);
6016
+ };
6017
+ //#endregion
5800
6018
  //#region src/library-search.ts
5801
6019
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
5802
6020
  const titleCollator = new Intl.Collator("ja", {
@@ -5999,203 +6217,10 @@ const markdownExtensions = /* @__PURE__ */ new Set([
5999
6217
  ".markdown",
6000
6218
  ".mdx"
6001
6219
  ]);
6002
- const ignoredNodeTypes = /* @__PURE__ */ new Set([
6003
- "definition",
6004
- "mdxFlowExpression",
6005
- "mdxjsEsm",
6006
- "mdxTextExpression",
6007
- "toml",
6008
- "yaml"
6009
- ]);
6010
- const searchableMdxAttributes = /* @__PURE__ */ new Set([
6011
- "alt",
6012
- "aria-label",
6013
- "caption",
6014
- "description",
6015
- "heading",
6016
- "label",
6017
- "name",
6018
- "placeholder",
6019
- "summary",
6020
- "text",
6021
- "title",
6022
- "tooltip",
6023
- "value"
6024
- ]);
6025
- const blockNodeTypes = /* @__PURE__ */ new Set([
6026
- "blockquote",
6027
- "code",
6028
- "heading",
6029
- "listItem",
6030
- "paragraph",
6031
- "tableCell",
6032
- "tableRow",
6033
- "thematicBreak"
6034
- ]);
6035
- const mdxParser = unified().use(remarkParse).use(remarkMdx).use(remarkFrontmatter).use(remarkGfm);
6036
- const markdownParser = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm);
6037
- const scalarText = (value) => {
6038
- if (typeof value === "string" || typeof value === "number") return String(value).trim();
6039
- if (typeof value === "boolean" || typeof value === "bigint") return String(value);
6040
- if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString();
6041
- return "";
6042
- };
6043
- const rawNode = (node) => node;
6044
- const appendLeafNodeText = (node, current, chunks) => {
6045
- if (node.type === "text" || node.type === "inlineCode" || node.type === "code") {
6046
- if (typeof current.value === "string") chunks.push(current.value);
6047
- return true;
6048
- }
6049
- if (node.type === "image" || node.type === "imageReference") {
6050
- if (typeof current.alt === "string") chunks.push(current.alt);
6051
- return true;
6052
- }
6053
- if (node.type === "break" || node.type === "thematicBreak") {
6054
- chunks.push("\n");
6055
- return true;
6056
- }
6057
- if (node.type === "html" && typeof current.value === "string") {
6058
- chunks.push(current.value.replaceAll(/<script\b[^>]*>[\s\S]*?<\/script\s*>/giu, " ").replaceAll(/<style\b[^>]*>[\s\S]*?<\/style\s*>/giu, " ").replaceAll(/<!--[\s\S]*?-->/gu, " ").replaceAll(/<[^>]*>/gu, " "));
6059
- return true;
6060
- }
6061
- return false;
6062
- };
6063
- const appendMdxAttributes = (current, chunks) => {
6064
- for (const rawAttribute of current.attributes ?? []) {
6065
- if (!isRecord(rawAttribute)) continue;
6066
- const { name, value } = rawAttribute;
6067
- if (typeof name === "string" && searchableMdxAttributes.has(name.toLowerCase()) && typeof value === "string") chunks.push(value);
6068
- }
6069
- };
6070
- const appendNodeText = (node, chunks) => {
6071
- if (ignoredNodeTypes.has(node.type)) return;
6072
- const current = rawNode(node);
6073
- if (appendLeafNodeText(node, current, chunks)) return;
6074
- if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") appendMdxAttributes(current, chunks);
6075
- const isBlock = blockNodeTypes.has(node.type);
6076
- if (isBlock) chunks.push("\n");
6077
- for (const child of current.children ?? []) appendNodeText(child, chunks);
6078
- if (isBlock) chunks.push("\n");
6079
- };
6080
- const plainText = (node) => {
6081
- const chunks = [];
6082
- appendNodeText(node, chunks);
6083
- return chunks.join("").replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll(/[ \t]+\n/gu, "\n").replaceAll(/\n{3,}/gu, "\n\n").trim();
6084
- };
6085
- const firstHeading = (tree) => {
6086
- let heading = "";
6087
- visit(tree, "heading", (node) => {
6088
- if (heading === "") heading = plainText(node);
6089
- });
6090
- return heading;
6091
- };
6092
- const withoutFrontmatter = (source) => {
6093
- const lines = source.replace(/^\uFEFF/u, "").split(/\r?\n/u);
6094
- if (lines[0]?.trim() !== "---") return source;
6095
- const closing = lines.findIndex((line, index) => index > 0 && (line.trim() === "---" || line.trim() === "..."));
6096
- return closing === -1 ? source : lines.slice(closing + 1).join("\n");
6097
- };
6098
- const parseDocument = (source, relativePath) => {
6099
- let metadata = {};
6100
- let parseWarning = false;
6101
- try {
6102
- const file = new VFile({ value: source });
6103
- matter(file);
6104
- metadata = isRecord(file.data.matter) ? file.data.matter : {};
6105
- } catch {
6106
- parseWarning = true;
6107
- }
6108
- let tree;
6109
- try {
6110
- tree = (path.extname(relativePath).toLowerCase() === ".mdx" ? mdxParser : markdownParser).parse(source);
6111
- } catch {
6112
- parseWarning = true;
6113
- tree = markdownParser.parse(withoutFrontmatter(source));
6114
- }
6115
- const title = scalarText(metadata.title) || firstHeading(tree) || path.basename(relativePath, path.extname(relativePath));
6116
- return {
6117
- body: plainText(tree),
6118
- parseWarning,
6119
- status: scalarText(metadata.status),
6120
- title
6121
- };
6122
- };
6123
- const isWithinRoot = (root, candidate) => {
6124
- const relative = path.relative(root, candidate);
6125
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
6126
- };
6127
- const filePathForId = (root, id) => {
6128
- if (id === "" || id.includes("\\") || id.includes("\0") || path.posix.isAbsolute(id) || path.win32.parse(id).root !== "") return;
6129
- const parts = id.split("/");
6130
- if (parts.some((part) => part === "" || part === "." || part === "..")) return;
6131
- const filePath = path.resolve(root, ...parts);
6132
- return isWithinRoot(root, filePath) && filePath !== root ? filePath : void 0;
6133
- };
6134
- const isMissingPathError = (error) => isRecord(error) && error.code === "ENOENT";
6135
- const signatureOf = (stats) => [
6136
- stats.dev,
6137
- stats.ino,
6138
- stats.mode,
6139
- stats.size,
6140
- stats.mtimeNs,
6141
- stats.ctimeNs
6142
- ].map(String).join(":");
6143
6220
  const warningFor = (relativePath, reason) => ({
6144
6221
  path: relativePath || ".",
6145
6222
  reason
6146
6223
  });
6147
- var UnsafeLibraryPathError = class extends Error {
6148
- constructor() {
6149
- super("Path is not a regular file inside the library root");
6150
- this.name = "UnsafeLibraryPathError";
6151
- }
6152
- };
6153
- const unsafePathError = () => new UnsafeLibraryPathError();
6154
- const checkedFileStat = async (root, filePath) => {
6155
- if (!isWithinRoot(root, filePath) || filePath === root) throw unsafePathError();
6156
- const parts = path.relative(root, filePath).split(path.sep).filter(Boolean);
6157
- let current = root;
6158
- let currentStats;
6159
- for (const [index, part] of parts.entries()) {
6160
- current = path.join(current, part);
6161
- currentStats = await lstat(current, { bigint: true });
6162
- const final = index === parts.length - 1;
6163
- if (currentStats.isSymbolicLink() || (final ? !currentStats.isFile() : !currentStats.isDirectory())) throw unsafePathError();
6164
- }
6165
- const actualPath = await realpath(filePath);
6166
- if (!isWithinRoot(root, actualPath)) throw unsafePathError();
6167
- if (currentStats === void 0) throw unsafePathError();
6168
- return currentStats;
6169
- };
6170
- const readStableFile$1 = async (root, filePath) => {
6171
- const flags = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
6172
- const readAttempt = async (attempt) => {
6173
- let handle;
6174
- let stableRead;
6175
- try {
6176
- await checkedFileStat(root, filePath);
6177
- handle = await open(filePath, flags);
6178
- const opened = await handle.stat({ bigint: true });
6179
- if (!opened.isFile()) throw unsafePathError();
6180
- const source = await handle.readFile({ encoding: "utf-8" });
6181
- const afterRead = await handle.stat({ bigint: true });
6182
- await handle.close();
6183
- handle = void 0;
6184
- const current = await checkedFileStat(root, filePath);
6185
- if (signatureOf(opened) === signatureOf(afterRead) && signatureOf(afterRead) === signatureOf(current)) stableRead = {
6186
- signature: signatureOf(current),
6187
- source,
6188
- stats: current
6189
- };
6190
- } finally {
6191
- await handle?.close();
6192
- }
6193
- if (stableRead !== void 0) return stableRead;
6194
- if (attempt >= 2) throw new Error("File changed while it was being indexed");
6195
- return await readAttempt(attempt + 1);
6196
- };
6197
- return await readAttempt(0);
6198
- };
6199
6224
  const compareWarnings = (left, right) => {
6200
6225
  if (left.path < right.path) return -1;
6201
6226
  if (left.path > right.path) return 1;
@@ -6204,17 +6229,6 @@ const compareWarnings = (left, right) => {
6204
6229
  return 0;
6205
6230
  };
6206
6231
  const statusList = (documents) => [...new Set(documents.map(({ document }) => document.status).filter(Boolean))].toSorted();
6207
- const assertRootDirectory = async (root) => {
6208
- const requestedRoot = path.resolve(root);
6209
- let rootStats;
6210
- try {
6211
- rootStats = await stat(requestedRoot, { bigint: true });
6212
- } catch {
6213
- throw new Error(`Document library root does not exist: ${requestedRoot}`);
6214
- }
6215
- if (!rootStats.isDirectory()) throw new Error(`Document library root is not a directory: ${requestedRoot}`);
6216
- return await realpath(requestedRoot);
6217
- };
6218
6232
  /** Index local Markdown documents without compiling or executing MDX. */
6219
6233
  const createLibraryIndex = async (root) => {
6220
6234
  const absoluteRoot = await assertRootDirectory(root);
@@ -6256,7 +6270,7 @@ const createLibraryIndex = async (root) => {
6256
6270
  return;
6257
6271
  }
6258
6272
  const read = await readStableFile$1(absoluteRoot, filePath);
6259
- const parsed = parseDocument(read.source, id);
6273
+ const parsed = parseLibraryDocument(read.source, id);
6260
6274
  const warning = parsed.parseWarning ? warningFor(id, "parse") : void 0;
6261
6275
  const entry = {
6262
6276
  document: {
@@ -6326,16 +6340,7 @@ const createLibraryIndex = async (root) => {
6326
6340
  if (filePath === void 0) return { status: "invalid" };
6327
6341
  try {
6328
6342
  await checkedFileStat(absoluteRoot, filePath);
6329
- } catch (error) {
6330
- if (error instanceof UnsafeLibraryPathError) return { status: "invalid" };
6331
- if (isMissingPathError(error)) {
6332
- indexed.delete(id);
6333
- return { status: "not-found" };
6334
- }
6335
- throw error;
6336
- }
6337
- if (!indexed.has(id)) return { status: "not-found" };
6338
- try {
6343
+ if (!indexed.has(id)) return { status: "not-found" };
6339
6344
  await unlink(filePath);
6340
6345
  } catch (error) {
6341
6346
  if (error instanceof UnsafeLibraryPathError) return { status: "invalid" };
@@ -6353,15 +6358,13 @@ const createLibraryIndex = async (root) => {
6353
6358
  }),
6354
6359
  resolve: async (id) => await withOperationLock(async () => {
6355
6360
  if (!hasScanned) await scan();
6356
- const filePath = indexed.has(id) ? filePathForId(absoluteRoot, id) : void 0;
6357
- let isSafePath = false;
6361
+ let filePath = indexed.has(id) ? filePathForId(absoluteRoot, id) : void 0;
6358
6362
  if (filePath !== void 0) try {
6359
6363
  await checkedFileStat(absoluteRoot, filePath);
6360
- isSafePath = true;
6361
6364
  } catch {
6362
- isSafePath = false;
6365
+ filePath = void 0;
6363
6366
  }
6364
- return isSafePath ? filePath : void 0;
6367
+ return filePath;
6365
6368
  }),
6366
6369
  search: async (options = {}) => {
6367
6370
  const refreshed = await refresh();
@@ -7270,6 +7273,26 @@ const libraryHtml = async () => {
7270
7273
  </html>`;
7271
7274
  };
7272
7275
  //#endregion
7276
+ //#region src/local-http.ts
7277
+ /** Match the loopback server's Host and, when present, Origin headers. */
7278
+ const isLocalOrigin = (request) => {
7279
+ const port = request.socket.localPort;
7280
+ const host = request.headers.host ?? "";
7281
+ return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
7282
+ };
7283
+ const replyText = (response, status, body, contentType, headers = {}) => {
7284
+ response.writeHead(status, {
7285
+ "cache-control": "no-store",
7286
+ "content-type": contentType,
7287
+ "x-content-type-options": "nosniff",
7288
+ ...headers
7289
+ });
7290
+ response.end(body);
7291
+ };
7292
+ const replyJson = (response, status, body, headers = {}) => {
7293
+ replyText(response, status, JSON.stringify(body), "application/json; charset=utf-8", headers);
7294
+ };
7295
+ //#endregion
7273
7296
  //#region src/open.ts
7274
7297
  /**
7275
7298
  * The platform's "open in the default browser" invocation for a file path
@@ -7383,20 +7406,6 @@ const view = (conversation) => ({
7383
7406
  })),
7384
7407
  sessionId: conversation.sessionId ?? null
7385
7408
  });
7386
- const reply$1 = (res, status, body, headers = {}) => {
7387
- res.writeHead(status, {
7388
- "cache-control": "no-store",
7389
- "content-type": "application/json; charset=utf-8",
7390
- "x-content-type-options": "nosniff",
7391
- ...headers
7392
- });
7393
- res.end(JSON.stringify(body));
7394
- };
7395
- const sameOrigin$3 = (req) => {
7396
- const port = req.socket.localPort;
7397
- const host = req.headers.host ?? "";
7398
- return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (req.headers.origin === void 0 || req.headers.origin === `http://${host}`);
7399
- };
7400
7409
  const readMessage = async (req) => {
7401
7410
  if (req.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") throw new Error("Expected application/json");
7402
7411
  let source = "";
@@ -7425,31 +7434,31 @@ const handleAgentRequest = async (req, res, agent, notify, beforeSend) => {
7425
7434
  let started = false;
7426
7435
  try {
7427
7436
  if (agent === void 0) {
7428
- reply$1(res, 404, { error: "Agent chat is disabled" });
7437
+ replyJson(res, 404, { error: "Agent chat is disabled" });
7429
7438
  return;
7430
7439
  }
7431
- if (!sameOrigin$3(req)) {
7432
- reply$1(res, 403, { error: "Request origin is not allowed" });
7440
+ if (!isLocalOrigin(req)) {
7441
+ replyJson(res, 403, { error: "Request origin is not allowed" });
7433
7442
  return;
7434
7443
  }
7435
7444
  if (req.method === "GET") {
7436
- reply$1(res, 200, view(await agent.current()));
7445
+ replyJson(res, 200, view(await agent.current()));
7437
7446
  return;
7438
7447
  }
7439
7448
  if (req.method !== "POST") {
7440
- reply$1(res, 405, { error: "Method not allowed" }, { allow: "GET, POST" });
7449
+ replyJson(res, 405, { error: "Method not allowed" }, { allow: "GET, POST" });
7441
7450
  return;
7442
7451
  }
7443
7452
  const message = await readMessage(req);
7444
7453
  await beforeSend?.();
7445
7454
  started = true;
7446
- reply$1(res, 200, view(await agent.send(message)));
7455
+ replyJson(res, 200, view(await agent.send(message)));
7447
7456
  notify();
7448
7457
  } catch (error) {
7449
7458
  if (started) notify();
7450
7459
  if (res.headersSent) return;
7451
7460
  const detail = formatError(error);
7452
- reply$1(res, errorStatus(detail), { error: detail });
7461
+ replyJson(res, errorStatus(detail), { error: detail });
7453
7462
  }
7454
7463
  };
7455
7464
  //#endregion
@@ -10219,20 +10228,6 @@ const parsePreviewTheme = (value) => {
10219
10228
  if (value === null) return;
10220
10229
  return value === "light" || value === "dark" ? value : null;
10221
10230
  };
10222
- const replyJson$2 = (response, status, body, headers = {}) => {
10223
- response.writeHead(status, {
10224
- "cache-control": "no-store",
10225
- "content-type": "application/json; charset=utf-8",
10226
- "x-content-type-options": "nosniff",
10227
- ...headers
10228
- });
10229
- response.end(JSON.stringify(body));
10230
- };
10231
- const sameOrigin$2 = (request) => {
10232
- const port = request.socket.localPort;
10233
- const host = request.headers.host ?? "";
10234
- return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
10235
- };
10236
10231
  /** Reconstruct both sides from the same diff snapshot before highlighting. */
10237
10232
  const replyDiff = async (response, history, from, to) => {
10238
10233
  const { lines } = await history.diff(from, to);
@@ -10243,7 +10238,7 @@ const replyDiff = async (response, history, from, to) => {
10243
10238
  if (line.type !== "remove") afterSource.push(line.text);
10244
10239
  }
10245
10240
  const [before, after] = await Promise.all([highlightMdxSource(beforeSource.join("\n")), highlightMdxSource(afterSource.join("\n"))]);
10246
- replyJson$2(response, 200, {
10241
+ replyJson(response, 200, {
10247
10242
  from,
10248
10243
  lines,
10249
10244
  syntax: {
@@ -10256,7 +10251,7 @@ const replyDiff = async (response, history, from, to) => {
10256
10251
  const replyPreview = async (response, history, filePath, id, requestedTheme) => {
10257
10252
  const theme = parsePreviewTheme(requestedTheme);
10258
10253
  if (theme === null) {
10259
- replyJson$2(response, 400, { error: "Invalid preview theme" });
10254
+ replyJson(response, 400, { error: "Invalid preview theme" });
10260
10255
  return;
10261
10256
  }
10262
10257
  const source = await history.read(id);
@@ -10266,24 +10261,19 @@ const replyPreview = async (response, history, filePath, id, requestedTheme) =>
10266
10261
  initialTheme: theme ?? void 0,
10267
10262
  liveReload: false
10268
10263
  });
10269
- response.writeHead(200, {
10270
- "cache-control": "no-store",
10271
- "content-type": "text/html; charset=utf-8",
10272
- "x-content-type-options": "nosniff"
10273
- });
10274
- response.end(html);
10264
+ replyText(response, 200, html, "text/html; charset=utf-8");
10275
10265
  };
10276
10266
  const replyHistoryView = async (response, history, filePath, params) => {
10277
10267
  const view = params.get("view");
10278
10268
  if (view === null) {
10279
- replyJson$2(response, 200, await history.list());
10269
+ replyJson(response, 200, await history.list());
10280
10270
  return;
10281
10271
  }
10282
10272
  const id = params.get("id");
10283
10273
  if (view === "source" && id !== null) {
10284
10274
  const source = await history.read(id);
10285
10275
  const syntax = await highlightMdxSource(source);
10286
- replyJson$2(response, 200, {
10276
+ replyJson(response, 200, {
10287
10277
  id,
10288
10278
  source,
10289
10279
  syntax
@@ -10302,20 +10292,20 @@ const replyHistoryView = async (response, history, filePath, params) => {
10302
10292
  await replyPreview(response, history, filePath, id, params.get("theme"));
10303
10293
  return;
10304
10294
  }
10305
- replyJson$2(response, 400, { error: "Invalid history view or version" });
10295
+ replyJson(response, 400, { error: "Invalid history view or version" });
10306
10296
  };
10307
10297
  /** Read-only endpoints for the local, token-free MDX version store. */
10308
10298
  const handleDocumentHistoryRequest = async (request, response, history, filePath) => {
10309
10299
  if (history === void 0 || filePath === void 0) {
10310
- replyJson$2(response, 404, { error: "Document history is unavailable" });
10300
+ replyJson(response, 404, { error: "Document history is unavailable" });
10311
10301
  return;
10312
10302
  }
10313
- if (!sameOrigin$2(request)) {
10314
- replyJson$2(response, 403, { error: "Request origin is not allowed" });
10303
+ if (!isLocalOrigin(request)) {
10304
+ replyJson(response, 403, { error: "Request origin is not allowed" });
10315
10305
  return;
10316
10306
  }
10317
10307
  if (request.method !== "GET") {
10318
- replyJson$2(response, 405, { error: "Method not allowed" }, { allow: "GET" });
10308
+ replyJson(response, 405, { error: "Method not allowed" }, { allow: "GET" });
10319
10309
  return;
10320
10310
  }
10321
10311
  const url = new URL(request.url ?? "", "http://localhost");
@@ -10324,7 +10314,7 @@ const handleDocumentHistoryRequest = async (request, response, history, filePath
10324
10314
  } catch (error) {
10325
10315
  const detail = formatError(error);
10326
10316
  const missingVersion = error instanceof DocumentHistoryError && (error.kind === "invalid-version" || error.kind === "unknown-version");
10327
- replyJson$2(response, missingVersion ? 404 : 500, { error: detail });
10317
+ replyJson(response, missingVersion ? 404 : 500, { error: detail });
10328
10318
  }
10329
10319
  };
10330
10320
  //#endregion
@@ -10345,20 +10335,6 @@ var FilePreviewError = class extends Error {
10345
10335
  this.status = status;
10346
10336
  }
10347
10337
  };
10348
- const replyJson$1 = (response, status, body) => {
10349
- response.writeHead(status, {
10350
- "cache-control": "no-store",
10351
- "content-type": "application/json; charset=utf-8",
10352
- "x-content-type-options": "nosniff",
10353
- ...status === 405 ? { allow: "GET" } : {}
10354
- });
10355
- response.end(JSON.stringify(body));
10356
- };
10357
- const sameOrigin$1 = (request) => {
10358
- const port = request.socket.localPort;
10359
- const host = request.headers.host ?? "";
10360
- return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`) && request.headers["sec-fetch-site"] !== "cross-site";
10361
- };
10362
10338
  const readPreviewFile = async (file, limit) => {
10363
10339
  if (await realpath(file.path) !== file.canonical) throw new FilePreviewError(403, "The referenced file has changed location");
10364
10340
  const handle = await open(file.canonical, "r");
@@ -10421,11 +10397,11 @@ const previewData = async (file, theme) => {
10421
10397
  };
10422
10398
  const replyError = (response, error) => {
10423
10399
  if (isRecord(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
10424
- replyJson$1(response, 404, { error: "The referenced file no longer exists" });
10400
+ replyJson(response, 404, { error: "The referenced file no longer exists" });
10425
10401
  return;
10426
10402
  }
10427
10403
  const status = error instanceof FilePreviewError ? error.status : 500;
10428
- replyJson$1(response, status, { error: formatError(error) });
10404
+ replyJson(response, status, { error: formatError(error) });
10429
10405
  };
10430
10406
  /** Per-server capabilities, issued only for files a rendered FileRef uses. */
10431
10407
  const createFilePreviews = () => {
@@ -10433,22 +10409,22 @@ const createFilePreviews = () => {
10433
10409
  const files = /* @__PURE__ */ new Map();
10434
10410
  return {
10435
10411
  async handle(request, response) {
10436
- if (!sameOrigin$1(request)) {
10437
- replyJson$1(response, 403, { error: "Request origin is not allowed" });
10412
+ if (!isLocalOrigin(request) || request.headers["sec-fetch-site"] === "cross-site") {
10413
+ replyJson(response, 403, { error: "Request origin is not allowed" });
10438
10414
  return;
10439
10415
  }
10440
10416
  if (request.method !== "GET") {
10441
- replyJson$1(response, 405, { error: "Method not allowed" });
10417
+ replyJson(response, 405, { error: "Method not allowed" }, { allow: "GET" });
10442
10418
  return;
10443
10419
  }
10444
10420
  const params = new URL(request.url ?? "", "http://localhost").searchParams;
10445
10421
  const file = files.get(params.get("id") ?? "");
10446
10422
  if (file === void 0) {
10447
- replyJson$1(response, 404, { error: "The referenced file is unavailable" });
10423
+ replyJson(response, 404, { error: "The referenced file is unavailable" });
10448
10424
  return;
10449
10425
  }
10450
10426
  try {
10451
- replyJson$1(response, 200, await previewData(file, params.get("theme") === "dark" ? "dark" : "light"));
10427
+ replyJson(response, 200, await previewData(file, params.get("theme") === "dark" ? "dark" : "light"));
10452
10428
  } catch (error) {
10453
10429
  replyError(response, error);
10454
10430
  }
@@ -10475,6 +10451,125 @@ const createFilePreviews = () => {
10475
10451
  };
10476
10452
  };
10477
10453
  //#endregion
10454
+ //#region src/preview-watch.ts
10455
+ /** Dependency/build output dirs — never worth a watch fd. */
10456
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
10457
+ ".git",
10458
+ ".mdxr-cache",
10459
+ "dist",
10460
+ "node_modules",
10461
+ "storybook-static"
10462
+ ]);
10463
+ /**
10464
+ * The set of directories being watched. `fs.watch` recursive mode exists only
10465
+ * on darwin/win32 — elsewhere `arm` puts one non-recursive watcher per
10466
+ * directory, which also survives atomic saves (rename-over kills a watch
10467
+ * aimed at the file itself).
10468
+ */
10469
+ const createWatchSet = () => {
10470
+ const watchers = [];
10471
+ const armed = /* @__PURE__ */ new Set();
10472
+ const track = (w, dir) => {
10473
+ watchers.push(w);
10474
+ if (dir !== void 0) {
10475
+ armed.add(dir);
10476
+ w.on("close", () => {
10477
+ armed.delete(dir);
10478
+ });
10479
+ }
10480
+ w.on("error", () => {
10481
+ w.close();
10482
+ });
10483
+ };
10484
+ /** One watcher on `dir` itself (no descent). */
10485
+ const armFlat = (dir, onEvent) => {
10486
+ if (armed.has(dir) || SKIP_DIRS.has(path.basename(dir))) return;
10487
+ try {
10488
+ track(fs.watch(dir, onEvent), dir);
10489
+ } catch {}
10490
+ };
10491
+ /**
10492
+ * Recursive fallback: a watcher on `dir` plus every directory under it.
10493
+ * The `armed` check guards only the watcher install — the descent still
10494
+ * runs on an already-armed root, so directories created mid-session get
10495
+ * picked up on the next rebuild (armDependencies re-arms the tree).
10496
+ */
10497
+ const arm = (dir, onEvent) => {
10498
+ if (SKIP_DIRS.has(path.basename(dir)) || path.basename(dir) === "history" && path.basename(path.dirname(dir)) === ".mdxr") return;
10499
+ if (!armed.has(dir)) try {
10500
+ track(fs.watch(dir, onEvent), dir);
10501
+ } catch {}
10502
+ let ents;
10503
+ try {
10504
+ ents = fs.readdirSync(dir, { withFileTypes: true });
10505
+ } catch {
10506
+ return;
10507
+ }
10508
+ for (const e of ents) if (e.isDirectory()) arm(path.join(dir, e.name), onEvent);
10509
+ };
10510
+ return {
10511
+ arm,
10512
+ armFlat,
10513
+ close() {
10514
+ for (const w of watchers) w.close();
10515
+ },
10516
+ get size() {
10517
+ return watchers.length;
10518
+ },
10519
+ track
10520
+ };
10521
+ };
10522
+ const watchTarget = (watch, target, notify) => {
10523
+ let recursiveWatch = false;
10524
+ try {
10525
+ watch.track(fs.watch(target.watchDir, { recursive: true }, notify));
10526
+ recursiveWatch = true;
10527
+ } catch {
10528
+ watch.arm(target.watchDir, notify);
10529
+ }
10530
+ if (watch.size === 0 && target.watchFile !== void 0) try {
10531
+ watch.track(fs.watch(target.watchFile, notify));
10532
+ } catch {}
10533
+ return recursiveWatch;
10534
+ };
10535
+ const RELOAD_DEBOUNCE_MS = 80;
10536
+ const SESSION_TEMP_FILE = /^\.sessions-.*\.tmp$/u;
10537
+ const isIgnoredChange = (watchDir, filename) => {
10538
+ const parts = filename?.split(path.sep) ?? [];
10539
+ const isHistoryWrite = parts.includes("history") && (parts.includes(".mdxr") || path.basename(watchDir) === ".mdxr");
10540
+ const isInternalMdxrWrite = path.basename(watchDir) !== ".mdxr" && parts.includes(".mdxr");
10541
+ return parts.some((part) => SKIP_DIRS.has(part)) || isHistoryWrite || isInternalMdxrWrite || parts.some((part) => part === "sessions.json" || SESSION_TEMP_FILE.test(part));
10542
+ };
10543
+ /** Own the preview's filesystem watchers and debounce source change bursts. */
10544
+ const createPreviewWatcher = (target, onChange) => {
10545
+ const watch = createWatchSet();
10546
+ let closed = false;
10547
+ let timer;
10548
+ const notify = (_event, filename) => {
10549
+ if (closed || isIgnoredChange(target.watchDir, filename)) return;
10550
+ clearTimeout(timer);
10551
+ timer = setTimeout(onChange, RELOAD_DEBOUNCE_MS);
10552
+ };
10553
+ const recursiveWatch = watchTarget(watch, target, notify);
10554
+ return {
10555
+ /** Watch external dependencies and newly created source directories. */
10556
+ armDependencies(dependencies) {
10557
+ if (closed) return;
10558
+ if (!recursiveWatch) watch.arm(target.watchDir, notify);
10559
+ for (const dependency of dependencies) {
10560
+ const directory = path.dirname(dependency);
10561
+ if (directory === target.watchDir || directory.startsWith(`${target.watchDir}${path.sep}`) || directory.split(path.sep).includes("node_modules")) continue;
10562
+ watch.armFlat(directory, notify);
10563
+ }
10564
+ },
10565
+ close() {
10566
+ closed = true;
10567
+ clearTimeout(timer);
10568
+ watch.close();
10569
+ }
10570
+ };
10571
+ };
10572
+ //#endregion
10478
10573
  //#region src/workspace-export-data.ts
10479
10574
  /** Collect a complete, internally consistent archive of one served document. */
10480
10575
  const createWorkspaceExportData = async (filePath, history, agent) => {
@@ -10499,26 +10594,13 @@ const createWorkspaceExportData = async (filePath, history, agent) => {
10499
10594
  versions: exportedVersions
10500
10595
  };
10501
10596
  };
10502
- const replyJson = (response, status, body) => {
10503
- response.writeHead(status, {
10504
- "cache-control": "no-store",
10505
- "content-type": "application/json; charset=utf-8",
10506
- "x-content-type-options": "nosniff"
10507
- });
10508
- response.end(JSON.stringify(body));
10509
- };
10510
- const sameOrigin = (request) => {
10511
- const port = request.socket.localPort;
10512
- const host = request.headers.host ?? "";
10513
- return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
10514
- };
10515
10597
  /** Serve a complete archive to the local workspace export action. */
10516
10598
  const handleWorkspaceExportRequest = async (request, response, filePath, history, agent) => {
10517
10599
  if (history === void 0 || filePath === void 0) {
10518
10600
  replyJson(response, 404, { error: "Document history is unavailable" });
10519
10601
  return;
10520
10602
  }
10521
- if (!sameOrigin(request)) {
10603
+ if (!isLocalOrigin(request)) {
10522
10604
  replyJson(response, 403, { error: "Request origin is not allowed" });
10523
10605
  return;
10524
10606
  }
@@ -10564,14 +10646,6 @@ const workspaceJs = async () => {
10564
10646
  //#endregion
10565
10647
  //#region src/serve.ts
10566
10648
  const errorPage = (err) => `<!doctype html><meta charset="utf-8"><body style="font-family:monospace;background:#1c1917;color:#fca5a5;padding:2rem"><h1>mdxr render error</h1><pre>${formatError(err).replaceAll("&", "&amp;").replaceAll("<", "&lt;")}</pre></body>`;
10567
- /** Dependency/build output dirs — never worth a watch fd. */
10568
- const SKIP_DIRS = /* @__PURE__ */ new Set([
10569
- ".git",
10570
- ".mdxr-cache",
10571
- "dist",
10572
- "node_modules",
10573
- "storybook-static"
10574
- ]);
10575
10649
  /** Upper bound on consecutive EADDRINUSE retries before give-up. */
10576
10650
  const MAX_PORT_ATTEMPTS = 100;
10577
10651
  /**
@@ -10604,78 +10678,6 @@ const handleDocumentResponse = (req, res, target, html) => {
10604
10678
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
10605
10679
  res.end(html);
10606
10680
  };
10607
- /**
10608
- * The set of directories being watched. `fs.watch` recursive mode exists only
10609
- * on darwin/win32 — elsewhere `arm` puts one non-recursive watcher per
10610
- * directory, which also survives atomic saves (rename-over kills a watch
10611
- * aimed at the file itself).
10612
- */
10613
- const createWatchSet = () => {
10614
- const watchers = [];
10615
- const armed = /* @__PURE__ */ new Set();
10616
- const track = (w, dir) => {
10617
- watchers.push(w);
10618
- if (dir !== void 0) {
10619
- armed.add(dir);
10620
- w.on("close", () => {
10621
- armed.delete(dir);
10622
- });
10623
- }
10624
- w.on("error", () => {
10625
- w.close();
10626
- });
10627
- };
10628
- /** One watcher on `dir` itself (no descent). */
10629
- const armFlat = (dir, onEvent) => {
10630
- if (armed.has(dir) || SKIP_DIRS.has(path.basename(dir))) return;
10631
- try {
10632
- track(fs.watch(dir, onEvent), dir);
10633
- } catch {}
10634
- };
10635
- /**
10636
- * Recursive fallback: a watcher on `dir` plus every directory under it.
10637
- * The `armed` check guards only the watcher install — the descent still
10638
- * runs on an already-armed root, so directories created mid-session get
10639
- * picked up on the next rebuild (armDeps re-arms the tree for this).
10640
- */
10641
- const arm = (dir, onEvent) => {
10642
- if (SKIP_DIRS.has(path.basename(dir)) || path.basename(dir) === "history" && path.basename(path.dirname(dir)) === ".mdxr") return;
10643
- if (!armed.has(dir)) try {
10644
- track(fs.watch(dir, onEvent), dir);
10645
- } catch {}
10646
- let ents;
10647
- try {
10648
- ents = fs.readdirSync(dir, { withFileTypes: true });
10649
- } catch {
10650
- return;
10651
- }
10652
- for (const e of ents) if (e.isDirectory()) arm(path.join(dir, e.name), onEvent);
10653
- };
10654
- return {
10655
- arm,
10656
- armFlat,
10657
- close() {
10658
- for (const w of watchers) w.close();
10659
- },
10660
- get size() {
10661
- return watchers.length;
10662
- },
10663
- track
10664
- };
10665
- };
10666
- const watchTarget = (watch, target, notify) => {
10667
- let recursiveWatch = false;
10668
- try {
10669
- watch.track(fs.watch(target.watchDir, { recursive: true }, notify));
10670
- recursiveWatch = true;
10671
- } catch {
10672
- watch.arm(target.watchDir, notify);
10673
- }
10674
- if (watch.size === 0 && target.watchFile !== void 0) try {
10675
- watch.track(fs.watch(target.watchFile, notify));
10676
- } catch {}
10677
- return recursiveWatch;
10678
- };
10679
10681
  const broadcast = (clients, event) => {
10680
10682
  for (const client of clients) try {
10681
10683
  client.write(`event: ${event}\ndata: {}\n\n`);
@@ -10743,25 +10745,7 @@ const servePreview = async (target, port) => {
10743
10745
  }
10744
10746
  handleDocumentResponse(req, res, target, html);
10745
10747
  });
10746
- const watch = createWatchSet();
10747
- let recursiveWatch = false;
10748
- let timer;
10749
- /**
10750
- * Dependency files (theme CSS, its nested imports) may live outside the
10751
- * watched document directory. Inside the watch dir the root watcher covers
10752
- * them; outside, a flat watcher on the file's own directory suffices —
10753
- * and avoids descending into a potentially huge ancestor tree. Also
10754
- * re-arms the tree so directories created mid-session are picked up.
10755
- */
10756
- const armDeps = (onEvent) => {
10757
- if (!recursiveWatch) watch.arm(target.watchDir, onEvent);
10758
- for (const dep of target.deps?.() ?? []) {
10759
- const dir = path.dirname(dep);
10760
- if (dir === target.watchDir || dir.startsWith(`${target.watchDir}${path.sep}`) || dir.split(path.sep).includes("node_modules")) continue;
10761
- watch.armFlat(dir, onEvent);
10762
- }
10763
- };
10764
- const rebuild = async (onEvent) => {
10748
+ const rebuild = async (watcher) => {
10765
10749
  try {
10766
10750
  if (target.history !== void 0) {
10767
10751
  await target.history.capture(capturedInitialVersion ? "change" : "initial");
@@ -10772,31 +10756,23 @@ const servePreview = async (target, port) => {
10772
10756
  } catch (error) {
10773
10757
  html = errorPage(error);
10774
10758
  }
10775
- if (!closed) armDeps(onEvent);
10759
+ if (!closed) watcher.armDependencies(target.deps?.() ?? []);
10776
10760
  };
10777
10761
  let reloading = Promise.resolve();
10778
- const reload = (onEvent) => {
10762
+ const reload = (watcher) => {
10779
10763
  const prev = reloading;
10780
10764
  reloading = (async () => {
10781
10765
  await prev;
10782
10766
  if (closed) return;
10783
10767
  try {
10784
- await rebuild(onEvent);
10768
+ await rebuild(watcher);
10785
10769
  broadcast(clients, "reload");
10786
10770
  } catch {}
10787
10771
  })();
10788
10772
  };
10789
- const notify = (_event, filename) => {
10790
- const parts = filename?.split(path.sep) ?? [];
10791
- const isHistoryWrite = parts.includes("history") && (parts.includes(".mdxr") || path.basename(target.watchDir) === ".mdxr");
10792
- const isInternalMdxrWrite = path.basename(target.watchDir) !== ".mdxr" && parts.includes(".mdxr");
10793
- if (closed || parts.some((part) => SKIP_DIRS.has(part)) || isHistoryWrite || isInternalMdxrWrite || parts.some((part) => part === "sessions.json" || /^\.sessions-.*\.tmp$/u.test(part))) return;
10794
- clearTimeout(timer);
10795
- timer = setTimeout(() => {
10796
- reload(notify);
10797
- }, 80);
10798
- };
10799
- recursiveWatch = watchTarget(watch, target, notify);
10773
+ const watcher = createPreviewWatcher(target, () => {
10774
+ reload(watcher);
10775
+ });
10800
10776
  const closeAgent = async () => {
10801
10777
  try {
10802
10778
  await target.agent?.close();
@@ -10804,11 +10780,10 @@ const servePreview = async (target, port) => {
10804
10780
  };
10805
10781
  server.on("close", () => {
10806
10782
  closed = true;
10807
- clearTimeout(timer);
10808
- watch.close();
10783
+ watcher.close();
10809
10784
  closeAgent();
10810
10785
  });
10811
- reloading = rebuild(notify);
10786
+ reloading = rebuild(watcher);
10812
10787
  await reloading;
10813
10788
  try {
10814
10789
  await listenOnFreePort(server, port);
@@ -10831,7 +10806,7 @@ const openPreview = async (server, port) => {
10831
10806
  /**
10832
10807
  * Wraps a render fn with dependency tracking: `deps` reads the paths the
10833
10808
  * last render reported via `onDependencies` (theme CSS + its imports), so
10834
- * `armDeps` can watch files outside the document's own directory.
10809
+ * the watcher can track files outside the document's own directory.
10835
10810
  */
10836
10811
  const trackDeps = (renderDoc) => {
10837
10812
  let deps = [];
@@ -10962,19 +10937,6 @@ const createPreviews = (opts) => {
10962
10937
  }
10963
10938
  };
10964
10939
  };
10965
- const localRequest = (request) => {
10966
- const port = request.socket.localPort;
10967
- const { host } = request.headers;
10968
- return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
10969
- };
10970
- const reply = (response, status, body, type = "application/json; charset=utf-8") => {
10971
- response.writeHead(status, {
10972
- "cache-control": "no-store",
10973
- "content-type": type,
10974
- "x-content-type-options": "nosniff"
10975
- });
10976
- response.end(body);
10977
- };
10978
10940
  const parseSort = (value) => {
10979
10941
  if (value === null) return "relevance";
10980
10942
  return value === "relevance" || value === "updated" || value === "title" ? value : void 0;
@@ -10991,12 +10953,12 @@ const openDocument = async (pathname, response, index, previews) => {
10991
10953
  try {
10992
10954
  id = decodeURIComponent(pathname.slice(21));
10993
10955
  } catch {
10994
- reply(response, 400, JSON.stringify({ error: "invalid-document" }));
10956
+ replyJson(response, 400, { error: "invalid-document" });
10995
10957
  return;
10996
10958
  }
10997
10959
  const file = await index.resolve(id);
10998
10960
  if (file === void 0) {
10999
- reply(response, 404, "文書が見つかりません。一覧を更新してください。\nDocument not found. Refresh the library.", "text/plain; charset=utf-8");
10961
+ replyText(response, 404, "文書が見つかりません。一覧を更新してください。\nDocument not found. Refresh the library.", "text/plain; charset=utf-8");
11000
10962
  return;
11001
10963
  }
11002
10964
  const url = await previews.open(file);
@@ -11010,23 +10972,23 @@ const openDocument = async (pathname, response, index, previews) => {
11010
10972
  const deleteDocument = async (pathname, response, index, previews) => {
11011
10973
  const encodedId = pathname.startsWith(DELETE_PREFIX) ? pathname.slice(DELETE_PREFIX.length) : "";
11012
10974
  if (encodedId === "" || encodedId.includes("/")) {
11013
- reply(response, 400, JSON.stringify({ error: "invalid-document" }));
10975
+ replyJson(response, 400, { error: "invalid-document" });
11014
10976
  return;
11015
10977
  }
11016
10978
  let id;
11017
10979
  try {
11018
10980
  id = decodeURIComponent(encodedId);
11019
10981
  } catch {
11020
- reply(response, 400, JSON.stringify({ error: "invalid-document" }));
10982
+ replyJson(response, 400, { error: "invalid-document" });
11021
10983
  return;
11022
10984
  }
11023
10985
  const result = await index.remove(id);
11024
10986
  if (result.status === "invalid") {
11025
- reply(response, 400, JSON.stringify({ error: "invalid-document" }));
10987
+ replyJson(response, 400, { error: "invalid-document" });
11026
10988
  return;
11027
10989
  }
11028
10990
  if (result.status === "not-found") {
11029
- reply(response, 404, JSON.stringify({ error: "not-found" }));
10991
+ replyJson(response, 404, { error: "not-found" });
11030
10992
  return;
11031
10993
  }
11032
10994
  await previews.remove(result.filePath);
@@ -11039,18 +11001,18 @@ const deleteDocument = async (pathname, response, index, previews) => {
11039
11001
  const handleDeleteRequest = async (request, pathname, response, index, previews) => {
11040
11002
  if (request.method !== "DELETE") {
11041
11003
  response.setHeader("allow", "DELETE");
11042
- reply(response, 405, JSON.stringify({ error: "method-not-allowed" }));
11004
+ replyJson(response, 405, { error: "method-not-allowed" });
11043
11005
  return;
11044
11006
  }
11045
11007
  if (request.headers[DELETE_ACTION_HEADER] !== "delete" || request.headers["sec-fetch-site"] === "cross-site") {
11046
- reply(response, 403, JSON.stringify({ error: "forbidden" }));
11008
+ replyJson(response, 403, { error: "forbidden" });
11047
11009
  return;
11048
11010
  }
11049
11011
  await deleteDocument(pathname, response, index, previews);
11050
11012
  };
11051
11013
  const handleLibraryRequest = async (request, response, index, previews, html) => {
11052
- if (!localRequest(request)) {
11053
- reply(response, 403, JSON.stringify({ error: "forbidden" }));
11014
+ if (!isLocalOrigin(request)) {
11015
+ replyJson(response, 403, { error: "forbidden" });
11054
11016
  return;
11055
11017
  }
11056
11018
  const url = new URL(request.url ?? "/", "http://localhost");
@@ -11060,25 +11022,25 @@ const handleLibraryRequest = async (request, response, index, previews, html) =>
11060
11022
  }
11061
11023
  if (request.method !== "GET") {
11062
11024
  response.setHeader("allow", "GET");
11063
- reply(response, 405, JSON.stringify({ error: "method-not-allowed" }));
11025
+ replyJson(response, 405, { error: "method-not-allowed" });
11064
11026
  return;
11065
11027
  }
11066
11028
  if (url.pathname === "/") {
11067
- reply(response, 200, html, "text/html; charset=utf-8");
11029
+ replyText(response, 200, html, "text/html; charset=utf-8");
11068
11030
  return;
11069
11031
  }
11070
11032
  if (url.pathname === "/__mdxr_library.js") {
11071
- reply(response, 200, await libraryJs(), "text/javascript; charset=utf-8");
11033
+ replyText(response, 200, await libraryJs(), "text/javascript; charset=utf-8");
11072
11034
  return;
11073
11035
  }
11074
11036
  if (request.headers["sec-fetch-site"] === "cross-site") {
11075
- reply(response, 403, JSON.stringify({ error: "forbidden" }));
11037
+ replyJson(response, 403, { error: "forbidden" });
11076
11038
  return;
11077
11039
  }
11078
11040
  if (url.pathname === SEARCH_PATH) {
11079
11041
  const sort = parseSort(url.searchParams.get("sort"));
11080
11042
  if (sort === void 0) {
11081
- reply(response, 400, JSON.stringify({ error: "invalid-sort" }));
11043
+ replyJson(response, 400, { error: "invalid-sort" });
11082
11044
  return;
11083
11045
  }
11084
11046
  const result = await index.search({
@@ -11086,14 +11048,14 @@ const handleLibraryRequest = async (request, response, index, previews, html) =>
11086
11048
  sort,
11087
11049
  status: url.searchParams.get("status") ?? void 0
11088
11050
  });
11089
- reply(response, 200, JSON.stringify(result));
11051
+ replyJson(response, 200, result);
11090
11052
  return;
11091
11053
  }
11092
11054
  if (url.pathname.startsWith(OPEN_PREFIX)) {
11093
11055
  await openDocument(url.pathname, response, index, previews);
11094
11056
  return;
11095
11057
  }
11096
- reply(response, 404, JSON.stringify({ error: "not-found" }));
11058
+ replyJson(response, 404, { error: "not-found" });
11097
11059
  };
11098
11060
  /** Browse a local directory without evaluating its documents during indexing. */
11099
11061
  const serveLibrary = async (dir, port, opts = {}) => {
@@ -11111,7 +11073,7 @@ const serveLibrary = async (dir, port, opts = {}) => {
11111
11073
  await handleLibraryRequest(request, response, index, previews, html);
11112
11074
  } catch (error) {
11113
11075
  console.error(`mdxr: library: ${formatError(error)}`);
11114
- reply(response, 500, JSON.stringify({ error: "library-error" }));
11076
+ replyJson(response, 500, { error: "library-error" });
11115
11077
  }
11116
11078
  })();
11117
11079
  });