@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.
- package/dist/cli.mjs +421 -459
- package/dist/components.d.mts +4 -4
- package/dist/components.mjs +2 -2
- package/dist/{config-DSZfMOIO.mjs → config-By0YyXJs.mjs} +1 -1
- package/dist/{doc-context-Cye5isVT.d.mts → doc-context-Cz2pR4Vn.d.mts} +10 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{ui-CyLil0AF.mjs → ui-CawgRB1q.mjs} +853 -6
- package/package.json +2 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/components/output.md +34 -2
- package/skill/references/components.md +1 -0
- package/src/agent-http.ts +13 -33
- package/src/ascii/output.ts +25 -1
- package/src/document-history-http.ts +3 -33
- package/src/file-preview-http.ts +11 -27
- package/src/library-app.tsx +7 -601
- package/src/library-controls.tsx +186 -0
- package/src/library-document.ts +242 -0
- package/src/library-files.ts +162 -0
- package/src/library-index.ts +38 -439
- package/src/library-language.ts +182 -0
- package/src/library-results.tsx +261 -0
- package/src/library.ts +24 -42
- package/src/local-http.ts +43 -0
- package/src/preview-watch.ts +197 -0
- package/src/serve.ts +13 -181
- package/src/styles/globals.css +1 -0
- package/src/styles/interactive-graph.css +345 -0
- package/src/ui/graph-node-inspector.tsx +232 -0
- package/src/ui/graph-specs.tsx +16 -2
- package/src/ui/graph-view-rail.tsx +195 -0
- package/src/ui/graph.tsx +13 -5
- package/src/ui/index.ts +13 -0
- package/src/ui/interactive-graph-model.ts +212 -0
- package/src/ui/interactive-graph-specs.tsx +149 -0
- package/src/ui/interactive-graph.tsx +462 -0
- 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 {
|
|
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-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6365
|
+
filePath = void 0;
|
|
6363
6366
|
}
|
|
6364
|
-
return
|
|
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
|
-
|
|
7437
|
+
replyJson(res, 404, { error: "Agent chat is disabled" });
|
|
7429
7438
|
return;
|
|
7430
7439
|
}
|
|
7431
|
-
if (!
|
|
7432
|
-
|
|
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
|
-
|
|
7445
|
+
replyJson(res, 200, view(await agent.current()));
|
|
7437
7446
|
return;
|
|
7438
7447
|
}
|
|
7439
7448
|
if (req.method !== "POST") {
|
|
7440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10300
|
+
replyJson(response, 404, { error: "Document history is unavailable" });
|
|
10311
10301
|
return;
|
|
10312
10302
|
}
|
|
10313
|
-
if (!
|
|
10314
|
-
replyJson
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
10437
|
-
replyJson
|
|
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
|
|
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
|
|
10423
|
+
replyJson(response, 404, { error: "The referenced file is unavailable" });
|
|
10448
10424
|
return;
|
|
10449
10425
|
}
|
|
10450
10426
|
try {
|
|
10451
|
-
replyJson
|
|
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 (!
|
|
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("&", "&").replaceAll("<", "<")}</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
|
|
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)
|
|
10759
|
+
if (!closed) watcher.armDependencies(target.deps?.() ?? []);
|
|
10776
10760
|
};
|
|
10777
10761
|
let reloading = Promise.resolve();
|
|
10778
|
-
const reload = (
|
|
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(
|
|
10768
|
+
await rebuild(watcher);
|
|
10785
10769
|
broadcast(clients, "reload");
|
|
10786
10770
|
} catch {}
|
|
10787
10771
|
})();
|
|
10788
10772
|
};
|
|
10789
|
-
const
|
|
10790
|
-
|
|
10791
|
-
|
|
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
|
-
|
|
10808
|
-
watch.close();
|
|
10783
|
+
watcher.close();
|
|
10809
10784
|
closeAgent();
|
|
10810
10785
|
});
|
|
10811
|
-
reloading = rebuild(
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10987
|
+
replyJson(response, 400, { error: "invalid-document" });
|
|
11026
10988
|
return;
|
|
11027
10989
|
}
|
|
11028
10990
|
if (result.status === "not-found") {
|
|
11029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
11053
|
-
|
|
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
|
-
|
|
11025
|
+
replyJson(response, 405, { error: "method-not-allowed" });
|
|
11064
11026
|
return;
|
|
11065
11027
|
}
|
|
11066
11028
|
if (url.pathname === "/") {
|
|
11067
|
-
|
|
11029
|
+
replyText(response, 200, html, "text/html; charset=utf-8");
|
|
11068
11030
|
return;
|
|
11069
11031
|
}
|
|
11070
11032
|
if (url.pathname === "/__mdxr_library.js") {
|
|
11071
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11076
|
+
replyJson(response, 500, { error: "library-error" });
|
|
11115
11077
|
}
|
|
11116
11078
|
})();
|
|
11117
11079
|
});
|