@riebeckite/core 0.0.26 → 0.0.28

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.js CHANGED
@@ -826,7 +826,7 @@ function getFileName(filePath) {
826
826
  }
827
827
 
828
828
  // src/content/content_build_state.ts
829
- var CONTENT_BUILD_STATE_VERSION = 16;
829
+ var CONTENT_BUILD_STATE_VERSION = 19;
830
830
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
831
831
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
832
832
 
@@ -922,9 +922,12 @@ function isContentBuildStateShape(value) {
922
922
  }
923
923
  function isManifestHookCache(value) {
924
924
  return Array.isArray(value) && value.every(
925
- (entry) => isRecord2(entry) && typeof entry.pluginName === "string" && typeof entry.cacheable === "boolean" && (entry.pipelineFingerprint === void 0 || typeof entry.pipelineFingerprint === "string") && typeof entry.inputFingerprint === "string" && typeof entry.outputFingerprint === "string" && isRecord2(entry.snapshot) && Array.isArray(entry.snapshot.entries)
925
+ (entry) => isRecord2(entry) && typeof entry.pluginName === "string" && typeof entry.cacheable === "boolean" && (entry.pipelineFingerprint === void 0 || typeof entry.pipelineFingerprint === "string") && typeof entry.inputFingerprint === "string" && (entry.outputFingerprint === void 0 || typeof entry.outputFingerprint === "string") && (entry.snapshot === void 0 || isManifestHookSnapshot(entry.snapshot))
926
926
  );
927
927
  }
