diffwiki-core 0.6.0 → 0.6.1
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/index.cjs +122 -71
- package/dist/index.d.cts +45 -5
- package/dist/index.d.ts +45 -5
- package/dist/index.js +120 -70
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -591,6 +591,7 @@ __export(index_exports, {
|
|
|
591
591
|
emitPluginEvent: () => emitPluginEvent,
|
|
592
592
|
ensureGitRepo: () => ensureGitRepo,
|
|
593
593
|
ensurePluginRoot: () => ensurePluginRoot,
|
|
594
|
+
fileTimestamps: () => fileTimestamps,
|
|
594
595
|
findCollection: () => findCollection,
|
|
595
596
|
findCollectionById: () => findCollectionById,
|
|
596
597
|
findEnabledPlugin: () => findEnabledPlugin,
|
|
@@ -755,8 +756,6 @@ function parseArticle(raw) {
|
|
|
755
756
|
}
|
|
756
757
|
function serializeArticle(article) {
|
|
757
758
|
const front = { title: article.title, tags: article.tags };
|
|
758
|
-
if (article.created) front.created = article.created;
|
|
759
|
-
if (article.updated) front.updated = article.updated;
|
|
760
759
|
return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
|
|
761
760
|
}
|
|
762
761
|
|
|
@@ -816,11 +815,10 @@ async function createArticle(opts) {
|
|
|
816
815
|
const { collection, title } = parseAddTarget(opts.target);
|
|
817
816
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
818
817
|
const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
|
|
819
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
820
818
|
const body = opts.body ?? `# ${title}
|
|
821
819
|
`;
|
|
822
820
|
await import_promises5.default.mkdir(dir, { recursive: true });
|
|
823
|
-
await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags,
|
|
821
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, body }), "utf8");
|
|
824
822
|
void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
|
|
825
823
|
return { title, tags: opts.tags, path: filePath, collection: name };
|
|
826
824
|
}
|
|
@@ -838,17 +836,7 @@ async function loadArticle(target) {
|
|
|
838
836
|
async function updateArticleBody(target, body) {
|
|
839
837
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
840
838
|
const parsed = parseArticle(raw);
|
|
841
|
-
await import_promises5.default.writeFile(
|
|
842
|
-
filePath,
|
|
843
|
-
serializeArticle({
|
|
844
|
-
title: parsed.title,
|
|
845
|
-
tags: parsed.tags,
|
|
846
|
-
created: parsed.created,
|
|
847
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
-
body
|
|
849
|
-
}),
|
|
850
|
-
"utf8"
|
|
851
|
-
);
|
|
839
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags: parsed.tags, body }), "utf8");
|
|
852
840
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
853
841
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
854
842
|
}
|
|
@@ -856,17 +844,7 @@ async function mutateTags(target, fn) {
|
|
|
856
844
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
857
845
|
const parsed = parseArticle(raw);
|
|
858
846
|
const tags = fn(parsed.tags);
|
|
859
|
-
await import_promises5.default.writeFile(
|
|
860
|
-
filePath,
|
|
861
|
-
serializeArticle({
|
|
862
|
-
title: parsed.title,
|
|
863
|
-
tags,
|
|
864
|
-
created: parsed.created,
|
|
865
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
866
|
-
body: parsed.body
|
|
867
|
-
}),
|
|
868
|
-
"utf8"
|
|
869
|
-
);
|
|
847
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags, body: parsed.body }), "utf8");
|
|
870
848
|
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
871
849
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
872
850
|
}
|
|
@@ -888,7 +866,7 @@ async function removeArticle(target) {
|
|
|
888
866
|
}
|
|
889
867
|
|
|
890
868
|
// src/repo.ts
|
|
891
|
-
var
|
|
869
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
892
870
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
893
871
|
var import_node_child_process3 = require("child_process");
|
|
894
872
|
var import_node_util2 = require("util");
|
|
@@ -897,6 +875,7 @@ init_collections();
|
|
|
897
875
|
init_registry();
|
|
898
876
|
|
|
899
877
|
// src/git.ts
|
|
878
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
900
879
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
901
880
|
var import_node_child_process2 = require("child_process");
|
|
902
881
|
var import_node_util = require("util");
|
|
@@ -959,6 +938,35 @@ async function ensureGitRepo(cwd) {
|
|
|
959
938
|
if (!after) throw new Error("git init did not produce a repository");
|
|
960
939
|
return after;
|
|
961
940
|
}
|
|
941
|
+
function isEpochish(d) {
|
|
942
|
+
return !Number.isFinite(d.getTime()) || d.getTime() <= 0;
|
|
943
|
+
}
|
|
944
|
+
var timestampCache = /* @__PURE__ */ new Map();
|
|
945
|
+
async function fileTimestamps(absPath) {
|
|
946
|
+
const cached = timestampCache.get(absPath);
|
|
947
|
+
if (cached) return cached;
|
|
948
|
+
const dir = import_node_path4.default.dirname(absPath);
|
|
949
|
+
const base = import_node_path4.default.basename(absPath);
|
|
950
|
+
const [addLog, lastLog] = await Promise.all([
|
|
951
|
+
git(dir, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", base]),
|
|
952
|
+
git(dir, ["log", "-1", "--format=%aI", "--", base])
|
|
953
|
+
]);
|
|
954
|
+
const gitCreated = addLog ? addLog.split("\n").filter(Boolean).pop() : void 0;
|
|
955
|
+
const gitUpdated = lastLog || void 0;
|
|
956
|
+
let created = gitCreated;
|
|
957
|
+
let updated = gitUpdated;
|
|
958
|
+
if (!created || !updated) {
|
|
959
|
+
try {
|
|
960
|
+
const st = await import_promises6.default.stat(absPath);
|
|
961
|
+
if (!updated) updated = st.mtime.toISOString();
|
|
962
|
+
if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const result = { created, updated };
|
|
967
|
+
timestampCache.set(absPath, result);
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
962
970
|
|
|
963
971
|
// src/repo.ts
|
|
964
972
|
init_errors();
|
|
@@ -974,7 +982,7 @@ async function detectRepoName(cwd) {
|
|
|
974
982
|
}
|
|
975
983
|
async function readRepoConfig(cwd) {
|
|
976
984
|
try {
|
|
977
|
-
const raw = await
|
|
985
|
+
const raw = await import_promises7.default.readFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), "utf8");
|
|
978
986
|
const parsed = (0, import_yaml2.parse)(raw) ?? {};
|
|
979
987
|
if (!parsed.path) return void 0;
|
|
980
988
|
return {
|
|
@@ -1004,9 +1012,9 @@ async function initRepoWiki(opts) {
|
|
|
1004
1012
|
n += 1;
|
|
1005
1013
|
}
|
|
1006
1014
|
}
|
|
1007
|
-
await
|
|
1015
|
+
await import_promises7.default.mkdir(absWiki, { recursive: true });
|
|
1008
1016
|
const config = { collection: name, path: wikiPath };
|
|
1009
|
-
await
|
|
1017
|
+
await import_promises7.default.writeFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
|
|
1010
1018
|
const entry = {
|
|
1011
1019
|
name,
|
|
1012
1020
|
type: "repo",
|
|
@@ -1078,7 +1086,7 @@ async function collectionGitStatus(entry) {
|
|
|
1078
1086
|
init_collections();
|
|
1079
1087
|
|
|
1080
1088
|
// src/browse.ts
|
|
1081
|
-
var
|
|
1089
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
1082
1090
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
1083
1091
|
init_collections();
|
|
1084
1092
|
init_errors();
|
|
@@ -1125,7 +1133,7 @@ function buildArticleTree(relPaths) {
|
|
|
1125
1133
|
async function collectRelPaths(dir, rel = "") {
|
|
1126
1134
|
let dirents;
|
|
1127
1135
|
try {
|
|
1128
|
-
dirents = await
|
|
1136
|
+
dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
|
|
1129
1137
|
} catch {
|
|
1130
1138
|
return [];
|
|
1131
1139
|
}
|
|
@@ -1143,7 +1151,7 @@ async function collectRelPaths(dir, rel = "") {
|
|
|
1143
1151
|
}
|
|
1144
1152
|
async function readdirSafe(dir) {
|
|
1145
1153
|
try {
|
|
1146
|
-
return await
|
|
1154
|
+
return await import_promises8.default.readdir(dir);
|
|
1147
1155
|
} catch {
|
|
1148
1156
|
return [];
|
|
1149
1157
|
}
|
|
@@ -1160,7 +1168,7 @@ function pickIndexFile(names) {
|
|
|
1160
1168
|
async function cheapTitle(absPath) {
|
|
1161
1169
|
let handle;
|
|
1162
1170
|
try {
|
|
1163
|
-
handle = await
|
|
1171
|
+
handle = await import_promises8.default.open(absPath, "r");
|
|
1164
1172
|
const buf = Buffer.alloc(512);
|
|
1165
1173
|
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
1166
1174
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -1210,11 +1218,12 @@ async function readArticle(coll, slug) {
|
|
|
1210
1218
|
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
1211
1219
|
const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
|
|
1212
1220
|
let abs;
|
|
1221
|
+
let isIndex = false;
|
|
1213
1222
|
for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
|
|
1214
1223
|
const candidate = import_node_path6.default.resolve(collDir, rel);
|
|
1215
1224
|
if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
|
|
1216
1225
|
try {
|
|
1217
|
-
await
|
|
1226
|
+
await import_promises8.default.access(candidate);
|
|
1218
1227
|
abs = candidate;
|
|
1219
1228
|
break;
|
|
1220
1229
|
} catch {
|
|
@@ -1224,16 +1233,20 @@ async function readArticle(coll, slug) {
|
|
|
1224
1233
|
const dir = import_node_path6.default.resolve(collDir, safeSlug);
|
|
1225
1234
|
if (inJail(dir)) {
|
|
1226
1235
|
const idx = pickIndexFile(await readdirSafe(dir));
|
|
1227
|
-
if (idx)
|
|
1236
|
+
if (idx) {
|
|
1237
|
+
abs = import_node_path6.default.join(dir, idx);
|
|
1238
|
+
isIndex = true;
|
|
1239
|
+
}
|
|
1228
1240
|
}
|
|
1229
1241
|
}
|
|
1230
1242
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
1231
|
-
const parsed = parseArticle(await
|
|
1243
|
+
const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
|
|
1232
1244
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
1233
|
-
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
1245
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs, isIndex };
|
|
1234
1246
|
}
|
|
1235
1247
|
|
|
1236
1248
|
// src/render.ts
|
|
1249
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1237
1250
|
var import_unified = require("unified");
|
|
1238
1251
|
var import_remark_parse = __toESM(require("remark-parse"), 1);
|
|
1239
1252
|
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
@@ -1243,6 +1256,36 @@ var import_rehype = __toESM(require("@shikijs/rehype"), 1);
|
|
|
1243
1256
|
var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
|
|
1244
1257
|
var import_unist_util_visit = require("unist-util-visit");
|
|
1245
1258
|
var import_hast_util_to_string = require("hast-util-to-string");
|
|
1259
|
+
function isNonRelativeHref(href) {
|
|
1260
|
+
if (href === "") return true;
|
|
1261
|
+
if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return true;
|
|
1262
|
+
if (href.startsWith("//")) return true;
|
|
1263
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
1264
|
+
}
|
|
1265
|
+
function rehypeRewriteLinks(ctx) {
|
|
1266
|
+
const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path7.default.posix.dirname(ctx.slug || ".");
|
|
1267
|
+
return (tree) => {
|
|
1268
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node) => {
|
|
1269
|
+
if (node.tagName !== "a") return;
|
|
1270
|
+
const raw = node.properties?.href;
|
|
1271
|
+
if (typeof raw !== "string" || isNonRelativeHref(raw)) return;
|
|
1272
|
+
const suffixMatch = /[#?].*$/.exec(raw);
|
|
1273
|
+
const suffix = suffixMatch ? suffixMatch[0] : "";
|
|
1274
|
+
const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
|
|
1275
|
+
if (pathPart === "") return;
|
|
1276
|
+
const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
|
|
1277
|
+
const normalized = import_node_path7.default.posix.normalize(joined);
|
|
1278
|
+
if (normalized.startsWith("..")) return;
|
|
1279
|
+
let resolved = normalized.replace(/\.(mdx?)$/i, "");
|
|
1280
|
+
resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
|
|
1281
|
+
resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
1282
|
+
node.properties = {
|
|
1283
|
+
...node.properties,
|
|
1284
|
+
href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
|
|
1285
|
+
};
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1246
1289
|
var LANG_ALIASES = {
|
|
1247
1290
|
"c#": "csharp",
|
|
1248
1291
|
cs: "csharp",
|
|
@@ -1286,9 +1329,9 @@ function rehypeMarkMermaid() {
|
|
|
1286
1329
|
});
|
|
1287
1330
|
};
|
|
1288
1331
|
}
|
|
1289
|
-
async function renderArticle(body) {
|
|
1332
|
+
async function renderArticle(body, ctx) {
|
|
1290
1333
|
const toc = [];
|
|
1291
|
-
const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(import_rehype.default, {
|
|
1334
|
+
const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeRewriteLinks, ctx).use(import_rehype.default, {
|
|
1292
1335
|
themes: { light: "github-light", dark: "github-dark" },
|
|
1293
1336
|
defaultColor: false,
|
|
1294
1337
|
langAlias: LANG_ALIASES,
|
|
@@ -1351,7 +1394,12 @@ function findNode(nodes, slug) {
|
|
|
1351
1394
|
async function resolvePage(coll, slug) {
|
|
1352
1395
|
try {
|
|
1353
1396
|
const art = await readArticle(coll, slug);
|
|
1354
|
-
const { html, toc } = await renderArticle(art.body
|
|
1397
|
+
const { html, toc } = await renderArticle(art.body, {
|
|
1398
|
+
coll: art.coll,
|
|
1399
|
+
slug: art.slug,
|
|
1400
|
+
isIndex: art.isIndex
|
|
1401
|
+
});
|
|
1402
|
+
const { created, updated } = await fileTimestamps(art.path);
|
|
1355
1403
|
return {
|
|
1356
1404
|
kind: "doc",
|
|
1357
1405
|
coll: art.coll,
|
|
@@ -1360,7 +1408,9 @@ async function resolvePage(coll, slug) {
|
|
|
1360
1408
|
tags: art.tags,
|
|
1361
1409
|
html,
|
|
1362
1410
|
toc,
|
|
1363
|
-
path: `/${art.coll}/${art.slug}
|
|
1411
|
+
path: `/${art.coll}/${art.slug}`,
|
|
1412
|
+
created,
|
|
1413
|
+
updated
|
|
1364
1414
|
};
|
|
1365
1415
|
} catch (err) {
|
|
1366
1416
|
if (err instanceof CollectionNotFoundError) return null;
|
|
@@ -1418,8 +1468,8 @@ function resolveCollectionSelection(all, sel = {}) {
|
|
|
1418
1468
|
}
|
|
1419
1469
|
|
|
1420
1470
|
// src/query.ts
|
|
1421
|
-
var
|
|
1422
|
-
var
|
|
1471
|
+
var import_promises9 = __toESM(require("fs/promises"), 1);
|
|
1472
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1423
1473
|
init_collections();
|
|
1424
1474
|
init_errors();
|
|
1425
1475
|
init_logger();
|
|
@@ -1488,13 +1538,13 @@ var log2 = getLogger("query");
|
|
|
1488
1538
|
async function collectMarkdownFiles(dir) {
|
|
1489
1539
|
let entries;
|
|
1490
1540
|
try {
|
|
1491
|
-
entries = await
|
|
1541
|
+
entries = await import_promises9.default.readdir(dir, { withFileTypes: true });
|
|
1492
1542
|
} catch {
|
|
1493
1543
|
return [];
|
|
1494
1544
|
}
|
|
1495
1545
|
const files = [];
|
|
1496
1546
|
for (const entry of entries) {
|
|
1497
|
-
const full =
|
|
1547
|
+
const full = import_node_path8.default.join(dir, entry.name);
|
|
1498
1548
|
if (entry.isDirectory()) {
|
|
1499
1549
|
files.push(...await collectMarkdownFiles(full));
|
|
1500
1550
|
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
@@ -1513,12 +1563,12 @@ async function nativeSearch(term, collections, collection) {
|
|
|
1513
1563
|
const docs = [];
|
|
1514
1564
|
for (const entry of targets) {
|
|
1515
1565
|
for (const full of await collectMarkdownFiles(entry.path)) {
|
|
1516
|
-
const fallbackTitle =
|
|
1566
|
+
const fallbackTitle = import_node_path8.default.basename(full).replace(/\.mdx?$/, "");
|
|
1517
1567
|
let title = fallbackTitle;
|
|
1518
1568
|
let tags = [];
|
|
1519
1569
|
let body = "";
|
|
1520
1570
|
try {
|
|
1521
|
-
const parsed = parseArticle(await
|
|
1571
|
+
const parsed = parseArticle(await import_promises9.default.readFile(full, "utf8"));
|
|
1522
1572
|
if (parsed.title) title = parsed.title;
|
|
1523
1573
|
tags = parsed.tags;
|
|
1524
1574
|
body = parsed.body;
|
|
@@ -1601,13 +1651,13 @@ async function query(term, opts) {
|
|
|
1601
1651
|
}
|
|
1602
1652
|
|
|
1603
1653
|
// src/search.ts
|
|
1604
|
-
var
|
|
1605
|
-
var
|
|
1654
|
+
var import_promises10 = __toESM(require("fs/promises"), 1);
|
|
1655
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1606
1656
|
init_collections();
|
|
1607
1657
|
async function readArticleParts(collDir, slug) {
|
|
1608
1658
|
for (const ext of [".mdx", ".md"]) {
|
|
1609
1659
|
try {
|
|
1610
|
-
const parsed = parseArticle(await
|
|
1660
|
+
const parsed = parseArticle(await import_promises10.default.readFile(import_node_path9.default.join(collDir, `${slug}${ext}`), "utf8"));
|
|
1611
1661
|
return { tags: parsed.tags, body: parsed.body };
|
|
1612
1662
|
} catch {
|
|
1613
1663
|
}
|
|
@@ -1641,7 +1691,7 @@ async function buildSearchIndex(collection) {
|
|
|
1641
1691
|
for (const e of entries) {
|
|
1642
1692
|
try {
|
|
1643
1693
|
const collEntry = await findCollection(e.name);
|
|
1644
|
-
const collDir =
|
|
1694
|
+
const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
|
|
1645
1695
|
await flatten(e.name, collDir, await listArticleTree(e.name), out);
|
|
1646
1696
|
} catch {
|
|
1647
1697
|
}
|
|
@@ -1650,15 +1700,15 @@ async function buildSearchIndex(collection) {
|
|
|
1650
1700
|
}
|
|
1651
1701
|
|
|
1652
1702
|
// src/doctor.ts
|
|
1653
|
-
var
|
|
1703
|
+
var import_promises12 = __toESM(require("fs/promises"), 1);
|
|
1654
1704
|
init_paths();
|
|
1655
1705
|
init_registry();
|
|
1656
1706
|
init_collections();
|
|
1657
1707
|
|
|
1658
1708
|
// src/plugins/manager.ts
|
|
1659
|
-
var
|
|
1709
|
+
var import_promises11 = __toESM(require("fs/promises"), 1);
|
|
1660
1710
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
1661
|
-
var
|
|
1711
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1662
1712
|
init_paths();
|
|
1663
1713
|
init_errors();
|
|
1664
1714
|
init_collections();
|
|
@@ -1669,7 +1719,7 @@ init_host();
|
|
|
1669
1719
|
var import_node_child_process4 = require("child_process");
|
|
1670
1720
|
var import_node_util3 = require("util");
|
|
1671
1721
|
var import_node_fs = require("fs");
|
|
1672
|
-
var
|
|
1722
|
+
var import_node_path10 = require("path");
|
|
1673
1723
|
var import_node_url = require("url");
|
|
1674
1724
|
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
1675
1725
|
async function runProcess(file, args, opts) {
|
|
@@ -1685,7 +1735,7 @@ async function runProcess(file, args, opts) {
|
|
|
1685
1735
|
// src/plugins/manager.ts
|
|
1686
1736
|
async function readPackageJson(file) {
|
|
1687
1737
|
try {
|
|
1688
|
-
return JSON.parse(await
|
|
1738
|
+
return JSON.parse(await import_promises11.default.readFile(file, "utf8"));
|
|
1689
1739
|
} catch {
|
|
1690
1740
|
return null;
|
|
1691
1741
|
}
|
|
@@ -1701,7 +1751,7 @@ function binOf(manifest) {
|
|
|
1701
1751
|
async function pluginManifestDirs(nodeModules) {
|
|
1702
1752
|
let entries;
|
|
1703
1753
|
try {
|
|
1704
|
-
entries = await
|
|
1754
|
+
entries = await import_promises11.default.readdir(nodeModules);
|
|
1705
1755
|
} catch {
|
|
1706
1756
|
return [];
|
|
1707
1757
|
}
|
|
@@ -1711,44 +1761,44 @@ async function pluginManifestDirs(nodeModules) {
|
|
|
1711
1761
|
if (entry.startsWith("@")) {
|
|
1712
1762
|
let scoped;
|
|
1713
1763
|
try {
|
|
1714
|
-
scoped = await
|
|
1764
|
+
scoped = await import_promises11.default.readdir(import_node_path11.default.join(nodeModules, entry));
|
|
1715
1765
|
} catch {
|
|
1716
1766
|
continue;
|
|
1717
1767
|
}
|
|
1718
1768
|
for (const pkg of scoped) {
|
|
1719
1769
|
if (pkg.startsWith(".")) continue;
|
|
1720
|
-
dirs.push(
|
|
1770
|
+
dirs.push(import_node_path11.default.join(nodeModules, entry, pkg));
|
|
1721
1771
|
}
|
|
1722
1772
|
} else {
|
|
1723
|
-
dirs.push(
|
|
1773
|
+
dirs.push(import_node_path11.default.join(nodeModules, entry));
|
|
1724
1774
|
}
|
|
1725
1775
|
}
|
|
1726
1776
|
return dirs;
|
|
1727
1777
|
}
|
|
1728
1778
|
async function ensurePluginRoot() {
|
|
1729
1779
|
const dir = pluginsDir();
|
|
1730
|
-
await
|
|
1731
|
-
const pkgPath =
|
|
1780
|
+
await import_promises11.default.mkdir(dir, { recursive: true });
|
|
1781
|
+
const pkgPath = import_node_path11.default.join(dir, "package.json");
|
|
1732
1782
|
if (await readPackageJson(pkgPath) === null) {
|
|
1733
1783
|
const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
|
|
1734
|
-
await
|
|
1784
|
+
await import_promises11.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1735
1785
|
`, "utf8");
|
|
1736
1786
|
}
|
|
1737
1787
|
return dir;
|
|
1738
1788
|
}
|
|
1739
1789
|
async function asSearchPackage(pkgDir) {
|
|
1740
|
-
const manifest = await readPackageJson(
|
|
1790
|
+
const manifest = await readPackageJson(import_node_path11.default.join(pkgDir, "package.json"));
|
|
1741
1791
|
if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
|
|
1742
1792
|
const bin = binOf(manifest);
|
|
1743
1793
|
if (!bin) return null;
|
|
1744
|
-
return { name: manifest.name, version: manifest.version, binAbs:
|
|
1794
|
+
return { name: manifest.name, version: manifest.version, binAbs: import_node_path11.default.resolve(pkgDir, bin) };
|
|
1745
1795
|
}
|
|
1746
1796
|
async function packageNameFromSpec(spec) {
|
|
1747
1797
|
const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
|
|
1748
1798
|
if (looksLikePath) {
|
|
1749
1799
|
const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
|
|
1750
|
-
const abs =
|
|
1751
|
-
return (await readPackageJson(
|
|
1800
|
+
const abs = import_node_path11.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os2.default.homedir()));
|
|
1801
|
+
return (await readPackageJson(import_node_path11.default.join(abs, "package.json")))?.name ?? null;
|
|
1752
1802
|
}
|
|
1753
1803
|
if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
|
|
1754
1804
|
if (spec.startsWith("@")) {
|
|
@@ -1759,10 +1809,10 @@ async function packageNameFromSpec(spec) {
|
|
|
1759
1809
|
return at <= 0 ? spec : spec.slice(0, at);
|
|
1760
1810
|
}
|
|
1761
1811
|
async function resolveSearchPackage(dir, name) {
|
|
1762
|
-
return asSearchPackage(
|
|
1812
|
+
return asSearchPackage(import_node_path11.default.join(dir, "node_modules", ...name.split("/")));
|
|
1763
1813
|
}
|
|
1764
1814
|
async function discoverSearchPackage(dir) {
|
|
1765
|
-
const nodeModules =
|
|
1815
|
+
const nodeModules = import_node_path11.default.join(dir, "node_modules");
|
|
1766
1816
|
for (const pkgDir of await pluginManifestDirs(nodeModules)) {
|
|
1767
1817
|
const found = await asSearchPackage(pkgDir);
|
|
1768
1818
|
if (found) return found;
|
|
@@ -1894,7 +1944,7 @@ init_host();
|
|
|
1894
1944
|
init_types();
|
|
1895
1945
|
async function exists(target) {
|
|
1896
1946
|
try {
|
|
1897
|
-
await
|
|
1947
|
+
await import_promises12.default.access(target);
|
|
1898
1948
|
return true;
|
|
1899
1949
|
} catch {
|
|
1900
1950
|
return false;
|
|
@@ -2061,6 +2111,7 @@ init_registry2();
|
|
|
2061
2111
|
emitPluginEvent,
|
|
2062
2112
|
ensureGitRepo,
|
|
2063
2113
|
ensurePluginRoot,
|
|
2114
|
+
fileTimestamps,
|
|
2064
2115
|
findCollection,
|
|
2065
2116
|
findCollectionById,
|
|
2066
2117
|
findEnabledPlugin,
|
package/dist/index.d.cts
CHANGED
|
@@ -60,6 +60,13 @@ interface ArticleContent {
|
|
|
60
60
|
body: string;
|
|
61
61
|
/** Absolute path to the file on disk. */
|
|
62
62
|
path: string;
|
|
63
|
+
/**
|
|
64
|
+
* True when the resolved file is a folder-index page (`<slug>/index.md` or
|
|
65
|
+
* `<slug>/README.md`) rather than a leaf `<slug>.md`. The route slug is the
|
|
66
|
+
* same for both, so this flag is needed to resolve folder-local relative
|
|
67
|
+
* links against the article's own folder.
|
|
68
|
+
*/
|
|
69
|
+
isIndex: boolean;
|
|
63
70
|
}
|
|
64
71
|
type PluginKind = 'search';
|
|
65
72
|
declare const NATIVE_ENGINE = "native";
|
|
@@ -181,6 +188,10 @@ interface DocData {
|
|
|
181
188
|
html: string;
|
|
182
189
|
toc: TocItem[];
|
|
183
190
|
path: string;
|
|
191
|
+
/** ISO-8601 creation date, sourced from git history / filesystem (not frontmatter). */
|
|
192
|
+
created?: string;
|
|
193
|
+
/** ISO-8601 last-update date, sourced from git history / filesystem (not frontmatter). */
|
|
194
|
+
updated?: string;
|
|
184
195
|
}
|
|
185
196
|
interface ListingEntry {
|
|
186
197
|
name: string;
|
|
@@ -261,8 +272,6 @@ interface ParsedArticle {
|
|
|
261
272
|
interface SerializableArticle {
|
|
262
273
|
title: string;
|
|
263
274
|
tags: string[];
|
|
264
|
-
created?: string;
|
|
265
|
-
updated?: string;
|
|
266
275
|
body: string;
|
|
267
276
|
}
|
|
268
277
|
declare function parseArticle(raw: string): ParsedArticle;
|
|
@@ -369,6 +378,23 @@ interface UpstreamStatus {
|
|
|
369
378
|
declare function upstreamStatus(cwd: string): Promise<UpstreamStatus>;
|
|
370
379
|
/** Ensure `cwd` is inside a git repo, running `git init` if not. Returns the toplevel. */
|
|
371
380
|
declare function ensureGitRepo(cwd: string): Promise<string>;
|
|
381
|
+
interface FileTimestamps {
|
|
382
|
+
/** ISO-8601 creation date — git first-add commit, else filesystem birthtime. */
|
|
383
|
+
created?: string;
|
|
384
|
+
/** ISO-8601 update date — git last commit, else filesystem mtime. */
|
|
385
|
+
updated?: string;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Creation/update timestamps for a file, preferring git history (stable across
|
|
389
|
+
* clones and worktrees) and falling back to filesystem stat. Git created date is
|
|
390
|
+
* the earliest add-commit (`--follow` tracks renames); updated is the latest
|
|
391
|
+
* commit touching the file. When git yields nothing (untracked / no repo), uses
|
|
392
|
+
* birthtime/mtime. Never throws — returns `{}` if even stat fails.
|
|
393
|
+
*
|
|
394
|
+
* Results are memoised per absolute path (process-scoped) and the two git
|
|
395
|
+
* `log` execs run concurrently, since this runs in the render request path.
|
|
396
|
+
*/
|
|
397
|
+
declare function fileTimestamps(absPath: string): Promise<FileTimestamps>;
|
|
372
398
|
|
|
373
399
|
/** Map core article nodes to UI nav nodes, assigning each its route path. */
|
|
374
400
|
declare function toTreeNodes(coll: string, nodes: ArticleNode[]): TreeNode[];
|
|
@@ -577,6 +603,19 @@ declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
|
|
|
577
603
|
*/
|
|
578
604
|
declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
|
|
579
605
|
|
|
606
|
+
/** Route context for an article being rendered — used to resolve relative links. */
|
|
607
|
+
interface RenderContext {
|
|
608
|
+
/** Collection name (first route segment). */
|
|
609
|
+
coll: string;
|
|
610
|
+
/** Article route slug (path within the collection, no extension), e.g. "a/b/deep". */
|
|
611
|
+
slug: string;
|
|
612
|
+
/**
|
|
613
|
+
* True when the source file is a folder-index page (`<slug>/index.md`). Such a
|
|
614
|
+
* page shares its route slug with the folder, so relative links must resolve
|
|
615
|
+
* against the folder itself, not its parent. Defaults to false (leaf page).
|
|
616
|
+
*/
|
|
617
|
+
isIndex?: boolean;
|
|
618
|
+
}
|
|
580
619
|
/** The result of rendering an article body to HTML. */
|
|
581
620
|
interface RenderedArticle {
|
|
582
621
|
/** Fully-rendered, Shiki-highlighted HTML — mounted directly on the client (no eval). */
|
|
@@ -586,12 +625,13 @@ interface RenderedArticle {
|
|
|
586
625
|
}
|
|
587
626
|
/**
|
|
588
627
|
* Render an article body (markdown) to Shiki-highlighted HTML plus its table of
|
|
589
|
-
* contents
|
|
628
|
+
* contents, rewriting relative links to in-app routes via `ctx`.
|
|
629
|
+
* Runs entirely server-side; the client mounts the HTML directly with
|
|
590
630
|
* no `eval`/`new Function`, so it works under a strict CSP. Code is highlighted
|
|
591
631
|
* here (dual github-light/github-dark via CSS variables); mermaid fences are
|
|
592
632
|
* marked for client rendering.
|
|
593
633
|
*/
|
|
594
|
-
declare function renderArticle(body: string): Promise<RenderedArticle>;
|
|
634
|
+
declare function renderArticle(body: string, ctx: RenderContext): Promise<RenderedArticle>;
|
|
595
635
|
|
|
596
636
|
/**
|
|
597
637
|
* Host-side plugin invocation and search-provider loading.
|
|
@@ -762,4 +802,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
|
|
|
762
802
|
/** Find a plugin record by name (regardless of enabled state). */
|
|
763
803
|
declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
|
|
764
804
|
|
|
765
|
-
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type ListingEntry, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
|
805
|
+
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type ListingEntry, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ interface ArticleContent {
|
|
|
60
60
|
body: string;
|
|
61
61
|
/** Absolute path to the file on disk. */
|
|
62
62
|
path: string;
|
|
63
|
+
/**
|
|
64
|
+
* True when the resolved file is a folder-index page (`<slug>/index.md` or
|
|
65
|
+
* `<slug>/README.md`) rather than a leaf `<slug>.md`. The route slug is the
|
|
66
|
+
* same for both, so this flag is needed to resolve folder-local relative
|
|
67
|
+
* links against the article's own folder.
|
|
68
|
+
*/
|
|
69
|
+
isIndex: boolean;
|
|
63
70
|
}
|
|
64
71
|
type PluginKind = 'search';
|
|
65
72
|
declare const NATIVE_ENGINE = "native";
|
|
@@ -181,6 +188,10 @@ interface DocData {
|
|
|
181
188
|
html: string;
|
|
182
189
|
toc: TocItem[];
|
|
183
190
|
path: string;
|
|
191
|
+
/** ISO-8601 creation date, sourced from git history / filesystem (not frontmatter). */
|
|
192
|
+
created?: string;
|
|
193
|
+
/** ISO-8601 last-update date, sourced from git history / filesystem (not frontmatter). */
|
|
194
|
+
updated?: string;
|
|
184
195
|
}
|
|
185
196
|
interface ListingEntry {
|
|
186
197
|
name: string;
|
|
@@ -261,8 +272,6 @@ interface ParsedArticle {
|
|
|
261
272
|
interface SerializableArticle {
|
|
262
273
|
title: string;
|
|
263
274
|
tags: string[];
|
|
264
|
-
created?: string;
|
|
265
|
-
updated?: string;
|
|
266
275
|
body: string;
|
|
267
276
|
}
|
|
268
277
|
declare function parseArticle(raw: string): ParsedArticle;
|
|
@@ -369,6 +378,23 @@ interface UpstreamStatus {
|
|
|
369
378
|
declare function upstreamStatus(cwd: string): Promise<UpstreamStatus>;
|
|
370
379
|
/** Ensure `cwd` is inside a git repo, running `git init` if not. Returns the toplevel. */
|
|
371
380
|
declare function ensureGitRepo(cwd: string): Promise<string>;
|
|
381
|
+
interface FileTimestamps {
|
|
382
|
+
/** ISO-8601 creation date — git first-add commit, else filesystem birthtime. */
|
|
383
|
+
created?: string;
|
|
384
|
+
/** ISO-8601 update date — git last commit, else filesystem mtime. */
|
|
385
|
+
updated?: string;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Creation/update timestamps for a file, preferring git history (stable across
|
|
389
|
+
* clones and worktrees) and falling back to filesystem stat. Git created date is
|
|
390
|
+
* the earliest add-commit (`--follow` tracks renames); updated is the latest
|
|
391
|
+
* commit touching the file. When git yields nothing (untracked / no repo), uses
|
|
392
|
+
* birthtime/mtime. Never throws — returns `{}` if even stat fails.
|
|
393
|
+
*
|
|
394
|
+
* Results are memoised per absolute path (process-scoped) and the two git
|
|
395
|
+
* `log` execs run concurrently, since this runs in the render request path.
|
|
396
|
+
*/
|
|
397
|
+
declare function fileTimestamps(absPath: string): Promise<FileTimestamps>;
|
|
372
398
|
|
|
373
399
|
/** Map core article nodes to UI nav nodes, assigning each its route path. */
|
|
374
400
|
declare function toTreeNodes(coll: string, nodes: ArticleNode[]): TreeNode[];
|
|
@@ -577,6 +603,19 @@ declare function listArticleTree(coll: string): Promise<ArticleNode[]>;
|
|
|
577
603
|
*/
|
|
578
604
|
declare function readArticle(coll: string, slug: string): Promise<ArticleContent>;
|
|
579
605
|
|
|
606
|
+
/** Route context for an article being rendered — used to resolve relative links. */
|
|
607
|
+
interface RenderContext {
|
|
608
|
+
/** Collection name (first route segment). */
|
|
609
|
+
coll: string;
|
|
610
|
+
/** Article route slug (path within the collection, no extension), e.g. "a/b/deep". */
|
|
611
|
+
slug: string;
|
|
612
|
+
/**
|
|
613
|
+
* True when the source file is a folder-index page (`<slug>/index.md`). Such a
|
|
614
|
+
* page shares its route slug with the folder, so relative links must resolve
|
|
615
|
+
* against the folder itself, not its parent. Defaults to false (leaf page).
|
|
616
|
+
*/
|
|
617
|
+
isIndex?: boolean;
|
|
618
|
+
}
|
|
580
619
|
/** The result of rendering an article body to HTML. */
|
|
581
620
|
interface RenderedArticle {
|
|
582
621
|
/** Fully-rendered, Shiki-highlighted HTML — mounted directly on the client (no eval). */
|
|
@@ -586,12 +625,13 @@ interface RenderedArticle {
|
|
|
586
625
|
}
|
|
587
626
|
/**
|
|
588
627
|
* Render an article body (markdown) to Shiki-highlighted HTML plus its table of
|
|
589
|
-
* contents
|
|
628
|
+
* contents, rewriting relative links to in-app routes via `ctx`.
|
|
629
|
+
* Runs entirely server-side; the client mounts the HTML directly with
|
|
590
630
|
* no `eval`/`new Function`, so it works under a strict CSP. Code is highlighted
|
|
591
631
|
* here (dual github-light/github-dark via CSS variables); mermaid fences are
|
|
592
632
|
* marked for client rendering.
|
|
593
633
|
*/
|
|
594
|
-
declare function renderArticle(body: string): Promise<RenderedArticle>;
|
|
634
|
+
declare function renderArticle(body: string, ctx: RenderContext): Promise<RenderedArticle>;
|
|
595
635
|
|
|
596
636
|
/**
|
|
597
637
|
* Host-side plugin invocation and search-provider loading.
|
|
@@ -762,4 +802,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
|
|
|
762
802
|
/** Find a plugin record by name (regardless of enabled state). */
|
|
763
803
|
declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
|
|
764
804
|
|
|
765
|
-
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type ListingEntry, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
|
805
|
+
export { type AddExternalOptions, type Article, type ArticleContent, type ArticleNode, ArticleNotFoundError, CollectionAlreadyAddedError, type CollectionEntry, CollectionExistsError, type CollectionGitStatus, type CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, type CollectionTree, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type DiffwikiLogLevel, type DocData, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, type ListingEntry, NATIVE_ENGINE, NoDefaultCollectionError, type NodeMeta, type PageData, type ParsedArticle, type PluginCapabilities, PluginError, type PluginEvent, type PluginHealth, type PluginKind, type PluginReadiness, type PluginRecord, type PluginRegistryFile, type QueryHit, type QueryOptions, REPO_CONFIG_FILE, type Registry, type RenderContext, type RenderedArticle, type RepoConfig, SearchDoc, type SearchMode, type SearchOutcome, type SearchProvider, type SerializableArticle, type SetupOutcome, type SetupStep, type TocItem, type TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildNavTree, buildSearchIndex, collectionDir, collectionGitStatus, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, isLinkedWorktree, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, loadSearchProvider, nativeSearch, parseAddTarget, parseArticle, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readPluginRegistry, readRegistry, readRepoConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveHome, resolveLogLevel, resolvePage, search, serializeArticle, setConfigValue, setTags, slugify, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
|
package/dist/index.js
CHANGED
|
@@ -139,8 +139,6 @@ function parseArticle(raw) {
|
|
|
139
139
|
}
|
|
140
140
|
function serializeArticle(article) {
|
|
141
141
|
const front = { title: article.title, tags: article.tags };
|
|
142
|
-
if (article.created) front.created = article.created;
|
|
143
|
-
if (article.updated) front.updated = article.updated;
|
|
144
142
|
return matter.stringify(article.body, front, MATTER_OPTS);
|
|
145
143
|
}
|
|
146
144
|
|
|
@@ -195,11 +193,10 @@ async function createArticle(opts) {
|
|
|
195
193
|
const { collection, title } = parseAddTarget(opts.target);
|
|
196
194
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
197
195
|
const filePath = path.join(dir, `${slugify(title)}.md`);
|
|
198
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
196
|
const body = opts.body ?? `# ${title}
|
|
200
197
|
`;
|
|
201
198
|
await fs2.mkdir(dir, { recursive: true });
|
|
202
|
-
await fs2.writeFile(filePath, serializeArticle({ title, tags: opts.tags,
|
|
199
|
+
await fs2.writeFile(filePath, serializeArticle({ title, tags: opts.tags, body }), "utf8");
|
|
203
200
|
void fireHook("article-created", name, path.relative(dir, filePath));
|
|
204
201
|
return { title, tags: opts.tags, path: filePath, collection: name };
|
|
205
202
|
}
|
|
@@ -217,17 +214,7 @@ async function loadArticle(target) {
|
|
|
217
214
|
async function updateArticleBody(target, body) {
|
|
218
215
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
219
216
|
const parsed = parseArticle(raw);
|
|
220
|
-
await fs2.writeFile(
|
|
221
|
-
filePath,
|
|
222
|
-
serializeArticle({
|
|
223
|
-
title: parsed.title,
|
|
224
|
-
tags: parsed.tags,
|
|
225
|
-
created: parsed.created,
|
|
226
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
227
|
-
body
|
|
228
|
-
}),
|
|
229
|
-
"utf8"
|
|
230
|
-
);
|
|
217
|
+
await fs2.writeFile(filePath, serializeArticle({ title: parsed.title, tags: parsed.tags, body }), "utf8");
|
|
231
218
|
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
232
219
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
233
220
|
}
|
|
@@ -235,17 +222,7 @@ async function mutateTags(target, fn) {
|
|
|
235
222
|
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
236
223
|
const parsed = parseArticle(raw);
|
|
237
224
|
const tags = fn(parsed.tags);
|
|
238
|
-
await fs2.writeFile(
|
|
239
|
-
filePath,
|
|
240
|
-
serializeArticle({
|
|
241
|
-
title: parsed.title,
|
|
242
|
-
tags,
|
|
243
|
-
created: parsed.created,
|
|
244
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
|
-
body: parsed.body
|
|
246
|
-
}),
|
|
247
|
-
"utf8"
|
|
248
|
-
);
|
|
225
|
+
await fs2.writeFile(filePath, serializeArticle({ title: parsed.title, tags, body: parsed.body }), "utf8");
|
|
249
226
|
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
250
227
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
251
228
|
}
|
|
@@ -267,13 +244,14 @@ async function removeArticle(target) {
|
|
|
267
244
|
}
|
|
268
245
|
|
|
269
246
|
// src/repo.ts
|
|
270
|
-
import
|
|
247
|
+
import fs4 from "fs/promises";
|
|
271
248
|
import path3 from "path";
|
|
272
249
|
import { execFile as execFile2 } from "child_process";
|
|
273
250
|
import { promisify as promisify2 } from "util";
|
|
274
251
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
275
252
|
|
|
276
253
|
// src/git.ts
|
|
254
|
+
import fs3 from "fs/promises";
|
|
277
255
|
import path2 from "path";
|
|
278
256
|
import { execFile } from "child_process";
|
|
279
257
|
import { promisify } from "util";
|
|
@@ -335,6 +313,35 @@ async function ensureGitRepo(cwd) {
|
|
|
335
313
|
if (!after) throw new Error("git init did not produce a repository");
|
|
336
314
|
return after;
|
|
337
315
|
}
|
|
316
|
+
function isEpochish(d) {
|
|
317
|
+
return !Number.isFinite(d.getTime()) || d.getTime() <= 0;
|
|
318
|
+
}
|
|
319
|
+
var timestampCache = /* @__PURE__ */ new Map();
|
|
320
|
+
async function fileTimestamps(absPath) {
|
|
321
|
+
const cached = timestampCache.get(absPath);
|
|
322
|
+
if (cached) return cached;
|
|
323
|
+
const dir = path2.dirname(absPath);
|
|
324
|
+
const base = path2.basename(absPath);
|
|
325
|
+
const [addLog, lastLog] = await Promise.all([
|
|
326
|
+
git(dir, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", base]),
|
|
327
|
+
git(dir, ["log", "-1", "--format=%aI", "--", base])
|
|
328
|
+
]);
|
|
329
|
+
const gitCreated = addLog ? addLog.split("\n").filter(Boolean).pop() : void 0;
|
|
330
|
+
const gitUpdated = lastLog || void 0;
|
|
331
|
+
let created = gitCreated;
|
|
332
|
+
let updated = gitUpdated;
|
|
333
|
+
if (!created || !updated) {
|
|
334
|
+
try {
|
|
335
|
+
const st = await fs3.stat(absPath);
|
|
336
|
+
if (!updated) updated = st.mtime.toISOString();
|
|
337
|
+
if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const result = { created, updated };
|
|
342
|
+
timestampCache.set(absPath, result);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
338
345
|
|
|
339
346
|
// src/repo.ts
|
|
340
347
|
var execFileAsync2 = promisify2(execFile2);
|
|
@@ -349,7 +356,7 @@ async function detectRepoName(cwd) {
|
|
|
349
356
|
}
|
|
350
357
|
async function readRepoConfig(cwd) {
|
|
351
358
|
try {
|
|
352
|
-
const raw = await
|
|
359
|
+
const raw = await fs4.readFile(path3.join(cwd, REPO_CONFIG_FILE), "utf8");
|
|
353
360
|
const parsed = parseYaml2(raw) ?? {};
|
|
354
361
|
if (!parsed.path) return void 0;
|
|
355
362
|
return {
|
|
@@ -379,9 +386,9 @@ async function initRepoWiki(opts) {
|
|
|
379
386
|
n += 1;
|
|
380
387
|
}
|
|
381
388
|
}
|
|
382
|
-
await
|
|
389
|
+
await fs4.mkdir(absWiki, { recursive: true });
|
|
383
390
|
const config = { collection: name, path: wikiPath };
|
|
384
|
-
await
|
|
391
|
+
await fs4.writeFile(path3.join(cwd, REPO_CONFIG_FILE), stringifyYaml2(config), "utf8");
|
|
385
392
|
const entry = {
|
|
386
393
|
name,
|
|
387
394
|
type: "repo",
|
|
@@ -450,7 +457,7 @@ async function collectionGitStatus(entry) {
|
|
|
450
457
|
}
|
|
451
458
|
|
|
452
459
|
// src/browse.ts
|
|
453
|
-
import
|
|
460
|
+
import fs5 from "fs/promises";
|
|
454
461
|
import path4 from "path";
|
|
455
462
|
var INDEX_RE = /^(index|readme)\.mdx?$/i;
|
|
456
463
|
function buildArticleTree(relPaths) {
|
|
@@ -495,7 +502,7 @@ function buildArticleTree(relPaths) {
|
|
|
495
502
|
async function collectRelPaths(dir, rel = "") {
|
|
496
503
|
let dirents;
|
|
497
504
|
try {
|
|
498
|
-
dirents = await
|
|
505
|
+
dirents = await fs5.readdir(dir, { withFileTypes: true });
|
|
499
506
|
} catch {
|
|
500
507
|
return [];
|
|
501
508
|
}
|
|
@@ -513,7 +520,7 @@ async function collectRelPaths(dir, rel = "") {
|
|
|
513
520
|
}
|
|
514
521
|
async function readdirSafe(dir) {
|
|
515
522
|
try {
|
|
516
|
-
return await
|
|
523
|
+
return await fs5.readdir(dir);
|
|
517
524
|
} catch {
|
|
518
525
|
return [];
|
|
519
526
|
}
|
|
@@ -530,7 +537,7 @@ function pickIndexFile(names) {
|
|
|
530
537
|
async function cheapTitle(absPath) {
|
|
531
538
|
let handle;
|
|
532
539
|
try {
|
|
533
|
-
handle = await
|
|
540
|
+
handle = await fs5.open(absPath, "r");
|
|
534
541
|
const buf = Buffer.alloc(512);
|
|
535
542
|
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
536
543
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -580,11 +587,12 @@ async function readArticle(coll, slug) {
|
|
|
580
587
|
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
581
588
|
const inJail = (p) => p === collDir || p.startsWith(collDir + path4.sep);
|
|
582
589
|
let abs;
|
|
590
|
+
let isIndex = false;
|
|
583
591
|
for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
|
|
584
592
|
const candidate = path4.resolve(collDir, rel);
|
|
585
593
|
if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
|
|
586
594
|
try {
|
|
587
|
-
await
|
|
595
|
+
await fs5.access(candidate);
|
|
588
596
|
abs = candidate;
|
|
589
597
|
break;
|
|
590
598
|
} catch {
|
|
@@ -594,16 +602,20 @@ async function readArticle(coll, slug) {
|
|
|
594
602
|
const dir = path4.resolve(collDir, safeSlug);
|
|
595
603
|
if (inJail(dir)) {
|
|
596
604
|
const idx = pickIndexFile(await readdirSafe(dir));
|
|
597
|
-
if (idx)
|
|
605
|
+
if (idx) {
|
|
606
|
+
abs = path4.join(dir, idx);
|
|
607
|
+
isIndex = true;
|
|
608
|
+
}
|
|
598
609
|
}
|
|
599
610
|
}
|
|
600
611
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
601
|
-
const parsed = parseArticle(await
|
|
612
|
+
const parsed = parseArticle(await fs5.readFile(abs, "utf8"));
|
|
602
613
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
603
|
-
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
614
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs, isIndex };
|
|
604
615
|
}
|
|
605
616
|
|
|
606
617
|
// src/render.ts
|
|
618
|
+
import path5 from "path";
|
|
607
619
|
import { unified } from "unified";
|
|
608
620
|
import remarkParse from "remark-parse";
|
|
609
621
|
import remarkGfm from "remark-gfm";
|
|
@@ -613,6 +625,36 @@ import rehypeShiki from "@shikijs/rehype";
|
|
|
613
625
|
import rehypeStringify from "rehype-stringify";
|
|
614
626
|
import { visit, SKIP } from "unist-util-visit";
|
|
615
627
|
import { toString as hastToString } from "hast-util-to-string";
|
|
628
|
+
function isNonRelativeHref(href) {
|
|
629
|
+
if (href === "") return true;
|
|
630
|
+
if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return true;
|
|
631
|
+
if (href.startsWith("//")) return true;
|
|
632
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
633
|
+
}
|
|
634
|
+
function rehypeRewriteLinks(ctx) {
|
|
635
|
+
const slugDir = ctx.isIndex ? ctx.slug || "." : path5.posix.dirname(ctx.slug || ".");
|
|
636
|
+
return (tree) => {
|
|
637
|
+
visit(tree, "element", (node) => {
|
|
638
|
+
if (node.tagName !== "a") return;
|
|
639
|
+
const raw = node.properties?.href;
|
|
640
|
+
if (typeof raw !== "string" || isNonRelativeHref(raw)) return;
|
|
641
|
+
const suffixMatch = /[#?].*$/.exec(raw);
|
|
642
|
+
const suffix = suffixMatch ? suffixMatch[0] : "";
|
|
643
|
+
const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
|
|
644
|
+
if (pathPart === "") return;
|
|
645
|
+
const joined = path5.posix.join(slugDir === "." ? "" : slugDir, pathPart);
|
|
646
|
+
const normalized = path5.posix.normalize(joined);
|
|
647
|
+
if (normalized.startsWith("..")) return;
|
|
648
|
+
let resolved = normalized.replace(/\.(mdx?)$/i, "");
|
|
649
|
+
resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
|
|
650
|
+
resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
651
|
+
node.properties = {
|
|
652
|
+
...node.properties,
|
|
653
|
+
href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
};
|
|
657
|
+
}
|
|
616
658
|
var LANG_ALIASES = {
|
|
617
659
|
"c#": "csharp",
|
|
618
660
|
cs: "csharp",
|
|
@@ -656,9 +698,9 @@ function rehypeMarkMermaid() {
|
|
|
656
698
|
});
|
|
657
699
|
};
|
|
658
700
|
}
|
|
659
|
-
async function renderArticle(body) {
|
|
701
|
+
async function renderArticle(body, ctx) {
|
|
660
702
|
const toc = [];
|
|
661
|
-
const file = await unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSlug).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeShiki, {
|
|
703
|
+
const file = await unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSlug).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(rehypeRewriteLinks, ctx).use(rehypeShiki, {
|
|
662
704
|
themes: { light: "github-light", dark: "github-dark" },
|
|
663
705
|
defaultColor: false,
|
|
664
706
|
langAlias: LANG_ALIASES,
|
|
@@ -720,7 +762,12 @@ function findNode(nodes, slug) {
|
|
|
720
762
|
async function resolvePage(coll, slug) {
|
|
721
763
|
try {
|
|
722
764
|
const art = await readArticle(coll, slug);
|
|
723
|
-
const { html, toc } = await renderArticle(art.body
|
|
765
|
+
const { html, toc } = await renderArticle(art.body, {
|
|
766
|
+
coll: art.coll,
|
|
767
|
+
slug: art.slug,
|
|
768
|
+
isIndex: art.isIndex
|
|
769
|
+
});
|
|
770
|
+
const { created, updated } = await fileTimestamps(art.path);
|
|
724
771
|
return {
|
|
725
772
|
kind: "doc",
|
|
726
773
|
coll: art.coll,
|
|
@@ -729,7 +776,9 @@ async function resolvePage(coll, slug) {
|
|
|
729
776
|
tags: art.tags,
|
|
730
777
|
html,
|
|
731
778
|
toc,
|
|
732
|
-
path: `/${art.coll}/${art.slug}
|
|
779
|
+
path: `/${art.coll}/${art.slug}`,
|
|
780
|
+
created,
|
|
781
|
+
updated
|
|
733
782
|
};
|
|
734
783
|
} catch (err) {
|
|
735
784
|
if (err instanceof CollectionNotFoundError) return null;
|
|
@@ -787,19 +836,19 @@ function resolveCollectionSelection(all, sel = {}) {
|
|
|
787
836
|
}
|
|
788
837
|
|
|
789
838
|
// src/query.ts
|
|
790
|
-
import
|
|
791
|
-
import
|
|
839
|
+
import fs6 from "fs/promises";
|
|
840
|
+
import path6 from "path";
|
|
792
841
|
var log2 = getLogger("query");
|
|
793
842
|
async function collectMarkdownFiles(dir) {
|
|
794
843
|
let entries;
|
|
795
844
|
try {
|
|
796
|
-
entries = await
|
|
845
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
797
846
|
} catch {
|
|
798
847
|
return [];
|
|
799
848
|
}
|
|
800
849
|
const files = [];
|
|
801
850
|
for (const entry of entries) {
|
|
802
|
-
const full =
|
|
851
|
+
const full = path6.join(dir, entry.name);
|
|
803
852
|
if (entry.isDirectory()) {
|
|
804
853
|
files.push(...await collectMarkdownFiles(full));
|
|
805
854
|
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
@@ -818,12 +867,12 @@ async function nativeSearch(term, collections, collection) {
|
|
|
818
867
|
const docs = [];
|
|
819
868
|
for (const entry of targets) {
|
|
820
869
|
for (const full of await collectMarkdownFiles(entry.path)) {
|
|
821
|
-
const fallbackTitle =
|
|
870
|
+
const fallbackTitle = path6.basename(full).replace(/\.mdx?$/, "");
|
|
822
871
|
let title = fallbackTitle;
|
|
823
872
|
let tags = [];
|
|
824
873
|
let body = "";
|
|
825
874
|
try {
|
|
826
|
-
const parsed = parseArticle(await
|
|
875
|
+
const parsed = parseArticle(await fs6.readFile(full, "utf8"));
|
|
827
876
|
if (parsed.title) title = parsed.title;
|
|
828
877
|
tags = parsed.tags;
|
|
829
878
|
body = parsed.body;
|
|
@@ -906,12 +955,12 @@ async function query(term, opts) {
|
|
|
906
955
|
}
|
|
907
956
|
|
|
908
957
|
// src/search.ts
|
|
909
|
-
import
|
|
910
|
-
import
|
|
958
|
+
import fs7 from "fs/promises";
|
|
959
|
+
import path7 from "path";
|
|
911
960
|
async function readArticleParts(collDir, slug) {
|
|
912
961
|
for (const ext of [".mdx", ".md"]) {
|
|
913
962
|
try {
|
|
914
|
-
const parsed = parseArticle(await
|
|
963
|
+
const parsed = parseArticle(await fs7.readFile(path7.join(collDir, `${slug}${ext}`), "utf8"));
|
|
915
964
|
return { tags: parsed.tags, body: parsed.body };
|
|
916
965
|
} catch {
|
|
917
966
|
}
|
|
@@ -945,7 +994,7 @@ async function buildSearchIndex(collection) {
|
|
|
945
994
|
for (const e of entries) {
|
|
946
995
|
try {
|
|
947
996
|
const collEntry = await findCollection(e.name);
|
|
948
|
-
const collDir =
|
|
997
|
+
const collDir = path7.resolve(collEntry?.path ?? e.path);
|
|
949
998
|
await flatten(e.name, collDir, await listArticleTree(e.name), out);
|
|
950
999
|
} catch {
|
|
951
1000
|
}
|
|
@@ -954,15 +1003,15 @@ async function buildSearchIndex(collection) {
|
|
|
954
1003
|
}
|
|
955
1004
|
|
|
956
1005
|
// src/doctor.ts
|
|
957
|
-
import
|
|
1006
|
+
import fs9 from "fs/promises";
|
|
958
1007
|
|
|
959
1008
|
// src/plugins/manager.ts
|
|
960
|
-
import
|
|
1009
|
+
import fs8 from "fs/promises";
|
|
961
1010
|
import os from "os";
|
|
962
|
-
import
|
|
1011
|
+
import path8 from "path";
|
|
963
1012
|
async function readPackageJson(file) {
|
|
964
1013
|
try {
|
|
965
|
-
return JSON.parse(await
|
|
1014
|
+
return JSON.parse(await fs8.readFile(file, "utf8"));
|
|
966
1015
|
} catch {
|
|
967
1016
|
return null;
|
|
968
1017
|
}
|
|
@@ -978,7 +1027,7 @@ function binOf(manifest) {
|
|
|
978
1027
|
async function pluginManifestDirs(nodeModules) {
|
|
979
1028
|
let entries;
|
|
980
1029
|
try {
|
|
981
|
-
entries = await
|
|
1030
|
+
entries = await fs8.readdir(nodeModules);
|
|
982
1031
|
} catch {
|
|
983
1032
|
return [];
|
|
984
1033
|
}
|
|
@@ -988,44 +1037,44 @@ async function pluginManifestDirs(nodeModules) {
|
|
|
988
1037
|
if (entry.startsWith("@")) {
|
|
989
1038
|
let scoped;
|
|
990
1039
|
try {
|
|
991
|
-
scoped = await
|
|
1040
|
+
scoped = await fs8.readdir(path8.join(nodeModules, entry));
|
|
992
1041
|
} catch {
|
|
993
1042
|
continue;
|
|
994
1043
|
}
|
|
995
1044
|
for (const pkg of scoped) {
|
|
996
1045
|
if (pkg.startsWith(".")) continue;
|
|
997
|
-
dirs.push(
|
|
1046
|
+
dirs.push(path8.join(nodeModules, entry, pkg));
|
|
998
1047
|
}
|
|
999
1048
|
} else {
|
|
1000
|
-
dirs.push(
|
|
1049
|
+
dirs.push(path8.join(nodeModules, entry));
|
|
1001
1050
|
}
|
|
1002
1051
|
}
|
|
1003
1052
|
return dirs;
|
|
1004
1053
|
}
|
|
1005
1054
|
async function ensurePluginRoot() {
|
|
1006
1055
|
const dir = pluginsDir();
|
|
1007
|
-
await
|
|
1008
|
-
const pkgPath =
|
|
1056
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
1057
|
+
const pkgPath = path8.join(dir, "package.json");
|
|
1009
1058
|
if (await readPackageJson(pkgPath) === null) {
|
|
1010
1059
|
const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
|
|
1011
|
-
await
|
|
1060
|
+
await fs8.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1012
1061
|
`, "utf8");
|
|
1013
1062
|
}
|
|
1014
1063
|
return dir;
|
|
1015
1064
|
}
|
|
1016
1065
|
async function asSearchPackage(pkgDir) {
|
|
1017
|
-
const manifest = await readPackageJson(
|
|
1066
|
+
const manifest = await readPackageJson(path8.join(pkgDir, "package.json"));
|
|
1018
1067
|
if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
|
|
1019
1068
|
const bin = binOf(manifest);
|
|
1020
1069
|
if (!bin) return null;
|
|
1021
|
-
return { name: manifest.name, version: manifest.version, binAbs:
|
|
1070
|
+
return { name: manifest.name, version: manifest.version, binAbs: path8.resolve(pkgDir, bin) };
|
|
1022
1071
|
}
|
|
1023
1072
|
async function packageNameFromSpec(spec) {
|
|
1024
1073
|
const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
|
|
1025
1074
|
if (looksLikePath) {
|
|
1026
1075
|
const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
|
|
1027
|
-
const abs =
|
|
1028
|
-
return (await readPackageJson(
|
|
1076
|
+
const abs = path8.resolve(raw.replace(/^~(?=$|\/)/, os.homedir()));
|
|
1077
|
+
return (await readPackageJson(path8.join(abs, "package.json")))?.name ?? null;
|
|
1029
1078
|
}
|
|
1030
1079
|
if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
|
|
1031
1080
|
if (spec.startsWith("@")) {
|
|
@@ -1036,10 +1085,10 @@ async function packageNameFromSpec(spec) {
|
|
|
1036
1085
|
return at <= 0 ? spec : spec.slice(0, at);
|
|
1037
1086
|
}
|
|
1038
1087
|
async function resolveSearchPackage(dir, name) {
|
|
1039
|
-
return asSearchPackage(
|
|
1088
|
+
return asSearchPackage(path8.join(dir, "node_modules", ...name.split("/")));
|
|
1040
1089
|
}
|
|
1041
1090
|
async function discoverSearchPackage(dir) {
|
|
1042
|
-
const nodeModules =
|
|
1091
|
+
const nodeModules = path8.join(dir, "node_modules");
|
|
1043
1092
|
for (const pkgDir of await pluginManifestDirs(nodeModules)) {
|
|
1044
1093
|
const found = await asSearchPackage(pkgDir);
|
|
1045
1094
|
if (found) return found;
|
|
@@ -1169,7 +1218,7 @@ async function listPlugins() {
|
|
|
1169
1218
|
// src/doctor.ts
|
|
1170
1219
|
async function exists(target) {
|
|
1171
1220
|
try {
|
|
1172
|
-
await
|
|
1221
|
+
await fs9.access(target);
|
|
1173
1222
|
return true;
|
|
1174
1223
|
} catch {
|
|
1175
1224
|
return false;
|
|
@@ -1325,6 +1374,7 @@ export {
|
|
|
1325
1374
|
emitPluginEvent,
|
|
1326
1375
|
ensureGitRepo,
|
|
1327
1376
|
ensurePluginRoot,
|
|
1377
|
+
fileTimestamps,
|
|
1328
1378
|
findCollection,
|
|
1329
1379
|
findCollectionById,
|
|
1330
1380
|
findEnabledPlugin,
|