928
+ function isManifestHookSnapshot(value) {
929
+ return isRecord2(value) && Array.isArray(value.entries) && (value.order === void 0 || Array.isArray(value.order) && value.order.every((slug) => typeof slug === "string"));
930
+ }
928
931
  function isOutputDescriptors(value) {
929
932
  return Array.isArray(value) && value.every(
930
933
  (output) => isRecord2(output) && (output.kind === "content" || output.kind === "redirect" || output.kind === "plugin-page" || output.kind === "generated") && isOutputPath(output.path) && typeof output.producer === "string" && Array.isArray(output.dependencies) && output.dependencies.every(isOutputDependency)
@@ -939,7 +942,10 @@ function isOutputPath(value) {
939
942
  function isOutputDependency(value) {
940
943
  if (!isRecord2(value) || typeof value.type !== "string") return false;
941
944
  if (value.type === "global" || value.type === "unknown") return true;
942
- return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string" || value.type === "file" && typeof value.path === "string" || value.type === "external" && typeof value.id === "string" || value.type === "projection" && (value.projection === "navigation" || value.projection === "recent-posts" || value.projection === "taxonomy-tags" || value.projection === "taxonomy-folders");
945
+ if (value.type === "content-query") {
946
+ return Array.isArray(value.properties) && value.properties.every((property) => typeof property === "string") && (value.unknown === void 0 || typeof value.unknown === "boolean");
947
+ }
948
+ return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string" || value.type === "file" && typeof value.path === "string" || value.type === "public-location" && typeof value.pathname === "string" || value.type === "external" && typeof value.id === "string" || value.type === "projection" && (value.projection === "navigation" || value.projection === "recent-posts" || value.projection === "taxonomy-tags" || value.projection === "taxonomy-folders");
943
949
  }
944
950
  function isExternalDependencies(value) {
945
951
  return Array.isArray(value) && value.every(
@@ -1291,15 +1297,8 @@ function normalizePath(value) {
1291
1297
 
1292
1298
  // src/content/content_fingerprint.ts
1293
1299
  import { createHash } from "node:crypto";
1294
- async function fingerprintContentEntries(entries, read) {
1295
- const sortedEntries = [...entries].sort(
1296
- (left, right) => left.path.localeCompare(right.path)
1297
- );
1298
- return await mapConcurrent(sortedEntries, 64, async (entry) => ({
1299
- entry,
1300
- fingerprint: await fingerprintContentEntry(entry, read)
1301
- }));
1302
- }
1300
+
1301
+ // src/utils/concurrency.ts
1303
1302
  async function mapConcurrent(values, concurrency, map) {
1304
1303
  const results = new Array(values.length);
1305
1304
  let nextIndex = 0;
@@ -1319,12 +1318,37 @@ async function mapConcurrent(values, concurrency, map) {
1319
1318
  );
1320
1319
  return results;
1321
1320
  }
1322
- async function fingerprintContentEntry(entry, read) {
1323
- if (entry.metadata?.hash) return `hash:${entry.metadata.hash}`;
1324
- if (entry.metadata?.etag) return `etag:${entry.metadata.etag}`;
1321
+
1322
+ // src/content/content_fingerprint.ts
1323
+ async function fingerprintContentEntries(entries, read, includeMetadata = true) {
1324
+ const sortedEntries = [...entries].sort(
1325
+ (left, right) => left.path.localeCompare(right.path)
1326
+ );
1327
+ return await mapConcurrent(sortedEntries, 64, async (entry) => ({
1328
+ entry,
1329
+ fingerprint: await fingerprintContentEntry(entry, read, includeMetadata)
1330
+ }));
1331
+ }
1332
+ async function fingerprintContentEntry(entry, read, includeMetadata) {
1333
+ const contentFingerprint = entry.metadata?.hash ? `hash:${entry.metadata.hash}` : entry.metadata?.etag ? `etag:${entry.metadata.etag}` : await fingerprintContent(entry, read);
1334
+ return includeMetadata ? `${contentFingerprint}:${fingerprintMetadata(entry)}` : contentFingerprint;
1335
+ }
1336
+ async function fingerprintContent(entry, read) {
1325
1337
  const content = await read(entry);
1326
1338
  return `sha256:${createHash("sha256").update(typeof content === "string" ? content : content).digest("hex")}`;
1327
1339
  }
1340
+ function fingerprintMetadata(entry) {
1341
+ const metadata = entry.metadata;
1342
+ if (!metadata) return "metadata:none";
1343
+ return JSON.stringify({
1344
+ createdAt: finiteMetadataValue(metadata.createdAt),
1345
+ modifiedAt: finiteMetadataValue(metadata.modifiedAt),
1346
+ size: finiteMetadataValue(metadata.size)
1347
+ });
1348
+ }
1349
+ function finiteMetadataValue(value) {
1350
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1351
+ }
1328
1352
 
1329
1353
  // src/utils/collections.ts
1330
1354
  function uniqueStrings(values) {
@@ -1391,6 +1415,48 @@ function selectEntriesByLanguage(entries, language) {
1391
1415
  return localized.filter((entry) => getEntryLanguage(entry) === language);
1392
1416
  }
1393
1417
 
1418
+ // src/content/content_links.ts
1419
+ import remarkParse from "remark-parse";
1420
+ import { unified } from "unified";
1421
+ var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
1422
+ function extractContentLinks(markdown, contentIndex) {
1423
+ WIKILINK_PATTERN.lastIndex = 0;
1424
+ const links = [];
1425
+ for (let match = WIKILINK_PATTERN.exec(markdown); match !== null; match = WIKILINK_PATTERN.exec(markdown)) {
1426
+ const rawTarget = match[2]?.trim();
1427
+ if (!rawTarget) continue;
1428
+ const resolved = contentIndex.get(rawTarget.toLowerCase()) ?? null;
1429
+ links.push({
1430
+ raw: rawTarget,
1431
+ slug: resolved,
1432
+ kind: resolved ? linkKind(resolved) : "unresolved",
1433
+ embed: match[1] === "!"
1434
+ });
1435
+ }
1436
+ return links;
1437
+ }
1438
+ function extractMarkdownLinkUrls(markdown) {
1439
+ const urls = [];
1440
+ visitMarkdownNodes(
1441
+ unified().use(remarkParse).parse(markdown),
1442
+ (node) => {
1443
+ if (node.type === "link" && typeof node.url === "string") {
1444
+ urls.push(node.url);
1445
+ }
1446
+ }
1447
+ );
1448
+ return urls;
1449
+ }
1450
+ function visitMarkdownNodes(node, visit) {
1451
+ visit(node);
1452
+ for (const child of node.children ?? []) visitMarkdownNodes(child, visit);
1453
+ }
1454
+ function linkKind(value) {
1455
+ if (isImagePath(value)) return "image";
1456
+ if (isAttachmentPath(value)) return "attachment";
1457
+ return "note";
1458
+ }
1459
+
1394
1460
  // src/content/content_location.ts
1395
1461
  function resolveDefaultContentLocation(content) {
1396
1462
  return {
@@ -1541,9 +1607,9 @@ import remarkDirective from "remark-directive";
1541
1607
  import remarkFrontmatter from "remark-frontmatter";
1542
1608
  import remarkGfm from "remark-gfm";
1543
1609
  import remarkMath from "remark-math";
1544
- import remarkParse2 from "remark-parse";
1610
+ import remarkParse3 from "remark-parse";
1545
1611
  import remarkRehype from "remark-rehype";
1546
- import { unified as unified2 } from "unified";
1612
+ import { unified as unified3 } from "unified";
1547
1613
  import { matter as matter2 } from "vfile-matter";
1548
1614
 
1549
1615
  // src/content/content_dependency_tracker.ts
@@ -1556,7 +1622,7 @@ function createContentDependencyTracker(source) {
1556
1622
  const normalized = normalizeDependencyIdentity(identity);
1557
1623
  dependencies.set(dependencyKey(normalized), {
1558
1624
  ...normalized,
1559
- fingerprint: fingerprintContent(content)
1625
+ fingerprint: fingerprintContent2(content)
1560
1626
  });
1561
1627
  };
1562
1628
  return {
@@ -1581,7 +1647,7 @@ function createContentDependencyTracker(source) {
1581
1647
  )
1582
1648
  };
1583
1649
  }
1584
- function fingerprintContent(content) {
1650
+ function fingerprintContent2(content) {
1585
1651
  return `sha256:${createHash2("sha256").update(typeof content === "string" ? content : content).digest("hex")}`;
1586
1652
  }
1587
1653
  function normalizeDependencyIdentity(dependency) {
@@ -1595,30 +1661,6 @@ function dependencyKey(dependency) {
1595
1661
  return `${normalized.kind}:${normalized.id}`;
1596
1662
  }
1597
1663
 
1598
- // src/content/content_links.ts
1599
- var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
1600
- function extractContentLinks(markdown, contentIndex) {
1601
- WIKILINK_PATTERN.lastIndex = 0;
1602
- const links = [];
1603
- for (let match = WIKILINK_PATTERN.exec(markdown); match !== null; match = WIKILINK_PATTERN.exec(markdown)) {
1604
- const rawTarget = match[2]?.trim();
1605
- if (!rawTarget) continue;
1606
- const resolved = contentIndex.get(rawTarget.toLowerCase()) ?? null;
1607
- links.push({
1608
- raw: rawTarget,
1609
- slug: resolved,
1610
- kind: resolved ? linkKind(resolved) : "unresolved",
1611
- embed: match[1] === "!"
1612
- });
1613
- }
1614
- return links;
1615
- }
1616
- function linkKind(value) {
1617
- if (isImagePath(value)) return "image";
1618
- if (isAttachmentPath(value)) return "attachment";
1619
- return "note";
1620
- }
1621
-
1622
1664
  // src/content/content_persistent_cache.ts
1623
1665
  import { createHash as createHash4 } from "node:crypto";
1624
1666
  import fs2 from "node:fs/promises";
@@ -2005,14 +2047,47 @@ function extractFrontmatter(markdown) {
2005
2047
  }
2006
2048
 
2007
2049
  // src/content/markdown_image.ts
2008
- import remarkParse from "remark-parse";
2009
- import { unified } from "unified";
2050
+ import remarkParse2 from "remark-parse";
2051
+ import { unified as unified2 } from "unified";
2052
+
2053
+ // src/utils/markdown.ts
2054
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2055
+ var FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
2056
+ function splitOnce(value, separator) {
2057
+ const index = value.indexOf(separator);
2058
+ if (index < 0) return [value];
2059
+ return [value.slice(0, index), value.slice(index + separator.length)];
2060
+ }
2061
+ function scanMarkdownFenceLines(source) {
2062
+ const lines = [];
2063
+ let fence = null;
2064
+ for (const value of source.split("\n")) {
2065
+ if (fence === null) {
2066
+ const open = FENCE_OPEN.exec(value);
2067
+ if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
2068
+ fence = { marker: open[1][0], length: open[1].length };
2069
+ }
2070
+ lines.push({ value, inFence: false });
2071
+ continue;
2072
+ }
2073
+ const close = FENCE_CLOSE.exec(value);
2074
+ if (close && close[1][0] === fence.marker && close[1].length >= fence.length) {
2075
+ fence = null;
2076
+ lines.push({ value, inFence: false });
2077
+ continue;
2078
+ }
2079
+ lines.push({ value, inFence: true });
2080
+ }
2081
+ return lines;
2082
+ }
2083
+
2084
+ // src/content/markdown_image.ts
2010
2085
  function extractMarkdownImages(markdown, sourceSlug) {
2011
- const tree = unified().use(remarkParse).parse(markdown);
2086
+ const tree = unified2().use(remarkParse2).parse(markdown);
2012
2087
  const definitions = /* @__PURE__ */ new Map();
2013
2088
  const references = [];
2014
2089
  const urls = [];
2015
- visitMarkdownNodes(tree, (node) => {
2090
+ visitMarkdownNodes2(tree, (node) => {
2016
2091
  if (node.type === "definition" && typeof node.url === "string") {
2017
2092
  const identifier = normalizeIdentifier(node.identifier);
2018
2093
  if (identifier) definitions.set(identifier, node.url);
@@ -2037,7 +2112,7 @@ function normalizeMarkdownImages(sourceSlug) {
2037
2112
  return () => (tree) => {
2038
2113
  const definitions = /* @__PURE__ */ new Map();
2039
2114
  const references = /* @__PURE__ */ new Set();
2040
- visitMarkdownNodes(tree, (node) => {
2115
+ visitMarkdownNodes2(tree, (node) => {
2041
2116
  if (node.type === "definition" && typeof node.url === "string") {
2042
2117
  const identifier = normalizeIdentifier(node.identifier);
2043
2118
  if (identifier) definitions.set(identifier, node);
@@ -2091,11 +2166,11 @@ function resolveContentAssetPath(sourceSlug, targetPath) {
2091
2166
  }
2092
2167
  return normalized.length > 0 ? normalized.join("/").normalize("NFC") : null;
2093
2168
  }
2094
- function visitMarkdownNodes(node, visitor) {
2169
+ function visitMarkdownNodes2(node, visitor) {
2095
2170
  const markdownNode = node;
2096
2171
  visitor(markdownNode);
2097
2172
  for (const child of markdownNode.children ?? [])
2098
- visitMarkdownNodes(child, visitor);
2173
+ visitMarkdownNodes2(child, visitor);
2099
2174
  }
2100
2175
  function normalizeIdentifier(value) {
2101
2176
  return typeof value === "string" ? value.toLowerCase() : null;
@@ -2107,114 +2182,264 @@ function decodeUriPath(path6) {
2107
2182
  return null;
2108
2183
  }
2109
2184
  }
2110
- function splitOnce(value, separator) {
2111
- const index = value.indexOf(separator);
2112
- return index < 0 ? [value] : [value.slice(0, index), value.slice(index + 1)];
2113
- }
2114
2185
  function uniqueImages(images) {
2115
2186
  return [...new Map(images.map((image) => [image.path, image])).values()];
2116
2187
  }
2117
2188
 
2118
- // src/plugin/plugin_cache.ts
2119
- import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
2120
- import fs3 from "node:fs/promises";
2121
- import path3 from "node:path";
2122
- function createUnavailablePluginCache() {
2123
- return {
2124
- get: unavailableAtRuntime,
2125
- set: unavailableAtRuntime,
2126
- delete: unavailableAtRuntime,
2127
- clear: unavailableAtRuntime
2128
- };
2129
- }
2130
- function createPluginCache(options) {
2131
- const pluginDirectory = path3.join(
2132
- options.cacheDirectory,
2133
- pluginNamespace(options.pluginName)
2134
- );
2135
- const versionDirectory2 = path3.join(
2136
- pluginDirectory,
2137
- hash3(options.cacheVersion ?? "1")
2138
- );
2139
- const writes = /* @__PURE__ */ new Map();
2140
- return {
2141
- async get(key) {
2142
- let raw;
2143
- try {
2144
- raw = await fs3.readFile(cachePath(versionDirectory2, key), "utf8");
2145
- } catch (error) {
2146
- if (isNotFoundError2(error)) {
2147
- options.tracer?.event("cache.miss", { plugin: options.pluginName });
2148
- return void 0;
2149
- }
2150
- throw error;
2151
- }
2152
- try {
2153
- const value = JSON.parse(raw);
2154
- options.tracer?.event("cache.hit", { plugin: options.pluginName });
2155
- return value;
2156
- } catch {
2157
- options.logger?.warn(
2158
- "Plugin cache entry is corrupted and will be ignored.",
2159
- {
2160
- plugin: options.pluginName
2161
- }
2162
- );
2163
- return void 0;
2164
- }
2165
- },
2166
- async set(key, value) {
2167
- const targetPath = cachePath(versionDirectory2, key);
2168
- const serialized = JSON.stringify(value);
2169
- if (serialized === void 0) {
2170
- throw new TypeError("Plugin cache values must be JSON serializable.");
2171
- }
2172
- const previousWrite = writes.get(targetPath) ?? Promise.resolve();
2173
- const write = previousWrite.catch(() => void 0).then(() => writeAtomically2(targetPath, versionDirectory2, serialized));
2174
- writes.set(targetPath, write);
2175
- try {
2176
- await write;
2177
- } finally {
2178
- if (writes.get(targetPath) === write) {
2179
- writes.delete(targetPath);
2180
- }
2181
- }
2182
- },
2183
- async delete(key) {
2184
- await fs3.rm(cachePath(versionDirectory2, key), { force: true });
2185
- },
2186
- async clear() {
2187
- await fs3.rm(pluginDirectory, { recursive: true, force: true });
2188
- }
2189
- };
2189
+ // src/utils/text.ts
2190
+ function escapeRegExp(value) {
2191
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2190
2192
  }
2191
- async function writeAtomically2(targetPath, versionDirectory2, serialized) {
2192
- await fs3.mkdir(versionDirectory2, { recursive: true });
2193
- const temporaryPath = `${targetPath}.${randomUUID2()}.tmp`;
2194
- try {
2195
- await fs3.writeFile(temporaryPath, serialized, "utf8");
2196
- await fs3.rename(temporaryPath, targetPath);
2197
- } catch (error) {
2198
- await fs3.rm(temporaryPath, { force: true }).catch(() => void 0);
2199
- throw error;
2200
- }
2193
+ function stripHtml(html) {
2194
+ return html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ");
2201
2195
  }
2202
- function resolvePluginCacheDirectory(config) {
2203
- if (!config) return path3.resolve(process.cwd(), ".riebeckite", "cache");
2204
- if (config.cache?.directory) return config.cache.directory;
2205
- if (config.buildDirectory) return path3.join(config.buildDirectory, "cache");
2206
- return path3.resolve(config.content.directory, "..", ".riebeckite", "cache");
2196
+ function calculateReadingTime(html) {
2197
+ const text = stripHtml(html).trim();
2198
+ if (!text) return 1;
2199
+ const cjkCharacters = text.match(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu)?.length ?? 0;
2200
+ const latinWords = text.replace(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu, " ").match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
2201
+ return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
2207
2202
  }
2208
- function cachePath(versionDirectory2, key) {
2209
- return path3.join(versionDirectory2, `${hash3(key)}.json`);
2203
+
2204
+ // src/markdown_fragments.ts
2205
+ function selectEmbedMarkdownFragment(markdown, fragment) {
2206
+ if (!fragment) return markdown;
2207
+ if (fragment.kind === "block")
2208
+ return selectBlockFragment(markdown, fragment.value);
2209
+ return selectHeadingFragment(markdown, fragment.value);
2210
2210
  }
2211
- function pluginNamespace(pluginName) {
2212
- return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash3(pluginName);
2211
+ function selectBlockFragment(markdown, blockId) {
2212
+ const lines = markdown.split(/\r?\n/);
2213
+ const blockIdRe = new RegExp(`(?:^|\\s)\\^${escapeRegExp(blockId)}\\s*$`);
2214
+ const line = lines.find((currentLine) => blockIdRe.test(currentLine));
2215
+ return line?.replace(blockIdRe, "").trimEnd() || null;
2213
2216
  }
2214
- function hash3(value) {
2215
- return createHash5("sha256").update(value).digest("hex");
2217
+ function selectHeadingFragment(markdown, heading) {
2218
+ const lines = markdown.split(/\r?\n/);
2219
+ const targetHeading = heading.trim().toLowerCase();
2220
+ const startIndex = lines.findIndex((line) => {
2221
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
2222
+ return match?.[2]?.trim().toLowerCase() === targetHeading;
2223
+ });
2224
+ if (startIndex < 0) return null;
2225
+ const level = lines[startIndex]?.match(/^(#{1,6})\s+/)?.[1]?.length ?? 6;
2226
+ const endIndex = lines.findIndex((line, index) => {
2227
+ if (index <= startIndex) return false;
2228
+ const match = line.match(/^(#{1,6})\s+/);
2229
+ return match !== null && match[1].length <= level;
2230
+ });
2231
+ const selectedLines = lines.slice(
2232
+ startIndex,
2233
+ endIndex === -1 ? void 0 : endIndex
2234
+ );
2235
+ return selectedLines.join("\n").trim() || null;
2216
2236
  }
2217
- function isNotFoundError2(error) {
2237
+
2238
+ // src/markdown_links.ts
2239
+ function normalizeMarkdownLinks(context) {
2240
+ return (tree) => {
2241
+ visitMarkdownLinkNodes(tree, (node) => {
2242
+ const normalizedUrl = normalizeMarkdownLinkUrl(node.url, context);
2243
+ if (normalizedUrl) node.url = normalizedUrl;
2244
+ });
2245
+ };
2246
+ }
2247
+ function resolveLinkDependencyValue(id, contentIndex, lookupPermalink, isRoutable) {
2248
+ if (id.startsWith(LINK_INDEX_PREFIX)) {
2249
+ return contentIndex.get(id.slice(LINK_INDEX_PREFIX.length)) ?? "";
2250
+ }
2251
+ if (id.startsWith(LINK_LOCATION_PREFIX)) {
2252
+ const slug = id.slice(LINK_LOCATION_PREFIX.length);
2253
+ const permalink = lookupPermalink(slug);
2254
+ if (permalink === void 0) return "";
2255
+ if (isRoutable && !isRoutable(slug)) return "";
2256
+ return permalink;
2257
+ }
2258
+ return "";
2259
+ }
2260
+ function visitMarkdownLinkNodes(node, visitor) {
2261
+ const linkNode = node;
2262
+ if ((node.type === "link" || node.type === "definition") && typeof linkNode.url === "string") {
2263
+ visitor(linkNode);
2264
+ }
2265
+ for (const child of linkNode.children ?? []) {
2266
+ visitMarkdownLinkNodes(child, visitor);
2267
+ }
2268
+ }
2269
+ function normalizeMarkdownLinkUrl(url, context) {
2270
+ if (!context.sourceSlug) return null;
2271
+ if (!isMarkdownContentLink(url)) return null;
2272
+ const [pathAndQuery, hash4 = ""] = splitOnce(url, "#");
2273
+ const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
2274
+ const decodedPath = decodeUriPath2(rawPath);
2275
+ if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
2276
+ const resolvedSlug = resolveRelativeMarkdownSlug(
2277
+ context.sourceSlug,
2278
+ decodedPath
2279
+ );
2280
+ if (!resolvedSlug) return null;
2281
+ const indexedSlug = getExactContentSlug(context, resolvedSlug);
2282
+ if (!indexedSlug) return null;
2283
+ recordLinkResolution(context, LINK_LOCATION_PREFIX, indexedSlug);
2284
+ if (context.isRoutable && !context.isRoutable(indexedSlug)) return null;
2285
+ const suffix = `${query ? `?${query}` : ""}${hash4 ? `#${hash4}` : ""}`;
2286
+ return `${context.resolvePermalink(indexedSlug)}${suffix}`;
2287
+ }
2288
+ function recordLinkResolution(context, prefix, key) {
2289
+ if (!context.recordLinkResolution) return;
2290
+ const value = prefix === LINK_INDEX_PREFIX ? context.contentIndex.get(key) ?? "" : resolveLinkDependencyValue(
2291
+ `${prefix}${key}`,
2292
+ context.contentIndex,
2293
+ (slug) => {
2294
+ try {
2295
+ return context.resolvePermalink(slug);
2296
+ } catch {
2297
+ return void 0;
2298
+ }
2299
+ },
2300
+ context.isRoutable
2301
+ );
2302
+ context.recordLinkResolution(`${prefix}${key}`, value);
2303
+ }
2304
+ function getExactContentSlug(context, slug) {
2305
+ try {
2306
+ context.resolvePermalink(slug);
2307
+ return slug;
2308
+ } catch {
2309
+ const indexKey = slug.toLowerCase();
2310
+ recordLinkResolution(context, LINK_INDEX_PREFIX, indexKey);
2311
+ return context.contentIndex.get(indexKey) ?? null;
2312
+ }
2313
+ }
2314
+ function isMarkdownContentLink(url) {
2315
+ if (url.startsWith("#") || url.startsWith("//")) return false;
2316
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return false;
2317
+ return true;
2318
+ }
2319
+ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
2320
+ const sourceDirectory = sourceSlug.includes("/") ? sourceSlug.slice(0, sourceSlug.lastIndexOf("/")) : "";
2321
+ const baseParts = targetPath.startsWith("/") ? [] : sourceDirectory.split("/");
2322
+ const parts = [...baseParts, ...targetPath.replace(/^\/+/, "").split("/")];
2323
+ const normalizedParts = [];
2324
+ for (const part of parts) {
2325
+ if (!part || part === ".") continue;
2326
+ if (part === "..") {
2327
+ if (normalizedParts.length === 0) return null;
2328
+ normalizedParts.pop();
2329
+ continue;
2330
+ }
2331
+ normalizedParts.push(part);
2332
+ }
2333
+ return normalizedParts.join("/").replace(/\.md$/i, "");
2334
+ }
2335
+ function decodeUriPath2(path6) {
2336
+ try {
2337
+ return decodeURI(path6);
2338
+ } catch {
2339
+ return null;
2340
+ }
2341
+ }
2342
+
2343
+ // src/plugin/plugin_cache.ts
2344
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
2345
+ import fs3 from "node:fs/promises";
2346
+ import path3 from "node:path";
2347
+ function createUnavailablePluginCache() {
2348
+ return {
2349
+ get: unavailableAtRuntime,
2350
+ set: unavailableAtRuntime,
2351
+ delete: unavailableAtRuntime,
2352
+ clear: unavailableAtRuntime
2353
+ };
2354
+ }
2355
+ function createPluginCache(options) {
2356
+ const pluginDirectory = path3.join(
2357
+ options.cacheDirectory,
2358
+ pluginNamespace(options.pluginName)
2359
+ );
2360
+ const versionDirectory2 = path3.join(
2361
+ pluginDirectory,
2362
+ hash3(options.cacheVersion ?? "1")
2363
+ );
2364
+ const writes = /* @__PURE__ */ new Map();
2365
+ return {
2366
+ async get(key) {
2367
+ let raw;
2368
+ try {
2369
+ raw = await fs3.readFile(cachePath(versionDirectory2, key), "utf8");
2370
+ } catch (error) {
2371
+ if (isNotFoundError2(error)) {
2372
+ options.tracer?.event("cache.miss", { plugin: options.pluginName });
2373
+ return void 0;
2374
+ }
2375
+ throw error;
2376
+ }
2377
+ try {
2378
+ const value = JSON.parse(raw);
2379
+ options.tracer?.event("cache.hit", { plugin: options.pluginName });
2380
+ return value;
2381
+ } catch {
2382
+ options.logger?.warn(
2383
+ "Plugin cache entry is corrupted and will be ignored.",
2384
+ {
2385
+ plugin: options.pluginName
2386
+ }
2387
+ );
2388
+ return void 0;
2389
+ }
2390
+ },
2391
+ async set(key, value) {
2392
+ const targetPath = cachePath(versionDirectory2, key);
2393
+ const serialized = JSON.stringify(value);
2394
+ if (serialized === void 0) {
2395
+ throw new TypeError("Plugin cache values must be JSON serializable.");
2396
+ }
2397
+ const previousWrite = writes.get(targetPath) ?? Promise.resolve();
2398
+ const write = previousWrite.catch(() => void 0).then(() => writeAtomically2(targetPath, versionDirectory2, serialized));
2399
+ writes.set(targetPath, write);
2400
+ try {
2401
+ await write;
2402
+ } finally {
2403
+ if (writes.get(targetPath) === write) {
2404
+ writes.delete(targetPath);
2405
+ }
2406
+ }
2407
+ },
2408
+ async delete(key) {
2409
+ await fs3.rm(cachePath(versionDirectory2, key), { force: true });
2410
+ },
2411
+ async clear() {
2412
+ await fs3.rm(pluginDirectory, { recursive: true, force: true });
2413
+ }
2414
+ };
2415
+ }
2416
+ async function writeAtomically2(targetPath, versionDirectory2, serialized) {
2417
+ await fs3.mkdir(versionDirectory2, { recursive: true });
2418
+ const temporaryPath = `${targetPath}.${randomUUID2()}.tmp`;
2419
+ try {
2420
+ await fs3.writeFile(temporaryPath, serialized, "utf8");
2421
+ await fs3.rename(temporaryPath, targetPath);
2422
+ } catch (error) {
2423
+ await fs3.rm(temporaryPath, { force: true }).catch(() => void 0);
2424
+ throw error;
2425
+ }
2426
+ }
2427
+ function resolvePluginCacheDirectory(config) {
2428
+ if (!config) return path3.resolve(process.cwd(), ".riebeckite", "cache");
2429
+ if (config.cache?.directory) return config.cache.directory;
2430
+ if (config.buildDirectory) return path3.join(config.buildDirectory, "cache");
2431
+ return path3.resolve(config.content.directory, "..", ".riebeckite", "cache");
2432
+ }
2433
+ function cachePath(versionDirectory2, key) {
2434
+ return path3.join(versionDirectory2, `${hash3(key)}.json`);
2435
+ }
2436
+ function pluginNamespace(pluginName) {
2437
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash3(pluginName);
2438
+ }
2439
+ function hash3(value) {
2440
+ return createHash5("sha256").update(value).digest("hex");
2441
+ }
2442
+ function isNotFoundError2(error) {
2218
2443
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2219
2444
  }
2220
2445
  async function unavailableAtRuntime() {
@@ -2409,9 +2634,9 @@ var Pipeline = class {
2409
2634
  this.options.onPersistentContentCacheResult?.("bypass");
2410
2635
  }
2411
2636
  }
2412
- const processor = unified2();
2637
+ const processor = unified3();
2413
2638
  const plugins = this.resolvePlugins();
2414
- this.use(processor, remarkParse2);
2639
+ this.use(processor, remarkParse3);
2415
2640
  this.use(processor, remarkDirective);
2416
2641
  this.use(processor, remarkFrontmatter, ["yaml", "toml"]);
2417
2642
  this.use(processor, function loadFrontmatter() {
@@ -2535,14 +2760,14 @@ var Pipeline = class {
2535
2760
  dep.id,
2536
2761
  async () => await this.getMarkdownBySlug?.(dep.id) ?? ""
2537
2762
  );
2538
- if (fingerprintContent(content) !== dep.fingerprint) return dep;
2763
+ if (fingerprintContent2(content) !== dep.fingerprint) return dep;
2539
2764
  } else if (dep.kind === "file") {
2540
2765
  const fileContent = await validationTracker.contentSource.read({
2541
2766
  path: dep.id
2542
2767
  });
2543
- if (fingerprintContent(fileContent) !== dep.fingerprint) return dep;
2768
+ if (fingerprintContent2(fileContent) !== dep.fingerprint) return dep;
2544
2769
  } else if (dep.kind === "link") {
2545
- if (fingerprintContent(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2770
+ if (fingerprintContent2(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2546
2771
  return dep;
2547
2772
  }
2548
2773
  }
@@ -2666,170 +2891,135 @@ var Pipeline = class {
2666
2891
  return cache;
2667
2892
  }
2668
2893
  };
2669
- function selectEmbedMarkdownFragment(markdown, fragment) {
2670
- if (!fragment) return markdown;
2671
- if (fragment.kind === "block")
2672
- return selectBlockFragment(markdown, fragment.value);
2673
- return selectHeadingFragment(markdown, fragment.value);
2674
- }
2675
- function selectBlockFragment(markdown, blockId) {
2676
- const lines = markdown.split(/\r?\n/);
2677
- const blockIdRe = new RegExp(`(?:^|\\s)\\^${escapeRegExp(blockId)}\\s*$`);
2678
- const line = lines.find((currentLine) => blockIdRe.test(currentLine));
2679
- return line?.replace(blockIdRe, "").trimEnd() || null;
2894
+
2895
+ // src/types/plugin_asset.ts
2896
+ function createStyleAsset(pluginName, moduleSpecifier) {
2897
+ return {
2898
+ pluginName,
2899
+ kind: "style",
2900
+ moduleSpecifier
2901
+ };
2680
2902
  }
2681
- function normalizeMarkdownLinks(context) {
2682
- return (tree) => {
2683
- visitMarkdownLinkNodes(tree, (node) => {
2684
- const normalizedUrl = normalizeMarkdownLinkUrl(node.url, context);
2685
- if (normalizedUrl) node.url = normalizedUrl;
2686
- });
2903
+ function createClientEntry(pluginName, moduleSpecifier, exportName, publicConfig) {
2904
+ return {
2905
+ pluginName,
2906
+ moduleSpecifier,
2907
+ ...exportName === void 0 ? {} : { exportName },
2908
+ ...publicConfig === void 0 ? {} : { publicConfig }
2687
2909
  };
2688
2910
  }
2689
- function resolveLinkDependencyValue(id, contentIndex, lookupPermalink, isRoutable) {
2690
- if (id.startsWith(LINK_INDEX_PREFIX)) {
2691
- return contentIndex.get(id.slice(LINK_INDEX_PREFIX.length)) ?? "";
2911
+ function serializePublicClientConfig(value) {
2912
+ assertJsonValue(value, /* @__PURE__ */ new Set());
2913
+ return JSON.stringify(value);
2914
+ }
2915
+ function assertJsonValue(value, ancestors) {
2916
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
2917
+ return;
2692
2918
  }
2693
- if (id.startsWith(LINK_LOCATION_PREFIX)) {
2694
- const slug = id.slice(LINK_LOCATION_PREFIX.length);
2695
- const permalink = lookupPermalink(slug);
2696
- if (permalink === void 0) return "";
2697
- if (isRoutable && !isRoutable(slug)) return "";
2698
- return permalink;
2919
+ if (typeof value === "number") {
2920
+ if (Number.isFinite(value)) return;
2921
+ throw new TypeError("Public client configuration numbers must be finite.");
2699
2922
  }
2700
- return "";
2701
- }
2702
- function visitMarkdownLinkNodes(node, visitor) {
2703
- const linkNode = node;
2704
- if ((node.type === "link" || node.type === "definition") && typeof linkNode.url === "string") {
2705
- visitor(linkNode);
2923
+ if (typeof value !== "object") {
2924
+ throw new TypeError(
2925
+ "Public client configuration must be JSON-serializable."
2926
+ );
2706
2927
  }
2707
- for (const child of linkNode.children ?? []) {
2708
- visitMarkdownLinkNodes(child, visitor);
2928
+ if (ancestors.has(value)) {
2929
+ throw new TypeError("Public client configuration must not contain cycles.");
2709
2930
  }
2931
+ const prototype = Object.getPrototypeOf(value);
2932
+ if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value)) {
2933
+ throw new TypeError(
2934
+ "Public client configuration must contain only plain objects."
2935
+ );
2936
+ }
2937
+ ancestors.add(value);
2938
+ for (const item of Array.isArray(value) ? value : Object.values(value)) {
2939
+ assertJsonValue(item, ancestors);
2940
+ }
2941
+ ancestors.delete(value);
2710
2942
  }
2711
- function normalizeMarkdownLinkUrl(url, context) {
2712
- if (!context.sourceSlug) return null;
2713
- if (!isMarkdownContentLink(url)) return null;
2714
- const [pathAndQuery, hash4 = ""] = splitOnce2(url, "#");
2715
- const [rawPath, query = ""] = splitOnce2(pathAndQuery, "?");
2716
- const decodedPath = decodeUriPath2(rawPath);
2717
- if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
2718
- const resolvedSlug = resolveRelativeMarkdownSlug(
2719
- context.sourceSlug,
2720
- decodedPath
2721
- );
2722
- if (!resolvedSlug) return null;
2723
- const indexedSlug = getExactContentSlug(context, resolvedSlug);
2724
- if (!indexedSlug) return null;
2725
- recordLinkResolution(context, LINK_LOCATION_PREFIX, indexedSlug);
2726
- if (context.isRoutable && !context.isRoutable(indexedSlug)) return null;
2727
- const suffix = `${query ? `?${query}` : ""}${hash4 ? `#${hash4}` : ""}`;
2728
- return `${context.resolvePermalink(indexedSlug)}${suffix}`;
2729
- }
2730
- function recordLinkResolution(context, prefix, key) {
2731
- if (!context.recordLinkResolution) return;
2732
- const value = prefix === LINK_INDEX_PREFIX ? context.contentIndex.get(key) ?? "" : resolveLinkDependencyValue(
2733
- `${prefix}${key}`,
2734
- context.contentIndex,
2735
- (slug) => {
2736
- try {
2737
- return context.resolvePermalink(slug);
2738
- } catch {
2739
- return void 0;
2943
+
2944
+ // src/plugin/generated_output_registry.ts
2945
+ var GeneratedOutputRegistry = class {
2946
+ outputs = [];
2947
+ owners = /* @__PURE__ */ new Map();
2948
+ sinkFor(pluginName) {
2949
+ const register = (path6, output) => {
2950
+ const owner = this.owners.get(path6);
2951
+ if (owner) {
2952
+ throw new Error(
2953
+ `Duplicate generated output path "${path6}" declared by "${owner}" and "${pluginName}".`
2954
+ );
2740
2955
  }
2741
- },
2742
- context.isRoutable
2743
- );
2744
- context.recordLinkResolution(`${prefix}${key}`, value);
2745
- }
2746
- function getExactContentSlug(context, slug) {
2747
- try {
2748
- context.resolvePermalink(slug);
2749
- return slug;
2750
- } catch {
2751
- const indexKey = slug.toLowerCase();
2752
- recordLinkResolution(context, LINK_INDEX_PREFIX, indexKey);
2753
- return context.contentIndex.get(indexKey) ?? null;
2956
+ this.owners.set(path6, pluginName);
2957
+ this.outputs.push({
2958
+ path: path6,
2959
+ content: output.content,
2960
+ dependencies: output.dependencies,
2961
+ owner: pluginName
2962
+ });
2963
+ };
2964
+ return {
2965
+ emit: (output) => {
2966
+ register(normalizeGeneratedOutputPath(output.path), output);
2967
+ },
2968
+ emitAsset: (output) => {
2969
+ register(normalizeContentAssetOutputPath(output.path), output);
2970
+ }
2971
+ };
2754
2972
  }
2755
- }
2756
- function isMarkdownContentLink(url) {
2757
- if (url.startsWith("#") || url.startsWith("//")) return false;
2758
- if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return false;
2759
- return true;
2760
- }
2761
- function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
2762
- const sourceDirectory = sourceSlug.includes("/") ? sourceSlug.slice(0, sourceSlug.lastIndexOf("/")) : "";
2763
- const baseParts = targetPath.startsWith("/") ? [] : sourceDirectory.split("/");
2764
- const parts = [...baseParts, ...targetPath.replace(/^\/+/, "").split("/")];
2765
- const normalizedParts = [];
2766
- for (const part of parts) {
2767
- if (!part || part === ".") continue;
2768
- if (part === "..") {
2769
- if (normalizedParts.length === 0) return null;
2770
- normalizedParts.pop();
2771
- continue;
2772
- }
2773
- normalizedParts.push(part);
2973
+ all() {
2974
+ return [...this.outputs].sort(
2975
+ (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
2976
+ );
2774
2977
  }
2775
- return normalizedParts.join("/").replace(/\.md$/i, "");
2776
- }
2777
- function decodeUriPath2(path6) {
2778
- try {
2779
- return decodeURI(path6);
2780
- } catch {
2781
- return null;
2978
+ count() {
2979
+ return this.outputs.length;
2782
2980
  }
2783
- }
2784
- function splitOnce2(value, separator) {
2785
- const index = value.indexOf(separator);
2786
- if (index < 0) return [value];
2787
- return [value.slice(0, index), value.slice(index + separator.length)];
2788
- }
2789
- function selectHeadingFragment(markdown, heading) {
2790
- const lines = markdown.split(/\r?\n/);
2791
- const targetHeading = heading.trim().toLowerCase();
2792
- const startIndex = lines.findIndex((line) => {
2793
- const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
2794
- return match?.[2]?.trim().toLowerCase() === targetHeading;
2795
- });
2796
- if (startIndex < 0) return null;
2797
- const level = lines[startIndex]?.match(/^(#{1,6})\s+/)?.[1]?.length ?? 6;
2798
- const endIndex = lines.findIndex((line, index) => {
2799
- if (index <= startIndex) return false;
2800
- const match = line.match(/^(#{1,6})\s+/);
2801
- return match !== null && match[1].length <= level;
2802
- });
2803
- const selectedLines = lines.slice(
2804
- startIndex,
2805
- endIndex === -1 ? void 0 : endIndex
2806
- );
2807
- return selectedLines.join("\n").trim() || null;
2808
- }
2809
- function escapeRegExp(value) {
2810
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2811
- }
2981
+ };
2812
2982
 
2813
2983
  // src/content/manifest_hook_cache.ts
2814
2984
  import { createHash as createHash6 } from "node:crypto";
2815
2985
  function fingerprintManifestHookInput(manifest) {
2816
2986
  return fingerprintManifest(manifestFingerprintValue(manifest));
2817
2987
  }
2818
- function createManifestHookCacheEntry(pluginName, cacheable, pipelineFingerprint, inputFingerprint, manifest) {
2819
- const snapshot = {
2820
- entries: JSON.parse(
2821
- JSON.stringify(manifest.entries)
2822
- )
2823
- };
2988
+ function serializeManifestEntries(entries) {
2989
+ const serialized = /* @__PURE__ */ new Map();
2990
+ for (const entry of entries)
2991
+ serialized.set(entry.slug, JSON.stringify(entry));
2992
+ return serialized;
2993
+ }
2994
+ function createManifestHookCacheEntry(pluginName, cacheable, pipelineFingerprint, inputFingerprint, manifest, beforeEntries, afterEntries) {
2995
+ if (!cacheable)
2996
+ return {
2997
+ pluginName,
2998
+ cacheable,
2999
+ pipelineFingerprint,
3000
+ inputFingerprint
3001
+ };
2824
3002
  return {
2825
3003
  pluginName,
2826
3004
  cacheable,
2827
3005
  pipelineFingerprint,
2828
3006
  inputFingerprint,
2829
- outputFingerprint: fingerprintManifestHookSnapshot(snapshot, manifest),
2830
- snapshot
3007
+ outputFingerprint: fingerprintManifestHookSnapshot(manifest),
3008
+ snapshot: createManifestHookSnapshot(beforeEntries, afterEntries)
2831
3009
  };
2832
3010
  }
3011
+ function createManifestHookSnapshot(beforeEntries, afterEntries) {
3012
+ const changed = [];
3013
+ const order = [];
3014
+ for (const [slug, serialized] of afterEntries) {
3015
+ order.push(slug);
3016
+ if (beforeEntries.get(slug) === serialized) continue;
3017
+ changed.push(JSON.parse(serialized));
3018
+ }
3019
+ const beforeOrder = [...beforeEntries.keys()];
3020
+ const orderChanged = beforeOrder.length !== order.length || beforeOrder.some((slug, index) => slug !== order[index]);
3021
+ return orderChanged ? { entries: changed, order } : { entries: changed };
3022
+ }
2833
3023
  function fingerprintManifestHookSideEffects(manifest) {
2834
3024
  const value = {
2835
3025
  ...manifest,
@@ -2837,70 +3027,138 @@ function fingerprintManifestHookSideEffects(manifest) {
2837
3027
  };
2838
3028
  return fingerprintManifest(manifestFingerprintValue(value));
2839
3029
  }
2840
- function fingerprintManifestHookSnapshot(snapshot, manifest) {
2841
- return fingerprintManifest(
2842
- manifestFingerprintValue({
2843
- ...manifest,
2844
- entries: [...snapshot.entries]
2845
- })
2846
- );
3030
+ function fingerprintManifestHookSnapshot(manifest) {
3031
+ return fingerprintManifest(manifestFingerprintValue(manifest));
2847
3032
  }
2848
- function applyManifestHookSnapshot(manifest, snapshot) {
3033
+ function captureManifestHookRestorePoint(manifest) {
3034
+ return {
3035
+ entries: manifest.entries,
3036
+ publicEntries: manifest.publicEntries,
3037
+ discoverableEntries: manifest.discoverableEntries
3038
+ };
3039
+ }
3040
+ function restoreManifestHookEntries(manifest, point) {
2849
3041
  try {
2850
- const entries = structuredClone(snapshot.entries);
2851
- if (!entries.every(isManifestEntry)) return false;
2852
- const entriesBySlug = new Map(entries.map((entry) => [entry.slug, entry]));
2853
- if (entriesBySlug.size !== entries.length) return false;
2854
- manifest.entries = entries;
2855
- manifest.publicEntries = manifest.publicEntries.map((entry) => entriesBySlug.get(entry.slug)).filter((entry) => entry !== void 0);
2856
- manifest.discoverableEntries = manifest.discoverableEntries.map((entry) => entriesBySlug.get(entry.slug)).filter((entry) => entry !== void 0);
2857
- manifest.bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
2858
- manifest.byContentId = new Map(
2859
- entries.filter((entry) => entry.contentId !== void 0).map((entry) => [entry.contentId, entry])
2860
- );
2861
- manifest.byPermalink = new Map(
2862
- entries.map((entry) => [entry.permalink, entry])
3042
+ manifest.publicEntries = point.publicEntries;
3043
+ manifest.discoverableEntries = point.discoverableEntries;
3044
+ const entriesBySlug = new Map(
3045
+ point.entries.map((entry) => [entry.slug, entry])
2863
3046
  );
2864
- manifest.byRoutablePermalink = new Map(
2865
- manifest.publicEntries.map((entry) => [entry.permalink, entry])
3047
+ if (entriesBySlug.size !== point.entries.length) return false;
3048
+ return commitManifestEntries(manifest, entriesBySlug);
3049
+ } catch {
3050
+ return false;
3051
+ }
3052
+ }
3053
+ function applyManifestHookSnapshot(manifest, snapshot) {
3054
+ try {
3055
+ const entriesBySlug = new Map(
3056
+ manifest.entries.map((entry) => [entry.slug, entry])
2866
3057
  );
2867
- manifest.byAlias = /* @__PURE__ */ new Map();
2868
- manifest.byTag = /* @__PURE__ */ new Map();
2869
- manifest.byAsset = /* @__PURE__ */ new Map();
2870
- manifest.outgoingLinks = /* @__PURE__ */ new Map();
2871
- manifest.incomingLinks = /* @__PURE__ */ new Map();
2872
- for (const entry of entries) {
2873
- manifest.outgoingLinks.set(entry.slug, entry.links);
2874
- for (const alias of entry.aliases ?? []) {
2875
- const aliases = manifest.byAlias.get(alias.toLowerCase()) ?? [];
2876
- aliases.push(entry);
2877
- manifest.byAlias.set(alias.toLowerCase(), aliases);
2878
- }
2879
- for (const tag of entry.tags) {
2880
- const tagged = manifest.byTag.get(tag) ?? [];
2881
- tagged.push(entry);
2882
- manifest.byTag.set(tag, tagged);
2883
- }
2884
- for (const asset of entry.assets) {
2885
- const assets = manifest.byAsset.get(asset.path) ?? [];
2886
- assets.push(entry);
2887
- manifest.byAsset.set(asset.path, assets);
2888
- }
2889
- for (const link of entry.links) {
2890
- if (link.kind !== "note" || link.slug === null || link.slug === entry.slug)
2891
- continue;
2892
- const incoming = manifest.incomingLinks.get(link.slug) ?? [];
2893
- if (!incoming.includes(entry.slug)) incoming.push(entry.slug);
2894
- manifest.incomingLinks.set(link.slug, incoming);
2895
- }
3058
+ const delta = [];
3059
+ for (const entry of structuredClone(snapshot.entries).map(
3060
+ reviveManifestEntry
3061
+ )) {
3062
+ if (entry === null || !isManifestEntry(entry)) return false;
3063
+ delta.push(entry);
2896
3064
  }
2897
- for (const entry of entries)
2898
- entry.backlinks = manifest.incomingLinks.get(entry.slug) ?? [];
2899
- return true;
3065
+ for (const entry of delta) entriesBySlug.set(entry.slug, entry);
3066
+ return commitManifestEntries(manifest, entriesBySlug, snapshot.order);
2900
3067
  } catch {
2901
3068
  return false;
2902
3069
  }
2903
3070
  }
3071
+ function commitManifestEntries(manifest, entriesBySlug, order) {
3072
+ let entries;
3073
+ if (order) {
3074
+ entries = [];
3075
+ for (const slug of order) {
3076
+ const entry = entriesBySlug.get(slug);
3077
+ if (!entry) return false;
3078
+ entries.push(entry);
3079
+ }
3080
+ if (entries.length !== entriesBySlug.size) return false;
3081
+ } else {
3082
+ entries = [...entriesBySlug.values()];
3083
+ }
3084
+ manifest.entries = entries;
3085
+ manifest.publicEntries = manifest.publicEntries.map((entry) => entriesBySlug.get(entry.slug)).filter((entry) => entry !== void 0);
3086
+ manifest.discoverableEntries = manifest.discoverableEntries.map((entry) => entriesBySlug.get(entry.slug)).filter((entry) => entry !== void 0);
3087
+ manifest.bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
3088
+ manifest.byContentId = new Map(
3089
+ entries.filter((entry) => entry.contentId !== void 0).map((entry) => [entry.contentId, entry])
3090
+ );
3091
+ manifest.byPermalink = new Map(
3092
+ entries.map((entry) => [entry.permalink, entry])
3093
+ );
3094
+ manifest.byRoutablePermalink = new Map(
3095
+ manifest.publicEntries.map((entry) => [entry.permalink, entry])
3096
+ );
3097
+ manifest.byAlias = /* @__PURE__ */ new Map();
3098
+ manifest.byTag = /* @__PURE__ */ new Map();
3099
+ manifest.byAsset = /* @__PURE__ */ new Map();
3100
+ manifest.outgoingLinks = /* @__PURE__ */ new Map();
3101
+ manifest.incomingLinks = /* @__PURE__ */ new Map();
3102
+ for (const entry of entries) {
3103
+ manifest.outgoingLinks.set(entry.slug, entry.links);
3104
+ for (const alias of entry.aliases ?? []) {
3105
+ const aliases = manifest.byAlias.get(alias.toLowerCase()) ?? [];
3106
+ aliases.push(entry);
3107
+ manifest.byAlias.set(alias.toLowerCase(), aliases);
3108
+ }
3109
+ for (const tag of entry.tags) {
3110
+ const tagged = manifest.byTag.get(tag) ?? [];
3111
+ tagged.push(entry);
3112
+ manifest.byTag.set(tag, tagged);
3113
+ }
3114
+ for (const asset of entry.assets) {
3115
+ const assets = manifest.byAsset.get(asset.path) ?? [];
3116
+ assets.push(entry);
3117
+ manifest.byAsset.set(asset.path, assets);
3118
+ }
3119
+ for (const link of entry.links) {
3120
+ if (link.kind !== "note" || link.slug === null || link.slug === entry.slug)
3121
+ continue;
3122
+ const incoming = manifest.incomingLinks.get(link.slug) ?? [];
3123
+ if (!incoming.includes(entry.slug)) incoming.push(entry.slug);
3124
+ manifest.incomingLinks.set(link.slug, incoming);
3125
+ }
3126
+ }
3127
+ for (const entry of entries)
3128
+ entry.backlinks = manifest.incomingLinks.get(entry.slug) ?? [];
3129
+ return true;
3130
+ }
3131
+ function reviveManifestEntry(entry) {
3132
+ const publishAt = entry.publishing.publishAt;
3133
+ const parsed = typeof publishAt === "string" ? new Date(publishAt) : publishAt;
3134
+ if (parsed instanceof Date && Number.isNaN(parsed.getTime())) return null;
3135
+ const file = reviveFileMetadata(entry.file);
3136
+ if (file === null) return null;
3137
+ return {
3138
+ ...entry,
3139
+ publishing: {
3140
+ ...entry.publishing,
3141
+ ...parsed ? { publishAt: parsed } : {}
3142
+ },
3143
+ ...file ? { file } : {}
3144
+ };
3145
+ }
3146
+ function reviveFileMetadata(file) {
3147
+ if (file === void 0) return void 0;
3148
+ const ctime = reviveDate(file.ctime);
3149
+ const mtime = reviveDate(file.mtime);
3150
+ if (ctime === null || mtime === null) return null;
3151
+ return {
3152
+ ...typeof file.size === "number" && Number.isSafeInteger(file.size) && file.size >= 0 ? { size: file.size } : {},
3153
+ ...ctime ? { ctime } : {},
3154
+ ...mtime ? { mtime } : {}
3155
+ };
3156
+ }
3157
+ function reviveDate(value) {
3158
+ if (value === void 0) return void 0;
3159
+ const parsed = value instanceof Date ? value : new Date(value);
3160
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
3161
+ }
2904
3162
  function manifestFingerprintValue(manifest) {
2905
3163
  return {
2906
3164
  entries: manifest.entries,
@@ -2933,212 +3191,457 @@ function isManifestEntry(value) {
2933
3191
  return typeof value === "object" && value !== null && typeof value.slug === "string" && typeof value.html === "string";
2934
3192
  }
2935
3193
 
2936
- // src/types/plugin_asset.ts
2937
- function createStyleAsset(pluginName, moduleSpecifier) {
2938
- return {
2939
- pluginName,
2940
- kind: "style",
2941
- moduleSpecifier
2942
- };
2943
- }
2944
- function createClientEntry(pluginName, moduleSpecifier, exportName, publicConfig) {
2945
- return {
2946
- pluginName,
2947
- moduleSpecifier,
2948
- ...exportName === void 0 ? {} : { exportName },
2949
- ...publicConfig === void 0 ? {} : { publicConfig }
2950
- };
2951
- }
2952
- function serializePublicClientConfig(value) {
2953
- assertJsonValue(value, /* @__PURE__ */ new Set());
2954
- return JSON.stringify(value);
2955
- }
2956
- function assertJsonValue(value, ancestors) {
2957
- if (value === null || typeof value === "boolean" || typeof value === "string") {
2958
- return;
2959
- }
2960
- if (typeof value === "number") {
2961
- if (Number.isFinite(value)) return;
2962
- throw new TypeError("Public client configuration numbers must be finite.");
3194
+ // src/plugin/plugin_lifecycle.ts
3195
+ var PluginHookError = class extends Error {
3196
+ path;
3197
+ constructor(pluginName, hookName, cause) {
3198
+ super(`Plugin "${pluginName}" failed during "${hookName}"`, { cause });
3199
+ this.name = "PluginHookError";
3200
+ const path6 = cause?.path;
3201
+ if (typeof path6 === "string") this.path = path6;
2963
3202
  }
2964
- if (typeof value !== "object") {
3203
+ };
3204
+ async function runPluginHook(plugin, hookName, context, hook) {
3205
+ try {
3206
+ return await context.tracer.span(
3207
+ `plugin.${hookName}`,
3208
+ {
3209
+ plugin: plugin.name,
3210
+ hook: hookName
3211
+ },
3212
+ () => hook(context)
3213
+ );
3214
+ } catch (error) {
3215
+ throw new PluginHookError(plugin.name, hookName, error);
3216
+ }
3217
+ }
3218
+ async function runSetup(plugins, createContext) {
3219
+ await runLifecycleHook(
3220
+ plugins,
3221
+ "setup",
3222
+ (plugin) => plugin.setup,
3223
+ createContext
3224
+ );
3225
+ }
3226
+ async function runBuildStart(plugins, createContext) {
3227
+ await runLifecycleHook(
3228
+ plugins,
3229
+ "buildStart",
3230
+ (plugin) => plugin.buildStart,
3231
+ createContext
3232
+ );
3233
+ }
3234
+ async function runBuildEnd(plugins, createContext) {
3235
+ await runLifecycleHook(
3236
+ plugins,
3237
+ "buildEnd",
3238
+ (plugin) => plugin.buildEnd,
3239
+ createContext
3240
+ );
3241
+ }
3242
+ async function runDispose(plugins, createContext) {
3243
+ await runLifecycleHook(
3244
+ [...plugins].reverse(),
3245
+ "dispose",
3246
+ (plugin) => plugin.dispose,
3247
+ createContext
3248
+ );
3249
+ }
3250
+ async function runLifecycleHook(plugins, hookName, getHook, createContext) {
3251
+ for (const plugin of plugins) {
3252
+ const hook = getHook(plugin);
3253
+ if (!hook) continue;
3254
+ await runPluginHook(plugin, hookName, createContext(plugin), hook);
3255
+ }
3256
+ }
3257
+
3258
+ // src/plugin/manifest_hooks.ts
3259
+ var ManifestHookRunner = class {
3260
+ constructor(options) {
3261
+ this.options = options;
3262
+ }
3263
+ options;
3264
+ manifestHooksExecuted = 0;
3265
+ manifestHookDurationMs = 0;
3266
+ manifestHooksReused = 0;
3267
+ manifestHookCache = [];
3268
+ manifestHookMetricsByPlugin = /* @__PURE__ */ new Map();
3269
+ getMetrics() {
3270
+ return {
3271
+ manifestHooksExecuted: this.manifestHooksExecuted,
3272
+ manifestHooksReused: this.manifestHooksReused,
3273
+ manifestHookCacheHit: this.manifestHooksReused,
3274
+ manifestHookCacheMiss: this.manifestHooksExecuted,
3275
+ manifestHookDurationMs: this.manifestHookDurationMs,
3276
+ byPlugin: Object.fromEntries(this.manifestHookMetricsByPlugin)
3277
+ };
3278
+ }
3279
+ getNoChangeFastPathBlockReason(previousCache, pipelineFingerprint) {
3280
+ let cacheIndex = 0;
3281
+ for (const plugin of this.options.plugins()) {
3282
+ if (plugin.buildEnd) return "plugin-build-end";
3283
+ if (plugin.planGeneratedOutputs) return "plugin-generated-output-plan";
3284
+ if (!plugin.onManifestCreated) continue;
3285
+ const cached = previousCache[cacheIndex];
3286
+ cacheIndex += 1;
3287
+ if (cached?.pluginName !== plugin.name || !cached.cacheable || cached.pipelineFingerprint !== pipelineFingerprint)
3288
+ return "manifest-hook-cache-miss";
3289
+ }
3290
+ return void 0;
3291
+ }
3292
+ getCache() {
3293
+ return this.manifestHookCache;
3294
+ }
3295
+ async runManifestCreated(manifest, contentIndex, previousCache = [], pipelineFingerprint) {
3296
+ const context = {
3297
+ ...this.options.createContext(contentIndex),
3298
+ manifest
3299
+ };
3300
+ let cacheIndex = 0;
3301
+ let entriesBeforeHook = serializeManifestEntries(manifest.entries);
3302
+ for (const plugin of this.options.plugins()) {
3303
+ const hook = plugin.onManifestCreated;
3304
+ if (!hook) continue;
3305
+ const inputFingerprint = fingerprintManifestHookInput(manifest);
3306
+ const cached = previousCache[cacheIndex];
3307
+ cacheIndex += 1;
3308
+ const reason = manifestHookCacheMissReason(
3309
+ cached,
3310
+ plugin.name,
3311
+ pipelineFingerprint,
3312
+ inputFingerprint
3313
+ );
3314
+ let restored = false;
3315
+ let restorePoint;
3316
+ if (cached !== void 0 && cached.pluginName === plugin.name && cached.cacheable && cached.pipelineFingerprint === pipelineFingerprint && cached.inputFingerprint === inputFingerprint && cached.snapshot !== void 0 && cached.outputFingerprint !== void 0) {
3317
+ restorePoint = captureManifestHookRestorePoint(manifest);
3318
+ restored = applyManifestHookSnapshot(manifest, cached.snapshot);
3319
+ if (restored && fingerprintManifestHookSnapshot(manifest) === cached.outputFingerprint) {
3320
+ this.manifestHooksReused += 1;
3321
+ this.manifestHookCache.push(cached);
3322
+ this.recordManifestHookMetric(plugin.name, "reused", "cache-hit", 0);
3323
+ entriesBeforeHook = serializeManifestEntries(manifest.entries);
3324
+ continue;
3325
+ }
3326
+ }
3327
+ if (restored && restorePoint)
3328
+ restoreManifestHookEntries(manifest, restorePoint);
3329
+ const sideEffectsBefore = fingerprintManifestHookSideEffects(manifest);
3330
+ const outputCountBefore = this.options.generatedOutputCount();
3331
+ const diagnosticCountBefore = this.options.diagnosticCount();
3332
+ const manifestDiagnosticCountBefore = manifest.diagnostics.length;
3333
+ const started = performance.now();
3334
+ this.manifestHooksExecuted += 1;
3335
+ try {
3336
+ await runPluginHook(
3337
+ plugin,
3338
+ "onManifestCreated",
3339
+ this.options.createPluginContext(plugin, context),
3340
+ hook
3341
+ );
3342
+ } finally {
3343
+ const durationMs = performance.now() - started;
3344
+ this.manifestHookDurationMs += durationMs;
3345
+ this.recordManifestHookMetric(
3346
+ plugin.name,
3347
+ "executed",
3348
+ reason,
3349
+ durationMs
3350
+ );
3351
+ }
3352
+ const entriesAfterHook = serializeManifestEntries(manifest.entries);
3353
+ this.manifestHookCache.push(
3354
+ createManifestHookCacheEntry(
3355
+ plugin.name,
3356
+ plugin.resolveExternalDependencies === void 0 && sideEffectsBefore === fingerprintManifestHookSideEffects(manifest) && outputCountBefore === this.options.generatedOutputCount() && diagnosticCountBefore === this.options.diagnosticCount() && manifestDiagnosticCountBefore === manifest.diagnostics.length,
3357
+ pipelineFingerprint,
3358
+ inputFingerprint,
3359
+ manifest,
3360
+ entriesBeforeHook,
3361
+ entriesAfterHook
3362
+ )
3363
+ );
3364
+ entriesBeforeHook = entriesAfterHook;
3365
+ }
3366
+ }
3367
+ recordManifestHookMetric(pluginName, outcome, reason, durationMs) {
3368
+ const previous = this.manifestHookMetricsByPlugin.get(pluginName) ?? {
3369
+ executed: 0,
3370
+ reused: 0,
3371
+ cacheHit: 0,
3372
+ cacheMiss: 0,
3373
+ durationMs: 0,
3374
+ reason
3375
+ };
3376
+ this.manifestHookMetricsByPlugin.set(pluginName, {
3377
+ executed: previous.executed + (outcome === "executed" ? 1 : 0),
3378
+ reused: previous.reused + (outcome === "reused" ? 1 : 0),
3379
+ cacheHit: previous.cacheHit + (outcome === "reused" ? 1 : 0),
3380
+ cacheMiss: previous.cacheMiss + (outcome === "executed" ? 1 : 0),
3381
+ durationMs: previous.durationMs + durationMs,
3382
+ reason
3383
+ });
3384
+ }
3385
+ };
3386
+ function manifestHookCacheMissReason(cached, pluginName, pipelineFingerprint, inputFingerprint) {
3387
+ if (!cached) return "cache-missing";
3388
+ if (cached.pluginName !== pluginName) return "plugin-order-changed";
3389
+ if (!cached.cacheable) return "uncacheable";
3390
+ if (cached.pipelineFingerprint !== pipelineFingerprint)
3391
+ return "pipeline-fingerprint-changed";
3392
+ if (cached.inputFingerprint !== inputFingerprint)
3393
+ return "manifest-input-changed";
3394
+ return "snapshot-invalid";
3395
+ }
3396
+
3397
+ // src/plugin/plugin_pages.ts
3398
+ var PluginPageResolver = class {
3399
+ constructor(options) {
3400
+ this.options = options;
3401
+ }
3402
+ options;
3403
+ pageDeclarations = /* @__PURE__ */ new WeakMap();
3404
+ async resolvePage(pathname, manifest, contentIndex) {
3405
+ const matches = [];
3406
+ const requestedPathname = normalizePagePath(pathname);
3407
+ for (const plugin of this.options.plugins()) {
3408
+ for (const pageType of plugin.pageTypes ?? []) {
3409
+ const pagePathname = normalizePageTypePath(pathname, pageType);
3410
+ const context = {
3411
+ ...this.options.createContext(contentIndex),
3412
+ manifest,
3413
+ pathname: pagePathname
3414
+ };
3415
+ const page = await pageType.resolve(
3416
+ this.options.createPluginContext(plugin, context)
3417
+ );
3418
+ if (page) {
3419
+ matches.push({
3420
+ page: {
3421
+ ...page,
3422
+ pathname: pageType.directoryIndex ? normalizeDirectoryPagePath(page.pathname) : page.pathname,
3423
+ type: pageType.id,
3424
+ pluginName: plugin.name
3425
+ },
3426
+ priority: pageType.priority ?? 0
3427
+ });
3428
+ }
3429
+ }
3430
+ }
3431
+ if (matches.length === 0) return null;
3432
+ const highestPriority = Math.max(...matches.map((match) => match.priority));
3433
+ const highest = matches.filter(
3434
+ (match) => match.priority === highestPriority
3435
+ );
3436
+ if (highest.length !== 1) {
3437
+ throw new Error(
3438
+ `Multiple plugin page types match ${requestedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
3439
+ );
3440
+ }
3441
+ return highest[0].page;
3442
+ }
3443
+ async getPagePaths(manifest, contentIndex) {
3444
+ return [
3445
+ ...new Set(
3446
+ (await this.getPageDeclarations(manifest, contentIndex)).map(
3447
+ (declaration) => declaration.pathname
3448
+ )
3449
+ )
3450
+ ];
3451
+ }
3452
+ async getPageRoutes(manifest, contentIndex) {
3453
+ return (await this.getPageDeclarations(manifest, contentIndex)).map(
3454
+ ({ pathname, plugin, pageType }) => ({
3455
+ pathname,
3456
+ pluginName: plugin.name,
3457
+ pageType: pageType.id
3458
+ })
3459
+ );
3460
+ }
3461
+ async getPageOutputs(manifest, contentIndex) {
3462
+ const outputs = await Promise.all(
3463
+ (await this.getPageDeclarations(manifest, contentIndex)).map(
3464
+ async ({ pathname, plugin, pageType }) => {
3465
+ const context = {
3466
+ ...this.options.createContext(contentIndex),
3467
+ manifest,
3468
+ pathname
3469
+ };
3470
+ const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
3471
+ this.options.createPluginContext(plugin, context)
3472
+ ) : pageType.outputDependencies : [{ type: "unknown" }];
3473
+ return {
3474
+ output: {
3475
+ kind: "plugin-page",
3476
+ path: pathname,
3477
+ producer: `plugin:${plugin.name}:page:${pageType.id}`,
3478
+ dependencies
3479
+ },
3480
+ priority: pageType.priority ?? 0
3481
+ };
3482
+ }
3483
+ )
3484
+ );
3485
+ return uniqueOutputs(outputs);
3486
+ }
3487
+ async getPageDeclarations(manifest, contentIndex) {
3488
+ const existing = this.pageDeclarations.get(manifest);
3489
+ if (existing) return await existing;
3490
+ const declarations = this.resolvePageDeclarations(manifest, contentIndex);
3491
+ this.pageDeclarations.set(manifest, declarations);
3492
+ return await declarations;
3493
+ }
3494
+ async resolvePageDeclarations(manifest, contentIndex) {
3495
+ const declarations = [];
3496
+ const context = {
3497
+ ...this.options.createContext(contentIndex),
3498
+ manifest
3499
+ };
3500
+ for (const plugin of this.options.plugins()) {
3501
+ for (const pageType of plugin.pageTypes ?? []) {
3502
+ const paths = pageType.paths;
3503
+ if (!paths) continue;
3504
+ const resolved = typeof paths === "function" ? await paths(this.options.createPluginContext(plugin, context)) : paths;
3505
+ for (const pathname of resolved) {
3506
+ declarations.push({
3507
+ pathname: normalizePageTypePath(pathname, pageType),
3508
+ plugin,
3509
+ pageType
3510
+ });
3511
+ }
3512
+ }
3513
+ }
3514
+ return declarations;
3515
+ }
3516
+ };
3517
+ function collectPageTypeOwners(plugins) {
3518
+ const pageTypes = /* @__PURE__ */ new Map();
3519
+ for (const plugin of plugins) {
3520
+ const declaredPageTypes = plugin.pageTypes;
3521
+ if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
3522
+ throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
3523
+ }
3524
+ for (const pageType of declaredPageTypes ?? []) {
3525
+ validatePageType(plugin.name, pageType);
3526
+ const previous = pageTypes.get(pageType.id);
3527
+ if (previous) {
3528
+ throw new Error(
3529
+ `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
3530
+ );
3531
+ }
3532
+ pageTypes.set(pageType.id, plugin.name);
3533
+ }
3534
+ }
3535
+ }
3536
+ function validatePageType(pluginName, pageType) {
3537
+ if (typeof pageType !== "object" || pageType === null) {
3538
+ throw new TypeError(`Plugin ${pluginName} pageTypes must contain objects`);
3539
+ }
3540
+ const candidate = pageType;
3541
+ if (typeof candidate.id !== "string" || candidate.id.trim() === "") {
2965
3542
  throw new TypeError(
2966
- "Public client configuration must be JSON-serializable."
3543
+ `Plugin ${pluginName} page type id must be a non-empty string`
2967
3544
  );
2968
3545
  }
2969
- if (ancestors.has(value)) {
2970
- throw new TypeError("Public client configuration must not contain cycles.");
2971
- }
2972
- const prototype = Object.getPrototypeOf(value);
2973
- if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value)) {
3546
+ if (typeof candidate.resolve !== "function") {
2974
3547
  throw new TypeError(
2975
- "Public client configuration must contain only plain objects."
3548
+ `Plugin ${pluginName} page type "${candidate.id}" must provide a resolve function`
2976
3549
  );
2977
3550
  }
2978
- ancestors.add(value);
2979
- for (const item of Array.isArray(value) ? value : Object.values(value)) {
2980
- assertJsonValue(item, ancestors);
2981
- }
2982
- ancestors.delete(value);
2983
- }
2984
-
2985
- // src/plugin/generated_output_registry.ts
2986
- var GeneratedOutputRegistry = class {
2987
- outputs = [];
2988
- owners = /* @__PURE__ */ new Map();
2989
- sinkFor(pluginName) {
2990
- const register = (path6, output) => {
2991
- const owner = this.owners.get(path6);
2992
- if (owner) {
2993
- throw new Error(
2994
- `Duplicate generated output path "${path6}" declared by "${owner}" and "${pluginName}".`
2995
- );
2996
- }
2997
- this.owners.set(path6, pluginName);
2998
- this.outputs.push({
2999
- path: path6,
3000
- content: output.content,
3001
- dependencies: output.dependencies,
3002
- owner: pluginName
3003
- });
3004
- };
3005
- return {
3006
- emit: (output) => {
3007
- register(normalizeGeneratedOutputPath(output.path), output);
3008
- },
3009
- emitAsset: (output) => {
3010
- register(normalizeContentAssetOutputPath(output.path), output);
3011
- }
3012
- };
3013
- }
3014
- all() {
3015
- return [...this.outputs].sort(
3016
- (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
3551
+ if (candidate.paths !== void 0 && !Array.isArray(candidate.paths) && typeof candidate.paths !== "function") {
3552
+ throw new TypeError(
3553
+ `Plugin ${pluginName} page type "${candidate.id}" paths must be an array or function`
3017
3554
  );
3018
3555
  }
3019
- count() {
3020
- return this.outputs.length;
3556
+ if (candidate.priority !== void 0 && (typeof candidate.priority !== "number" || !Number.isFinite(candidate.priority))) {
3557
+ throw new TypeError(
3558
+ `Plugin ${pluginName} page type "${candidate.id}" priority must be a finite number`
3559
+ );
3021
3560
  }
3022
- };
3023
-
3024
- // src/plugin/plugin_lifecycle.ts
3025
- var PluginHookError = class extends Error {
3026
- path;
3027
- constructor(pluginName, hookName, cause) {
3028
- super(`Plugin "${pluginName}" failed during "${hookName}"`, { cause });
3029
- this.name = "PluginHookError";
3030
- const path6 = cause?.path;
3031
- if (typeof path6 === "string") this.path = path6;
3561
+ if (candidate.outputDependencies !== void 0 && !Array.isArray(candidate.outputDependencies) && typeof candidate.outputDependencies !== "function") {
3562
+ throw new TypeError(
3563
+ `Plugin ${pluginName} page type "${candidate.id}" outputDependencies must be an array or function`
3564
+ );
3032
3565
  }
3033
- };
3034
- async function runPluginHook(plugin, hookName, context, hook) {
3035
- try {
3036
- return await context.tracer.span(
3037
- `plugin.${hookName}`,
3038
- {
3039
- plugin: plugin.name,
3040
- hook: hookName
3041
- },
3042
- () => hook(context)
3566
+ if (candidate.directoryIndex !== void 0 && typeof candidate.directoryIndex !== "boolean") {
3567
+ throw new TypeError(
3568
+ `Plugin ${pluginName} page type "${candidate.id}" directoryIndex must be a boolean`
3043
3569
  );
3044
- } catch (error) {
3045
- throw new PluginHookError(plugin.name, hookName, error);
3046
3570
  }
3047
3571
  }
3048
- async function runSetup(plugins, createContext) {
3049
- await runLifecycleHook(
3050
- plugins,
3051
- "setup",
3052
- (plugin) => plugin.setup,
3053
- createContext
3054
- );
3055
- }
3056
- async function runBuildStart(plugins, createContext) {
3057
- await runLifecycleHook(
3058
- plugins,
3059
- "buildStart",
3060
- (plugin) => plugin.buildStart,
3061
- createContext
3062
- );
3572
+ function normalizePageTypePath(pathname, pageType) {
3573
+ return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
3063
3574
  }
3064
- async function runBuildEnd(plugins, createContext) {
3065
- await runLifecycleHook(
3066
- plugins,
3067
- "buildEnd",
3068
- (plugin) => plugin.buildEnd,
3069
- createContext
3070
- );
3575
+ function normalizeDirectoryPagePath(pathname) {
3576
+ const path6 = normalizePagePath(pathname);
3577
+ return path6 === "/" ? path6 : `${path6}/`;
3071
3578
  }
3072
- async function runDispose(plugins, createContext) {
3073
- await runLifecycleHook(
3074
- [...plugins].reverse(),
3075
- "dispose",
3076
- (plugin) => plugin.dispose,
3077
- createContext
3078
- );
3579
+ function normalizePagePath(pathname) {
3580
+ const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3581
+ return path6 === "/" ? path6 : path6.replace(/\/$/, "");
3079
3582
  }
3080
- async function runLifecycleHook(plugins, hookName, getHook, createContext) {
3081
- for (const plugin of plugins) {
3082
- const hook = getHook(plugin);
3083
- if (!hook) continue;
3084
- await runPluginHook(plugin, hookName, createContext(plugin), hook);
3583
+ function uniqueOutputs(outputs) {
3584
+ const byPath = /* @__PURE__ */ new Map();
3585
+ for (const output of outputs) {
3586
+ const previous = byPath.get(output.output.path);
3587
+ if (!previous || output.priority > previous.priority) {
3588
+ byPath.set(output.output.path, output);
3589
+ continue;
3590
+ }
3591
+ if (output.output.producer === previous.output.producer) continue;
3592
+ if (output.priority === previous.priority) {
3593
+ throw new Error(
3594
+ `Multiple plugin page types declare ${output.output.path} at priority ${output.priority}: ${previous.output.producer}, ${output.output.producer}`
3595
+ );
3596
+ }
3085
3597
  }
3598
+ return [...byPath.values()].map(({ output }) => output).sort((left, right) => left.path.localeCompare(right.path));
3086
3599
  }
3087
3600
 
3088
3601
  // src/plugin/plugin_runtime.ts
3089
3602
  var PluginRuntime = class {
3090
3603
  constructor(pipelineOptions = {}) {
3091
3604
  this.pipelineOptions = pipelineOptions;
3605
+ this.pageResolver = new PluginPageResolver({
3606
+ plugins: () => this.plugins(),
3607
+ createContext: (contentIndex) => this.createContext(contentIndex),
3608
+ createPluginContext: (plugin, context) => this.createPluginContext(plugin, context)
3609
+ });
3610
+ this.manifestHooks = new ManifestHookRunner({
3611
+ plugins: () => this.plugins(),
3612
+ createContext: (contentIndex) => this.createContext(contentIndex),
3613
+ createPluginContext: (plugin, context) => this.createPluginContext(plugin, context),
3614
+ generatedOutputCount: () => this.generatedOutputs.count(),
3615
+ diagnosticCount: () => this.diagnostics.length
3616
+ });
3092
3617
  }
3093
3618
  pipelineOptions;
3094
3619
  buildLifecycle = null;
3095
- buildStage = "idle";
3096
3620
  disposed = false;
3097
3621
  diagnostics = [];
3098
3622
  pluginCaches = /* @__PURE__ */ new Map();
3099
3623
  generatedOutputs = new GeneratedOutputRegistry();
3100
3624
  isBuildTime = false;
3101
3625
  resolvedPlugins = null;
3102
- pageDeclarations = /* @__PURE__ */ new WeakMap();
3103
3626
  externalDependencies = /* @__PURE__ */ new Map();
3104
3627
  externalDependencyFailures = [];
3105
3628
  externalDependenciesResolved = false;
3106
- manifestHooksExecuted = 0;
3107
- manifestHookDurationMs = 0;
3108
- manifestHooksReused = 0;
3109
- manifestHookCache = [];
3110
- manifestHookMetricsByPlugin = /* @__PURE__ */ new Map();
3629
+ pageResolver;
3630
+ manifestHooks;
3111
3631
  getDiagnostics() {
3112
3632
  return this.diagnostics;
3113
3633
  }
3114
- getBuildStage() {
3115
- return this.buildStage;
3116
- }
3117
3634
  getManifestHookMetrics() {
3118
- return {
3119
- manifestHooksExecuted: this.manifestHooksExecuted,
3120
- manifestHooksReused: this.manifestHooksReused,
3121
- manifestHookCacheHit: this.manifestHooksReused,
3122
- manifestHookCacheMiss: this.manifestHooksExecuted,
3123
- manifestHookDurationMs: this.manifestHookDurationMs,
3124
- byPlugin: Object.fromEntries(this.manifestHookMetricsByPlugin)
3125
- };
3635
+ return this.manifestHooks.getMetrics();
3126
3636
  }
3127
3637
  getNoChangeFastPathBlockReason(previousCache, pipelineFingerprint) {
3128
- let cacheIndex = 0;
3129
- for (const plugin of this.plugins()) {
3130
- if (plugin.buildEnd) return "plugin-build-end";
3131
- if (plugin.planGeneratedOutputs) return "plugin-generated-output-plan";
3132
- if (!plugin.onManifestCreated) continue;
3133
- const cached = previousCache[cacheIndex];
3134
- cacheIndex += 1;
3135
- if (cached?.pluginName !== plugin.name || !cached.cacheable || cached.pipelineFingerprint !== pipelineFingerprint)
3136
- return "manifest-hook-cache-miss";
3137
- }
3138
- return void 0;
3638
+ return this.manifestHooks.getNoChangeFastPathBlockReason(
3639
+ previousCache,
3640
+ pipelineFingerprint
3641
+ );
3139
3642
  }
3140
3643
  getManifestHookCache() {
3141
- return this.manifestHookCache;
3644
+ return this.manifestHooks.getCache();
3142
3645
  }
3143
3646
  /**
3144
3647
  * Starts the plugin build lifecycle exactly once, regardless of which public
@@ -3204,124 +3707,51 @@ var PluginRuntime = class {
3204
3707
  const pluginContext = this.createPluginContext(plugin, context);
3205
3708
  const resolved = await runPluginHook(
3206
3709
  plugin,
3207
- "resolveContentLocations",
3208
- pluginContext,
3209
- resolver
3210
- );
3211
- locations.push(...resolved);
3212
- }
3213
- return locations;
3214
- }
3215
- async extendContentLocations(entries, locations, contentIndex) {
3216
- await this.runHook(
3217
- "extendContentLocations",
3218
- (plugin) => plugin.extendContentLocations,
3219
- {
3220
- ...this.createContext(contentIndex),
3221
- entries,
3222
- locations
3223
- }
3224
- );
3225
- }
3226
- async runPostHook(hookName, slug, markdown, content, contentIndex) {
3227
- await this.runHook(hookName, (plugin) => plugin[hookName], {
3228
- ...this.createContext(contentIndex),
3229
- slug,
3230
- markdown,
3231
- content
3232
- });
3233
- }
3234
- async runGraphHook(entries, contentIndex) {
3235
- await this.runHook(
3236
- "extendContentGraph",
3237
- (plugin) => plugin.extendContentGraph,
3238
- {
3239
- ...this.createContext(contentIndex),
3240
- entries
3241
- }
3242
- );
3243
- }
3244
- async runManifestCreated(manifest, contentIndex, previousCache = [], pipelineFingerprint) {
3245
- const context = { ...this.createContext(contentIndex), manifest };
3246
- let cacheIndex = 0;
3247
- for (const plugin of this.plugins()) {
3248
- const hook = plugin.onManifestCreated;
3249
- if (!hook) continue;
3250
- const inputFingerprint = fingerprintManifestHookInput(manifest);
3251
- const cached = previousCache[cacheIndex];
3252
- cacheIndex += 1;
3253
- const reason = manifestHookCacheMissReason(
3254
- cached,
3255
- plugin.name,
3256
- pipelineFingerprint,
3257
- inputFingerprint
3258
- );
3259
- const canRestore = cached?.pluginName === plugin.name && cached.cacheable && cached.pipelineFingerprint === pipelineFingerprint && cached.inputFingerprint === inputFingerprint;
3260
- const manifestBeforeRestore = canRestore ? {
3261
- entries: structuredClone(manifest.entries)
3262
- } : void 0;
3263
- const restored = canRestore && applyManifestHookSnapshot(manifest, cached.snapshot);
3264
- const cacheHit = restored && fingerprintManifestHookSnapshot(cached.snapshot, manifest) === cached.outputFingerprint;
3265
- if (cacheHit) {
3266
- this.manifestHooksReused += 1;
3267
- this.manifestHookCache.push(cached);
3268
- this.recordManifestHookMetric(plugin.name, "reused", "cache-hit", 0);
3269
- continue;
3270
- }
3271
- if (restored && manifestBeforeRestore)
3272
- applyManifestHookSnapshot(manifest, manifestBeforeRestore);
3273
- const sideEffectsBefore = fingerprintManifestHookSideEffects(manifest);
3274
- const outputCountBefore = this.generatedOutputs.count();
3275
- const diagnosticCountBefore = this.diagnostics.length;
3276
- const manifestDiagnosticCountBefore = manifest.diagnostics.length;
3277
- const started = performance.now();
3278
- this.manifestHooksExecuted += 1;
3279
- try {
3280
- await runPluginHook(
3281
- plugin,
3282
- "onManifestCreated",
3283
- this.createPluginContext(plugin, context),
3284
- hook
3285
- );
3286
- } finally {
3287
- const durationMs = performance.now() - started;
3288
- this.manifestHookDurationMs += durationMs;
3289
- this.recordManifestHookMetric(
3290
- plugin.name,
3291
- "executed",
3292
- reason,
3293
- durationMs
3294
- );
3295
- }
3296
- this.manifestHookCache.push(
3297
- createManifestHookCacheEntry(
3298
- plugin.name,
3299
- plugin.resolveExternalDependencies === void 0 && sideEffectsBefore === fingerprintManifestHookSideEffects(manifest) && outputCountBefore === this.generatedOutputs.count() && diagnosticCountBefore === this.diagnostics.length && manifestDiagnosticCountBefore === manifest.diagnostics.length,
3300
- pipelineFingerprint,
3301
- inputFingerprint,
3302
- manifest
3303
- )
3710
+ "resolveContentLocations",
3711
+ pluginContext,
3712
+ resolver
3304
3713
  );
3714
+ locations.push(...resolved);
3305
3715
  }
3716
+ return locations;
3306
3717
  }
3307
- recordManifestHookMetric(pluginName, outcome, reason, durationMs) {
3308
- const previous = this.manifestHookMetricsByPlugin.get(pluginName) ?? {
3309
- executed: 0,
3310
- reused: 0,
3311
- cacheHit: 0,
3312
- cacheMiss: 0,
3313
- durationMs: 0,
3314
- reason
3315
- };
3316
- this.manifestHookMetricsByPlugin.set(pluginName, {
3317
- executed: previous.executed + (outcome === "executed" ? 1 : 0),
3318
- reused: previous.reused + (outcome === "reused" ? 1 : 0),
3319
- cacheHit: previous.cacheHit + (outcome === "reused" ? 1 : 0),
3320
- cacheMiss: previous.cacheMiss + (outcome === "executed" ? 1 : 0),
3321
- durationMs: previous.durationMs + durationMs,
3322
- reason
3718
+ async extendContentLocations(entries, locations, contentIndex) {
3719
+ await this.runHook(
3720
+ "extendContentLocations",
3721
+ (plugin) => plugin.extendContentLocations,
3722
+ {
3723
+ ...this.createContext(contentIndex),
3724
+ entries,
3725
+ locations
3726
+ }
3727
+ );
3728
+ }
3729
+ async runPostHook(hookName, slug, markdown, content, contentIndex) {
3730
+ await this.runHook(hookName, (plugin) => plugin[hookName], {
3731
+ ...this.createContext(contentIndex),
3732
+ slug,
3733
+ markdown,
3734
+ content
3323
3735
  });
3324
3736
  }
3737
+ async runGraphHook(entries, contentIndex) {
3738
+ await this.runHook(
3739
+ "extendContentGraph",
3740
+ (plugin) => plugin.extendContentGraph,
3741
+ {
3742
+ ...this.createContext(contentIndex),
3743
+ entries
3744
+ }
3745
+ );
3746
+ }
3747
+ async runManifestCreated(manifest, contentIndex, previousCache = [], pipelineFingerprint) {
3748
+ await this.manifestHooks.runManifestCreated(
3749
+ manifest,
3750
+ contentIndex,
3751
+ previousCache,
3752
+ pipelineFingerprint
3753
+ );
3754
+ }
3325
3755
  async runBuildEnd(manifest, contentIndex) {
3326
3756
  const context = {
3327
3757
  ...this.createContext(contentIndex),
@@ -3404,118 +3834,27 @@ var PluginRuntime = class {
3404
3834
  });
3405
3835
  }
3406
3836
  async resolvePage(pathname, manifest, contentIndex) {
3407
- const matches = [];
3408
- const requestedPathname = normalizePagePath(pathname);
3409
- for (const plugin of this.plugins()) {
3410
- for (const pageType of plugin.pageTypes ?? []) {
3411
- const pagePathname = normalizePageTypePath(pathname, pageType);
3412
- const context = {
3413
- ...this.createContext(contentIndex),
3414
- manifest,
3415
- pathname: pagePathname
3416
- };
3417
- const page = await pageType.resolve(
3418
- this.createPluginContext(plugin, context)
3419
- );
3420
- if (page) {
3421
- matches.push({
3422
- page: {
3423
- ...page,
3424
- pathname: pageType.directoryIndex ? normalizeDirectoryPagePath(page.pathname) : page.pathname,
3425
- type: pageType.id,
3426
- pluginName: plugin.name
3427
- },
3428
- priority: pageType.priority ?? 0
3429
- });
3430
- }
3431
- }
3432
- }
3433
- if (matches.length === 0) return null;
3434
- const highestPriority = Math.max(...matches.map((match) => match.priority));
3435
- const highest = matches.filter(
3436
- (match) => match.priority === highestPriority
3837
+ return await this.pageResolver.resolvePage(
3838
+ pathname,
3839
+ manifest,
3840
+ contentIndex
3437
3841
  );
3438
- if (highest.length !== 1) {
3439
- throw new Error(
3440
- `Multiple plugin page types match ${requestedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
3441
- );
3442
- }
3443
- return highest[0].page;
3444
3842
  }
3445
3843
  async getPagePaths(manifest, contentIndex) {
3446
- return [
3447
- ...new Set(
3448
- (await this.getPageDeclarations(manifest, contentIndex)).map(
3449
- (declaration) => declaration.pathname
3450
- )
3451
- )
3452
- ];
3844
+ return await this.pageResolver.getPagePaths(manifest, contentIndex);
3453
3845
  }
3454
3846
  async getPageRoutes(manifest, contentIndex) {
3455
- return (await this.getPageDeclarations(manifest, contentIndex)).map(
3456
- ({ pathname, plugin, pageType }) => ({
3457
- pathname,
3458
- pluginName: plugin.name,
3459
- pageType: pageType.id
3460
- })
3461
- );
3847
+ return await this.pageResolver.getPageRoutes(manifest, contentIndex);
3462
3848
  }
3463
3849
  async getPageOutputs(manifest, contentIndex) {
3464
- const outputs = await Promise.all(
3465
- (await this.getPageDeclarations(manifest, contentIndex)).map(
3466
- async ({ pathname, plugin, pageType }) => {
3467
- const context = {
3468
- ...this.createContext(contentIndex),
3469
- manifest,
3470
- pathname
3471
- };
3472
- const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
3473
- this.createPluginContext(plugin, context)
3474
- ) : pageType.outputDependencies : [{ type: "unknown" }];
3475
- return {
3476
- output: {
3477
- kind: "plugin-page",
3478
- path: pathname,
3479
- producer: `plugin:${plugin.name}:page:${pageType.id}`,
3480
- dependencies
3481
- },
3482
- priority: pageType.priority ?? 0
3483
- };
3484
- }
3485
- )
3486
- );
3487
- return uniqueOutputs(outputs);
3488
- }
3489
- async getPageDeclarations(manifest, contentIndex) {
3490
- const existing = this.pageDeclarations.get(manifest);
3491
- if (existing) return await existing;
3492
- const declarations = this.resolvePageDeclarations(manifest, contentIndex);
3493
- this.pageDeclarations.set(manifest, declarations);
3494
- return await declarations;
3495
- }
3496
- async resolvePageDeclarations(manifest, contentIndex) {
3497
- const declarations = [];
3498
- const context = { ...this.createContext(contentIndex), manifest };
3499
- for (const plugin of this.plugins()) {
3500
- for (const pageType of plugin.pageTypes ?? []) {
3501
- const paths = pageType.paths;
3502
- if (!paths) continue;
3503
- const resolved = typeof paths === "function" ? await paths(this.createPluginContext(plugin, context)) : paths;
3504
- for (const pathname of resolved) {
3505
- declarations.push({
3506
- pathname: normalizePageTypePath(pathname, pageType),
3507
- plugin,
3508
- pageType
3509
- });
3510
- }
3511
- }
3512
- }
3513
- return declarations;
3850
+ return await this.pageResolver.getPageOutputs(manifest, contentIndex);
3514
3851
  }
3515
3852
  getContentOutputDependencies() {
3516
- return this.plugins().flatMap(
3517
- (plugin) => plugin.contentOutputDependencies ?? []
3518
- );
3853
+ const plugins = this.plugins();
3854
+ return ({ entry, manifest }) => plugins.flatMap((plugin) => [
3855
+ ...plugin.contentOutputDependencies ?? [],
3856
+ ...plugin.resolveContentOutputDependencies?.({ entry, manifest }) ?? []
3857
+ ]);
3519
3858
  }
3520
3859
  async collectDiagnostics(contentIndex) {
3521
3860
  const tracer = this.observability().tracer;
@@ -3550,28 +3889,17 @@ var PluginRuntime = class {
3550
3889
  const context = this.createContext(contentIndex);
3551
3890
  const createPluginContext = (plugin) => this.createPluginContext(plugin, context);
3552
3891
  const stages = [
3553
- {
3554
- stage: "setup",
3555
- run: () => runSetup(this.plugins(), createPluginContext)
3556
- },
3557
- {
3558
- stage: "buildStart",
3559
- run: () => runBuildStart(this.plugins(), createPluginContext)
3560
- },
3561
- {
3562
- stage: "onConfigResolved",
3563
- run: () => this.runHook(
3564
- "onConfigResolved",
3565
- (plugin) => plugin.onConfigResolved,
3566
- context
3567
- )
3568
- }
3892
+ () => runSetup(this.plugins(), createPluginContext),
3893
+ () => runBuildStart(this.plugins(), createPluginContext),
3894
+ () => this.runHook(
3895
+ "onConfigResolved",
3896
+ (plugin) => plugin.onConfigResolved,
3897
+ context
3898
+ )
3569
3899
  ];
3570
- for (const { stage, run } of stages) {
3571
- this.buildStage = stage;
3900
+ for (const run of stages) {
3572
3901
  await run();
3573
3902
  }
3574
- this.buildStage = "started";
3575
3903
  }
3576
3904
  createContext(contentIndex) {
3577
3905
  return {
@@ -3639,101 +3967,8 @@ function deepFreeze(value) {
3639
3967
  for (const property of Object.values(value)) deepFreeze(property);
3640
3968
  return value;
3641
3969
  }
3642
- function collectPageTypeOwners(plugins) {
3643
- const pageTypes = /* @__PURE__ */ new Map();
3644
- for (const plugin of plugins) {
3645
- const declaredPageTypes = plugin.pageTypes;
3646
- if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
3647
- throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
3648
- }
3649
- for (const pageType of declaredPageTypes ?? []) {
3650
- validatePageType(plugin.name, pageType);
3651
- const previous = pageTypes.get(pageType.id);
3652
- if (previous) {
3653
- throw new Error(
3654
- `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
3655
- );
3656
- }
3657
- pageTypes.set(pageType.id, plugin.name);
3658
- }
3659
- }
3660
- }
3661
- function validatePageType(pluginName, pageType) {
3662
- if (typeof pageType !== "object" || pageType === null) {
3663
- throw new TypeError(`Plugin ${pluginName} pageTypes must contain objects`);
3664
- }
3665
- const candidate = pageType;
3666
- if (typeof candidate.id !== "string" || candidate.id.trim() === "") {
3667
- throw new TypeError(
3668
- `Plugin ${pluginName} page type id must be a non-empty string`
3669
- );
3670
- }
3671
- if (typeof candidate.resolve !== "function") {
3672
- throw new TypeError(
3673
- `Plugin ${pluginName} page type "${candidate.id}" must provide a resolve function`
3674
- );
3675
- }
3676
- if (candidate.paths !== void 0 && !Array.isArray(candidate.paths) && typeof candidate.paths !== "function") {
3677
- throw new TypeError(
3678
- `Plugin ${pluginName} page type "${candidate.id}" paths must be an array or function`
3679
- );
3680
- }
3681
- if (candidate.priority !== void 0 && (typeof candidate.priority !== "number" || !Number.isFinite(candidate.priority))) {
3682
- throw new TypeError(
3683
- `Plugin ${pluginName} page type "${candidate.id}" priority must be a finite number`
3684
- );
3685
- }
3686
- if (candidate.outputDependencies !== void 0 && !Array.isArray(candidate.outputDependencies) && typeof candidate.outputDependencies !== "function") {
3687
- throw new TypeError(
3688
- `Plugin ${pluginName} page type "${candidate.id}" outputDependencies must be an array or function`
3689
- );
3690
- }
3691
- if (candidate.directoryIndex !== void 0 && typeof candidate.directoryIndex !== "boolean") {
3692
- throw new TypeError(
3693
- `Plugin ${pluginName} page type "${candidate.id}" directoryIndex must be a boolean`
3694
- );
3695
- }
3696
- }
3697
- function normalizePageTypePath(pathname, pageType) {
3698
- return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
3699
- }
3700
- function normalizeDirectoryPagePath(pathname) {
3701
- const path6 = normalizePagePath(pathname);
3702
- return path6 === "/" ? path6 : `${path6}/`;
3703
- }
3704
- function manifestHookCacheMissReason(cached, pluginName, pipelineFingerprint, inputFingerprint) {
3705
- if (!cached) return "cache-missing";
3706
- if (cached.pluginName !== pluginName) return "plugin-order-changed";
3707
- if (!cached.cacheable) return "uncacheable";
3708
- if (cached.pipelineFingerprint !== pipelineFingerprint)
3709
- return "pipeline-fingerprint-changed";
3710
- if (cached.inputFingerprint !== inputFingerprint)
3711
- return "manifest-input-changed";
3712
- return "snapshot-invalid";
3713
- }
3714
- function normalizePagePath(pathname) {
3715
- const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3716
- return path6 === "/" ? path6 : path6.replace(/\/$/, "");
3717
- }
3718
- function countSeverity(diagnostics, severity) {
3719
- return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
3720
- }
3721
- function uniqueOutputs(outputs) {
3722
- const byPath = /* @__PURE__ */ new Map();
3723
- for (const output of outputs) {
3724
- const previous = byPath.get(output.output.path);
3725
- if (!previous || output.priority > previous.priority) {
3726
- byPath.set(output.output.path, output);
3727
- continue;
3728
- }
3729
- if (output.output.producer === previous.output.producer) continue;
3730
- if (output.priority === previous.priority) {
3731
- throw new Error(
3732
- `Multiple plugin page types declare ${output.output.path} at priority ${output.priority}: ${previous.output.producer}, ${output.output.producer}`
3733
- );
3734
- }
3735
- }
3736
- return [...byPath.values()].map(({ output }) => output).sort((left, right) => left.path.localeCompare(right.path));
3970
+ function countSeverity(diagnostics, severity) {
3971
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
3737
3972
  }
3738
3973
 
3739
3974
  // src/utils/error.ts
@@ -4106,7 +4341,8 @@ var ContentBuildCoordinator = class {
4106
4341
  {},
4107
4342
  () => fingerprintContentEntries(
4108
4343
  entries,
4109
- (entry) => this.dependencies.readEntry(entry)
4344
+ (entry) => this.dependencies.readEntry(entry),
4345
+ this.dependencies.includeFileMetadata
4110
4346
  )
4111
4347
  );
4112
4348
  const previousState = await this.dependencies.observability.tracer.span(
@@ -4412,7 +4648,7 @@ var ContentLocationResolver = class {
4412
4648
  this.dependencies.getContentIndex()
4413
4649
  ]);
4414
4650
  await this.dependencies.pluginRuntime.startBuild(contentIndex);
4415
- const inputs = await mapConcurrent2(
4651
+ const inputs = await mapConcurrent(
4416
4652
  entries.filter((entry) => entry.path.endsWith(".md")),
4417
4653
  64,
4418
4654
  async (entry) => ({
@@ -4473,24 +4709,52 @@ var ContentLocationResolver = class {
4473
4709
  function toSlug2(path6) {
4474
4710
  return path6.replace(/\.md$/, "");
4475
4711
  }
4476
- async function mapConcurrent2(values, concurrency, map) {
4477
- const results = new Array(values.length);
4478
- let nextIndex = 0;
4479
- async function worker() {
4480
- while (nextIndex < values.length) {
4481
- const index = nextIndex;
4482
- nextIndex += 1;
4483
- const value = values[index];
4484
- if (value !== void 0) results[index] = await map(value);
4485
- }
4712
+
4713
+ // src/content/content_manifest_reuse.ts
4714
+ function reuseManifestEntry(input) {
4715
+ const { plan, slug } = input;
4716
+ if (!plan.previousState?.manifestEntries) return null;
4717
+ if (plan.previousState.pipelineFingerprint !== input.pipelineFingerprint) {
4718
+ return null;
4486
4719
  }
4487
- await Promise.all(
4488
- Array.from(
4489
- { length: Math.min(concurrency, values.length) },
4490
- () => worker()
4491
- )
4492
- );
4493
- return results;
4720
+ if (plan.affectedContent.direct.has(slug)) return null;
4721
+ if (plan.affectedContent.dependent.has(slug)) return null;
4722
+ const contentCacheFingerprint = input.getContentCacheFingerprint();
4723
+ if (!contentCacheFingerprint) return null;
4724
+ if (plan.previousState.entries[`${slug}.md`]?.processedContentCacheFingerprint !== contentCacheFingerprint) {
4725
+ return null;
4726
+ }
4727
+ const entry = plan.previousManifestEntriesBySlug.get(slug);
4728
+ if (!entry) return null;
4729
+ return {
4730
+ entry: cloneManifestEntry(entry),
4731
+ contentCacheFingerprint
4732
+ };
4733
+ }
4734
+ function cloneManifestEntry(entry) {
4735
+ return {
4736
+ ...entry,
4737
+ publicLocation: { ...entry.publicLocation },
4738
+ frontmatter: { ...entry.frontmatter },
4739
+ ...entry.file ? {
4740
+ file: {
4741
+ ...entry.file.size === void 0 ? {} : { size: entry.file.size },
4742
+ ...reviveFileDate(entry.file.ctime, "ctime"),
4743
+ ...reviveFileDate(entry.file.mtime, "mtime")
4744
+ }
4745
+ } : {},
4746
+ aliases: [...entry.aliases ?? []],
4747
+ tags: [...entry.tags],
4748
+ links: entry.links.map((link) => ({ ...link })),
4749
+ backlinks: [...entry.backlinks],
4750
+ assets: entry.assets.map((asset) => ({ ...asset })),
4751
+ ...entry.headTags ? { headTags: [...entry.headTags] } : {}
4752
+ };
4753
+ }
4754
+ function reviveFileDate(value, key) {
4755
+ if (value === void 0) return {};
4756
+ const date = value instanceof Date ? value : new Date(value);
4757
+ return Number.isNaN(date.getTime()) ? {} : { [key]: date };
4494
4758
  }
4495
4759
 
4496
4760
  // src/content/file_system_content_source.ts
@@ -4583,12 +4847,25 @@ var FileSystemContentSource = class {
4583
4847
  continue;
4584
4848
  }
4585
4849
  if (!directoryEntry.isFile()) continue;
4586
- const stats = await fs4.stat(filePath);
4850
+ let stats;
4851
+ try {
4852
+ stats = await fs4.stat(filePath);
4853
+ } catch (error) {
4854
+ if (isFileNotFoundError(error)) continue;
4855
+ throw new Error(
4856
+ `Could not read content file metadata: ${logicalPath}`,
4857
+ { cause: error }
4858
+ );
4859
+ }
4860
+ const createdAt = validTimestamp(stats.birthtimeMs, logicalPath, "ctime");
4861
+ const modifiedAt = validTimestamp(stats.mtimeMs, logicalPath, "mtime");
4862
+ const size = validSize(stats.size, logicalPath);
4587
4863
  entries.push({
4588
4864
  path: logicalPath,
4589
4865
  metadata: {
4590
- modifiedAt: stats.mtimeMs,
4591
- size: stats.size
4866
+ createdAt,
4867
+ modifiedAt,
4868
+ size
4592
4869
  }
4593
4870
  });
4594
4871
  }
@@ -4661,6 +4938,14 @@ function isMissingDirectoryError(error, directory) {
4661
4938
  function isFileNotFoundError(error) {
4662
4939
  return error instanceof Error && "code" in error && error.code === "ENOENT";
4663
4940
  }
4941
+ function validTimestamp(value, logicalPath, name) {
4942
+ if (Number.isFinite(value)) return value;
4943
+ throw new Error(`Invalid ${name} metadata for content file: ${logicalPath}`);
4944
+ }
4945
+ function validSize(value, logicalPath) {
4946
+ if (Number.isSafeInteger(value) && value >= 0) return value;
4947
+ throw new Error(`Invalid size metadata for content file: ${logicalPath}`);
4948
+ }
4664
4949
  function missingContentDirectoryError(directory) {
4665
4950
  const relative = path4.relative(process.cwd(), directory);
4666
4951
  const display = relative && !relative.startsWith("..") ? relative : directory;
@@ -4696,7 +4981,7 @@ function readOptionalStableId(value) {
4696
4981
 
4697
4982
  // src/content/manifest_builder.ts
4698
4983
  var ManifestBuilder = class {
4699
- createEntry(slug, markdown, processed, contentIndex, location, publishing) {
4984
+ createEntry(slug, markdown, processed, contentIndex, location, publishing, sourceMetadata) {
4700
4985
  const links = extractContentLinks(markdown, contentIndex);
4701
4986
  const assets = [
4702
4987
  ...links.filter(
@@ -4713,6 +4998,7 @@ var ManifestBuilder = class {
4713
4998
  permalink: location.permalink,
4714
4999
  publicLocation: location,
4715
5000
  title: getManifestTitle(slug, processed.frontmatter.title),
5001
+ file: toFileMetadata(sourceMetadata),
4716
5002
  aliases: extractFrontmatterAliases(markdown),
4717
5003
  frontmatter: processed.frontmatter,
4718
5004
  publishing,
@@ -4779,6 +5065,21 @@ var ManifestBuilder = class {
4779
5065
  return { ...manifest, graph: createContentGraph(manifest) };
4780
5066
  }
4781
5067
  };
5068
+ function toFileMetadata(metadata) {
5069
+ return {
5070
+ ...validSize2(metadata?.size) ? { size: metadata.size } : {},
5071
+ ...toDate(metadata?.createdAt, "ctime"),
5072
+ ...toDate(metadata?.modifiedAt, "mtime")
5073
+ };
5074
+ }
5075
+ function validSize2(value) {
5076
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
5077
+ }
5078
+ function toDate(value, key) {
5079
+ if (typeof value !== "number" || !Number.isFinite(value)) return {};
5080
+ const date = new Date(value);
5081
+ return Number.isNaN(date.getTime()) ? {} : { [key]: date };
5082
+ }
4782
5083
  function createAliasIndex(entries) {
4783
5084
  const byAlias = /* @__PURE__ */ new Map();
4784
5085
  for (const entry of entries) {
@@ -4823,6 +5124,64 @@ function appendUniqueToMap(map, key, value) {
4823
5124
  map.set(key, values);
4824
5125
  }
4825
5126
 
5127
+ // src/content/manifest_assembler.ts
5128
+ var ManifestAssembler = class {
5129
+ constructor(contentCache) {
5130
+ this.contentCache = contentCache;
5131
+ }
5132
+ contentCache;
5133
+ builder = new ManifestBuilder();
5134
+ createEntry(...args) {
5135
+ return this.builder.createEntry(...args);
5136
+ }
5137
+ build(...args) {
5138
+ return this.builder.build(...args);
5139
+ }
5140
+ applyPublicView(manifest, strategy, publishingBuildTime) {
5141
+ for (const entry of manifest.entries) {
5142
+ entry.publishing = resolvePublishingState(entry.frontmatter, {
5143
+ strategy,
5144
+ buildTime: publishingBuildTime
5145
+ });
5146
+ }
5147
+ manifest.publicEntries = manifest.entries.filter(
5148
+ (entry) => entry.publishing.routable
5149
+ );
5150
+ manifest.discoverableEntries = manifest.entries.filter(
5151
+ (entry) => entry.publishing.discoverable
5152
+ );
5153
+ const publicSlugs = new Set(
5154
+ manifest.publicEntries.map((entry) => entry.slug)
5155
+ );
5156
+ const publicPermalinks = new Map(
5157
+ manifest.publicEntries.map((entry) => [entry.permalink, entry])
5158
+ );
5159
+ manifest.byRoutablePermalink = publicPermalinks;
5160
+ manifest.publicRedirects = new Map(
5161
+ [...manifest.redirects].filter(
5162
+ ([, redirect]) => publicSlugs.has(redirect.slug)
5163
+ )
5164
+ );
5165
+ }
5166
+ synchronizeHtml(manifest, preHookEntries) {
5167
+ const preHookHtml = new Map(
5168
+ preHookEntries.map((entry) => [entry.slug, entry.html])
5169
+ );
5170
+ for (const entry of manifest.entries) {
5171
+ const changed = preHookHtml.get(entry.slug) !== entry.html;
5172
+ const cached = this.contentCache.get(entry.slug);
5173
+ if (cached) {
5174
+ if (changed) cached.html = entry.html;
5175
+ continue;
5176
+ }
5177
+ this.contentCache.set(entry.slug, {
5178
+ frontmatter: entry.frontmatter,
5179
+ html: entry.html
5180
+ });
5181
+ }
5182
+ }
5183
+ };
5184
+
4826
5185
  // src/content/output_dependency.ts
4827
5186
  function determineOutputChanges(input) {
4828
5187
  const current = buildOutputInventory(
@@ -4833,7 +5192,16 @@ function determineOutputChanges(input) {
4833
5192
  const previous = new Map(
4834
5193
  (input.previousState?.outputs ?? []).map((output) => [output.path, output])
4835
5194
  );
5195
+ const currentPublicLocations = publicLocationOwners(
5196
+ input.manifest.publicEntries
5197
+ );
5198
+ const previousPublicLocations = publicLocationOwners(
5199
+ (input.previousState?.manifestEntries ?? []).filter(
5200
+ (entry) => entry.publishing.routable
5201
+ )
5202
+ );
4836
5203
  const changedContent = collectChangedContent(input);
5204
+ const refreshedContent = collectRefreshedContent(input);
4837
5205
  const changedFiles = /* @__PURE__ */ new Set([
4838
5206
  ...input.changeSet.added,
4839
5207
  ...input.changeSet.changed,
@@ -4855,13 +5223,18 @@ function determineOutputChanges(input) {
4855
5223
  const unchanged = [];
4856
5224
  for (const output of current) {
4857
5225
  if (isOutputAffected(
4858
- output,
5226
+ withPreviousDependencies(output, previous.get(output.path)),
4859
5227
  changedContent,
5228
+ refreshedContent,
4860
5229
  changedTags,
4861
5230
  changedFolders,
4862
5231
  changedFiles,
4863
5232
  changedExternal,
4864
- changedExternalScopes
5233
+ changedExternalScopes,
5234
+ currentPublicLocations,
5235
+ previousPublicLocations,
5236
+ input.previousState?.manifestEntries ?? [],
5237
+ input.manifest.entries
4865
5238
  )) {
4866
5239
  affected.set(output.path, output);
4867
5240
  } else {
@@ -4897,7 +5270,10 @@ function determineOutputChanges(input) {
4897
5270
  function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDependencies = []) {
4898
5271
  const outputs = /* @__PURE__ */ new Map();
4899
5272
  for (const entry of manifest.publicEntries) {
4900
- addOutput(outputs, contentOutput(entry, contentOutputDependencies));
5273
+ addOutput(
5274
+ outputs,
5275
+ contentOutput(entry, manifest, contentOutputDependencies)
5276
+ );
4901
5277
  }
4902
5278
  for (const [path6, redirect] of manifest.publicRedirects) {
4903
5279
  addOutput(outputs, {
@@ -4930,21 +5306,26 @@ function htmlOutputPath(pathname) {
4930
5306
  if (pathname.endsWith("/")) return `${withoutSlash}/index.html`;
4931
5307
  return `${withoutSlash}.html`;
4932
5308
  }
4933
- function contentOutput(entry, dependencies) {
5309
+ function contentOutput(entry, manifest, dependencies) {
4934
5310
  return {
4935
5311
  kind: "content",
4936
5312
  path: htmlOutputPath(entry.permalink),
4937
5313
  producer: `content:${entry.slug}`,
4938
- dependencies: [{ type: "content", slug: entry.slug }, ...dependencies]
5314
+ dependencies: [
5315
+ { type: "content", slug: entry.slug },
5316
+ ...resolveContentOutputDependencies(dependencies, entry, manifest)
5317
+ ]
4939
5318
  };
4940
5319
  }
4941
- function isOutputAffected(output, changedContent, changedTags, changedFolders, changedFiles, changedExternal, changedExternalScopes) {
5320
+ function isOutputAffected(output, changedContent, refreshedContent, changedTags, changedFolders, changedFiles, changedExternal, changedExternalScopes, currentPublicLocations, previousPublicLocations, previousEntries, currentEntries) {
4942
5321
  if (isAffectedByExternalScope(output, changedExternalScopes)) return true;
4943
5322
  for (const dependency of output.dependencies) {
4944
5323
  if (dependency.type === "unknown") return true;
4945
5324
  if (dependency.type === "external" && changedExternal.has(dependency.id))
4946
5325
  return true;
4947
5326
  if (dependency.type === "global") return changedContent.size > 0;
5327
+ if (dependency.type === "content-query" && isContentQueryAffected(dependency, previousEntries, currentEntries))
5328
+ return true;
4948
5329
  if (dependency.type === "projection" && (dependency.projection === "navigation" || dependency.projection === "recent-posts")) {
4949
5330
  return changedContent.size > 0;
4950
5331
  }
@@ -4954,7 +5335,7 @@ function isOutputAffected(output, changedContent, changedTags, changedFolders, c
4954
5335
  if (dependency.type === "projection" && dependency.projection === "taxonomy-folders") {
4955
5336
  return changedFolders.size > 0;
4956
5337
  }
4957
- if (dependency.type === "content" && changedContent.has(dependency.slug)) {
5338
+ if (dependency.type === "content" && (changedContent.has(dependency.slug) || refreshedContent.has(dependency.slug))) {
4958
5339
  return true;
4959
5340
  }
4960
5341
  if (dependency.type === "tag" && changedTags.has(dependency.tag))
@@ -4965,9 +5346,98 @@ function isOutputAffected(output, changedContent, changedTags, changedFolders, c
4965
5346
  if (dependency.type === "file" && changedFiles.has(dependency.path)) {
4966
5347
  return true;
4967
5348
  }
5349
+ if (dependency.type === "public-location") {
5350
+ const currentOwner = currentPublicLocations.get(
5351
+ normalizePublicPath(dependency.pathname)
5352
+ );
5353
+ const previousOwner = previousPublicLocations.get(
5354
+ normalizePublicPath(dependency.pathname)
5355
+ );
5356
+ if (currentOwner?.slug !== previousOwner?.slug) return true;
5357
+ if (currentOwner && (changedContent.has(currentOwner.slug) || refreshedContent.has(currentOwner.slug)) || previousOwner && (changedContent.has(previousOwner.slug) || refreshedContent.has(previousOwner.slug))) {
5358
+ return true;
5359
+ }
5360
+ }
5361
+ }
5362
+ return false;
5363
+ }
5364
+ function isContentQueryAffected(dependency, previousEntries, currentEntries) {
5365
+ const previous = new Map(previousEntries.map((entry) => [entry.slug, entry]));
5366
+ const current = new Map(currentEntries.map((entry) => [entry.slug, entry]));
5367
+ const slugs = /* @__PURE__ */ new Set([...previous.keys(), ...current.keys()]);
5368
+ for (const slug of slugs) {
5369
+ const before = previous.get(slug);
5370
+ const after = current.get(slug);
5371
+ if (!before || !after) {
5372
+ if ((before ?? after)?.publishing.discoverable) return true;
5373
+ continue;
5374
+ }
5375
+ if (before.publishing.discoverable !== after.publishing.discoverable)
5376
+ return true;
5377
+ if (!before.publishing.discoverable && !after.publishing.discoverable)
5378
+ continue;
5379
+ if (dependency.unknown || dependency.properties.some(
5380
+ (property) => propertyChanged(before, after, property)
5381
+ ))
5382
+ return true;
4968
5383
  }
4969
5384
  return false;
4970
5385
  }
5386
+ function propertyChanged(before, after, property) {
5387
+ if (property === "*")
5388
+ return JSON.stringify(before.frontmatter) !== JSON.stringify(after.frontmatter);
5389
+ return JSON.stringify(contentQueryValue(before, property)) !== JSON.stringify(contentQueryValue(after, property));
5390
+ }
5391
+ function contentQueryValue(entry, property) {
5392
+ switch (property) {
5393
+ case "file.name":
5394
+ return `${entry.slug.split("/").at(-1) ?? entry.slug}.md`;
5395
+ case "file.path":
5396
+ return `${entry.slug}.md`;
5397
+ case "file.slug":
5398
+ return entry.slug;
5399
+ case "file.folder":
5400
+ return entry.slug.split("/").slice(0, -1).join("/");
5401
+ case "file.title":
5402
+ return entry.title;
5403
+ case "file.link":
5404
+ case "file.permalink":
5405
+ return entry.permalink;
5406
+ case "file.tags":
5407
+ return entry.tags;
5408
+ case "file.size":
5409
+ return entry.file?.size;
5410
+ case "file.ctime":
5411
+ return entry.file?.ctime;
5412
+ case "file.mtime":
5413
+ return entry.file?.mtime;
5414
+ case "file.links":
5415
+ return entry.links;
5416
+ default:
5417
+ return property.split(".").reduce(
5418
+ (value, key) => value && typeof value === "object" && Object.hasOwn(value, key) ? value[key] : void 0,
5419
+ entry.frontmatter
5420
+ );
5421
+ }
5422
+ }
5423
+ function resolveContentOutputDependencies(dependencies, entry, manifest) {
5424
+ return typeof dependencies === "function" ? dependencies({ entry, manifest }) : dependencies;
5425
+ }
5426
+ function withPreviousDependencies(output, previous) {
5427
+ if (!previous || previous.kind !== output.kind) return output;
5428
+ return {
5429
+ ...output,
5430
+ dependencies: [...output.dependencies, ...previous.dependencies]
5431
+ };
5432
+ }
5433
+ function publicLocationOwners(entries) {
5434
+ return new Map(
5435
+ entries.map((entry) => [
5436
+ normalizePublicPath(entry.publicLocation.permalink),
5437
+ entry
5438
+ ])
5439
+ );
5440
+ }
4971
5441
  function isAffectedByExternalScope(output, changedExternalScopes) {
4972
5442
  for (const scope of changedExternalScopes.values()) {
4973
5443
  if (scope.type === "all") return true;
@@ -5014,6 +5484,21 @@ function changedExternalDependencyScopes(input) {
5014
5484
  }
5015
5485
  return changed;
5016
5486
  }
5487
+ function collectRefreshedContent(input) {
5488
+ const refreshed = /* @__PURE__ */ new Set();
5489
+ const fingerprints = input.contentFingerprints;
5490
+ const previousEntries = input.previousState?.entries;
5491
+ if (!fingerprints || !previousEntries) return refreshed;
5492
+ for (const entry of input.manifest.entries) {
5493
+ const fingerprint = fingerprints.get(entry.slug);
5494
+ if (fingerprint === void 0) continue;
5495
+ if (previousEntries[`${entry.slug}.md`]?.processedContentCacheFingerprint === fingerprint) {
5496
+ continue;
5497
+ }
5498
+ refreshed.add(entry.slug);
5499
+ }
5500
+ return refreshed;
5501
+ }
5017
5502
  function collectChangedContent(input) {
5018
5503
  const changed = /* @__PURE__ */ new Set([
5019
5504
  ...input.affectedContent.direct,
@@ -5141,6 +5626,7 @@ var ContentManager = class {
5141
5626
  }
5142
5627
  };
5143
5628
  this.contentIndexBuilder = new ContentIndexBuilder(this.source);
5629
+ this.manifestAssembler = new ManifestAssembler(this.contentCache);
5144
5630
  this.pluginRuntime = new PluginRuntime(this.pipelineOptions);
5145
5631
  const buildStatePath = resolveContentBuildStatePath(
5146
5632
  pipelineOptions.config,
@@ -5159,13 +5645,16 @@ var ContentManager = class {
5159
5645
  buildStatePath,
5160
5646
  getEntries: () => this.entryReader.getEntries(),
5161
5647
  readEntry: (entry) => this.entryReader.read(entry),
5162
- observability
5648
+ observability,
5649
+ includeFileMetadata: this.pipelineOptions.plugins?.some(
5650
+ (plugin) => plugin && plugin.contentFileMetadata === true
5651
+ ) ?? false
5163
5652
  });
5164
5653
  }
5165
5654
  pipelineOptions;
5166
5655
  source;
5167
5656
  contentIndexBuilder;
5168
- manifestBuilder = new ManifestBuilder();
5657
+ manifestAssembler;
5169
5658
  pluginRuntime;
5170
5659
  contentIndex = null;
5171
5660
  contentIndexAmbiguities = /* @__PURE__ */ new Map();
@@ -5327,10 +5816,13 @@ var ContentManager = class {
5327
5816
  this.getAllPosts(),
5328
5817
  this.getContentIndex(plan)
5329
5818
  ]);
5819
+ const entriesByPath = new Map(
5820
+ (await this.scan()).map((entry) => [entry.path, entry])
5821
+ );
5330
5822
  const entries = await this.observability().tracer.span(
5331
5823
  "content.manifest.entries",
5332
5824
  {},
5333
- () => mapConcurrent3(
5825
+ () => mapConcurrent(
5334
5826
  posts,
5335
5827
  CONTENT_PROCESSING_CONCURRENCY,
5336
5828
  async (post) => {
@@ -5348,7 +5840,7 @@ var ContentManager = class {
5348
5840
  `Content public location was not resolved: ${post.slug}`
5349
5841
  );
5350
5842
  }
5351
- return this.manifestBuilder.createEntry(
5843
+ return this.manifestAssembler.createEntry(
5352
5844
  post.slug,
5353
5845
  rawPost,
5354
5846
  processed,
@@ -5357,7 +5849,8 @@ var ContentManager = class {
5357
5849
  resolvePublishingState(processed.frontmatter, {
5358
5850
  strategy: this.publishStrategy(),
5359
5851
  buildTime: this.publishingBuildTime
5360
- })
5852
+ }),
5853
+ entriesByPath.get(`${post.slug}.md`)?.metadata
5361
5854
  );
5362
5855
  }
5363
5856
  )
@@ -5366,7 +5859,7 @@ var ContentManager = class {
5366
5859
  const manifest = await this.observability().tracer.span(
5367
5860
  "content.graph",
5368
5861
  {},
5369
- () => this.manifestBuilder.build(
5862
+ () => this.manifestAssembler.build(
5370
5863
  entries,
5371
5864
  contentIndex,
5372
5865
  this.contentIndexAmbiguities
@@ -5374,7 +5867,11 @@ var ContentManager = class {
5374
5867
  );
5375
5868
  const cacheManifestEntries = structuredClone(manifest.entries);
5376
5869
  this.locationResolver.populateRedirects(manifest, locations);
5377
- this.applyPublicView(manifest);
5870
+ this.manifestAssembler.applyPublicView(
5871
+ manifest,
5872
+ this.publishStrategy(),
5873
+ this.publishingBuildTime
5874
+ );
5378
5875
  manifest.pagePaths = [
5379
5876
  ...await this.pluginRuntime.getPagePaths(manifest, contentIndex)
5380
5877
  ];
@@ -5387,7 +5884,7 @@ var ContentManager = class {
5387
5884
  plan.previousState?.manifestHookCache,
5388
5885
  this.getPipelineFingerprint()
5389
5886
  );
5390
- this.synchronizeManifestHtml(manifest, cacheManifestEntries);
5887
+ this.manifestAssembler.synchronizeHtml(manifest, cacheManifestEntries);
5391
5888
  manifest.assets = this.pluginRuntime.collectAssets();
5392
5889
  manifest.clientEntries = this.pluginRuntime.collectClientEntries();
5393
5890
  manifest.diagnostics = [
@@ -5414,7 +5911,8 @@ var ContentManager = class {
5414
5911
  pluginPageOutputs,
5415
5912
  contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies(),
5416
5913
  externalDependencies: plan.externalDependencies,
5417
- externalDependencyFailures: plan.externalDependencyFailures
5914
+ externalDependencyFailures: plan.externalDependencyFailures,
5915
+ contentFingerprints: this.processedContentCacheFingerprints
5418
5916
  })
5419
5917
  );
5420
5918
  if (this.outputChangeSet) {
@@ -5565,74 +6063,24 @@ var ContentManager = class {
5565
6063
  async getContentLocations() {
5566
6064
  return await this.locationResolver.getLocations();
5567
6065
  }
5568
- applyPublicView(manifest) {
5569
- const strategy = this.publishStrategy();
5570
- for (const entry of manifest.entries) {
5571
- entry.publishing = resolvePublishingState(entry.frontmatter, {
5572
- strategy,
5573
- buildTime: this.publishingBuildTime
5574
- });
5575
- }
5576
- manifest.publicEntries = manifest.entries.filter(
5577
- (entry) => entry.publishing.routable
5578
- );
5579
- manifest.discoverableEntries = manifest.entries.filter(
5580
- (entry) => entry.publishing.discoverable
5581
- );
5582
- const publicSlugs = new Set(
5583
- manifest.publicEntries.map((entry) => entry.slug)
5584
- );
5585
- const publicPermalinks = new Map(
5586
- manifest.publicEntries.map((entry) => [entry.permalink, entry])
5587
- );
5588
- manifest.byRoutablePermalink = publicPermalinks;
5589
- manifest.publicRedirects = new Map(
5590
- [...manifest.redirects].filter(
5591
- ([, redirect]) => publicSlugs.has(redirect.slug)
5592
- )
5593
- );
5594
- }
5595
- synchronizeManifestHtml(manifest, preHookEntries) {
5596
- const preHookHtml = new Map(
5597
- preHookEntries.map((entry) => [entry.slug, entry.html])
5598
- );
5599
- for (const entry of manifest.entries) {
5600
- const changed = preHookHtml.get(entry.slug) !== entry.html;
5601
- const cached = this.contentCache.get(entry.slug);
5602
- if (cached) {
5603
- if (changed) cached.html = entry.html;
5604
- continue;
5605
- }
5606
- this.contentCache.set(entry.slug, {
5607
- frontmatter: entry.frontmatter,
5608
- html: entry.html
5609
- });
5610
- }
5611
- }
5612
6066
  publishStrategy() {
5613
6067
  return this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
5614
6068
  }
5615
6069
  reuseManifestEntry(slug, source, plan) {
5616
- if (!plan.previousState?.manifestEntries) return null;
5617
- if (plan.previousState.pipelineFingerprint !== this.getPipelineFingerprint()) {
5618
- return null;
5619
- }
5620
- if (plan.affectedContent.direct.has(slug)) return null;
5621
- if (plan.affectedContent.dependent.has(slug)) return null;
5622
- const contentCacheFingerprint = this.getProcessedContentCacheFingerprint(
6070
+ const reused = reuseManifestEntry({
5623
6071
  slug,
5624
- source
5625
- );
5626
- if (!contentCacheFingerprint) return null;
5627
- if (plan.previousState.entries[`${slug}.md`]?.processedContentCacheFingerprint !== contentCacheFingerprint) {
5628
- return null;
5629
- }
5630
- const entry = plan.previousManifestEntriesBySlug.get(slug);
5631
- if (!entry) return null;
6072
+ plan,
6073
+ pipelineFingerprint: this.getPipelineFingerprint(),
6074
+ getContentCacheFingerprint: () => this.getProcessedContentCacheFingerprint(slug, source)
6075
+ });
6076
+ if (!reused) return null;
5632
6077
  this.reusedContentCount += 1;
5633
- this.processedContentCacheFingerprints.set(slug, contentCacheFingerprint);
6078
+ this.processedContentCacheFingerprints.set(
6079
+ slug,
6080
+ reused.contentCacheFingerprint
6081
+ );
5634
6082
  this.observability().tracer.event("content.reuse", { slug });
5635
- return cloneManifestEntry(entry);
6083
+ return reused.entry;
5636
6084
  }
5637
6085
  getPipelineFingerprint() {
5638
6086
  if (!this.pipelineOptions.config) return void 0;
@@ -5679,6 +6127,7 @@ var ContentManager = class {
5679
6127
  return plan;
5680
6128
  }
5681
6129
  async createBuildPlan(options) {
6130
+ this.enableBuildTime();
5682
6131
  const config = this.pipelineOptions.config;
5683
6132
  if (config && hasGlobalContentDependency(config)) {
5684
6133
  this.pipelineFingerprint = this.computePipelineFingerprint(
@@ -5741,7 +6190,7 @@ var ContentManager = class {
5741
6190
  const strategy = this.publishStrategy();
5742
6191
  const posts = await this.getAllPosts();
5743
6192
  const routableSlugs = /* @__PURE__ */ new Set();
5744
- await mapConcurrent3(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
6193
+ await mapConcurrent(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
5745
6194
  const markdown = await this.getPost(post.slug);
5746
6195
  let frontmatter;
5747
6196
  try {
@@ -5785,38 +6234,6 @@ function readFrontmatter(markdown) {
5785
6234
  matter3(file);
5786
6235
  return file.data.matter ?? {};
5787
6236
  }
5788
- function cloneManifestEntry(entry) {
5789
- return {
5790
- ...entry,
5791
- publicLocation: { ...entry.publicLocation },
5792
- frontmatter: { ...entry.frontmatter },
5793
- aliases: [...entry.aliases ?? []],
5794
- tags: [...entry.tags],
5795
- links: entry.links.map((link) => ({ ...link })),
5796
- backlinks: [...entry.backlinks],
5797
- assets: entry.assets.map((asset) => ({ ...asset })),
5798
- ...entry.headTags ? { headTags: [...entry.headTags] } : {}
5799
- };
5800
- }
5801
- async function mapConcurrent3(values, concurrency, map) {
5802
- const results = new Array(values.length);
5803
- let nextIndex = 0;
5804
- async function worker() {
5805
- while (nextIndex < values.length) {
5806
- const index = nextIndex;
5807
- nextIndex += 1;
5808
- const value = values[index];
5809
- if (value !== void 0) results[index] = await map(value);
5810
- }
5811
- }
5812
- await Promise.all(
5813
- Array.from(
5814
- { length: Math.min(concurrency, values.length) },
5815
- () => worker()
5816
- )
5817
- );
5818
- return results;
5819
- }
5820
6237
 
5821
6238
  // src/content/content_source.ts
5822
6239
  async function readContentSourceEntry(source, logicalPath) {
@@ -6288,44 +6705,6 @@ function escapeHtmlAttribute(value) {
6288
6705
  function escapeScriptJson(value) {
6289
6706
  return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6290
6707
  }
6291
-
6292
- // src/utils/markdown.ts
6293
- var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
6294
- var FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
6295
- function scanMarkdownFenceLines(source) {
6296
- const lines = [];
6297
- let fence = null;
6298
- for (const value of source.split("\n")) {
6299
- if (fence === null) {
6300
- const open = FENCE_OPEN.exec(value);
6301
- if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
6302
- fence = { marker: open[1][0], length: open[1].length };
6303
- }
6304
- lines.push({ value, inFence: false });
6305
- continue;
6306
- }
6307
- const close = FENCE_CLOSE.exec(value);
6308
- if (close && close[1][0] === fence.marker && close[1].length >= fence.length) {
6309
- fence = null;
6310
- lines.push({ value, inFence: false });
6311
- continue;
6312
- }
6313
- lines.push({ value, inFence: true });
6314
- }
6315
- return lines;
6316
- }
6317
-
6318
- // src/utils/text.ts
6319
- function stripHtml(html) {
6320
- return html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ");
6321
- }
6322
- function calculateReadingTime(html) {
6323
- const text = stripHtml(html).trim();
6324
- if (!text) return 1;
6325
- const cjkCharacters = text.match(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu)?.length ?? 0;
6326
- const latinWords = text.replace(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu, " ").match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
6327
- return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
6328
- }
6329
6708
  export {
6330
6709
  ATTACHMENTS_BASE_PATH,
6331
6710
  CONTENT_BUILD_STATE_VERSION,
@@ -6362,8 +6741,11 @@ export {
6362
6741
  determineOutputChanges,
6363
6742
  escapeHtml,
6364
6743
  escapeHtmlAttribute,
6744
+ escapeRegExp,
6365
6745
  escapeScriptJson,
6746
+ extractContentLinks,
6366
6747
  extractFrontmatterAliases,
6748
+ extractMarkdownLinkUrls,
6367
6749
  fingerprintContentEntries,
6368
6750
  fingerprintExternalValue,
6369
6751
  getContentSourceEntry,