@riebeckite/core 0.0.21 → 0.0.23

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
@@ -825,17 +825,15 @@ function getFileName(filePath) {
825
825
  return normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
826
826
  }
827
827
 
828
- // src/content/content_build_state_store.ts
829
- import { randomUUID } from "node:crypto";
830
- import fs from "node:fs/promises";
831
- import path from "node:path";
832
-
833
828
  // src/content/content_build_state.ts
834
- var CONTENT_BUILD_STATE_VERSION = 8;
829
+ var CONTENT_BUILD_STATE_VERSION = 16;
835
830
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
836
831
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
837
832
 
838
833
  // src/content/content_build_state_store.ts
834
+ import { randomUUID } from "node:crypto";
835
+ import fs from "node:fs/promises";
836
+ import path from "node:path";
839
837
  async function loadContentBuildState(statePath) {
840
838
  try {
841
839
  const parsed = JSON.parse(await fs.readFile(statePath, "utf8"));
@@ -876,13 +874,26 @@ async function saveContentBuildState(statePath, state2) {
876
874
  const temporaryPath = `${statePath}.${randomUUID()}.tmp`;
877
875
  await fs.mkdir(directory, { recursive: true });
878
876
  try {
877
+ await waitForFaultInjection("content-build-state-save-start");
879
878
  await fs.writeFile(temporaryPath, JSON.stringify(state2), "utf8");
880
879
  await fs.rename(temporaryPath, statePath);
880
+ await waitForFaultInjection("content-build-state-committed");
881
881
  } catch (error) {
882
882
  await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
883
883
  throw error;
884
884
  }
885
885
  }
886
+ async function waitForFaultInjection(point) {
887
+ if (process.env.NODE_ENV !== "test" || process.env.RIEBECKITE_SSG_FAULT_INJECTION !== "1" || process.env.RIEBECKITE_SSG_FAULT_POINT !== point) {
888
+ return;
889
+ }
890
+ const signalFile = process.env.RIEBECKITE_SSG_FAULT_SIGNAL_FILE;
891
+ if (!signalFile) return;
892
+ await fs.writeFile(signalFile, `${point}
893
+ `, "utf8");
894
+ await new Promise(() => {
895
+ });
896
+ }
886
897
  function resolveContentBuildStatePath(config, contentDirectory) {
887
898
  if (config?.buildDirectory) {
888
899
  return path.join(config.buildDirectory, "build", "content-state.json");
@@ -898,7 +909,7 @@ function isContentBuildState(value) {
898
909
  return isRecord2(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
899
910
  }
900
911
  function isContentBuildStateShape(value) {
901
- if (!isRecord2(value.entries) || !isRecord2(value.contentIndex) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs)) {
912
+ if (!isRecord2(value.entries) || !isRecord2(value.contentIndex) || !isExternalDependencies(value.externalDependencies) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs) || value.manifestHookCache !== void 0 && !isManifestHookCache(value.manifestHookCache) || value.nextPublishingChangeAt !== void 0 && (typeof value.nextPublishingChangeAt !== "string" || Number.isNaN(Date.parse(value.nextPublishingChangeAt)))) {
902
913
  return false;
903
914
  }
904
915
  return Object.values(value.entries).every(
@@ -909,6 +920,11 @@ function isContentBuildStateShape(value) {
909
920
  (entry) => typeof entry === "string"
910
921
  );
911
922
  }
923
+ function isManifestHookCache(value) {
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)
926
+ );
927
+ }
912
928
  function isOutputDescriptors(value) {
913
929
  return Array.isArray(value) && value.every(
914
930
  (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)
@@ -923,7 +939,17 @@ function isOutputPath(value) {
923
939
  function isOutputDependency(value) {
924
940
  if (!isRecord2(value) || typeof value.type !== "string") return false;
925
941
  if (value.type === "global" || value.type === "unknown") return true;
926
- return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string";
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");
943
+ }
944
+ function isExternalDependencies(value) {
945
+ return Array.isArray(value) && value.every(
946
+ (dependency) => isRecord2(dependency) && typeof dependency.id === "string" && dependency.id.length > 0 && typeof dependency.fingerprint === "string" && dependency.fingerprint.length > 0 && isExternalScope(dependency.scope) && (dependency.expiresAt === void 0 || typeof dependency.expiresAt === "string" && !Number.isNaN(Date.parse(dependency.expiresAt)))
947
+ );
948
+ }
949
+ function isExternalScope(value) {
950
+ if (!isRecord2(value) || typeof value.type !== "string") return false;
951
+ if (value.type === "content" || value.type === "all") return true;
952
+ return value.type === "output" && Array.isArray(value.paths) && value.paths.every((path6) => isOutputPath(path6));
927
953
  }
928
954
  function isRecord2(value) {
929
955
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1515,9 +1541,9 @@ import remarkDirective from "remark-directive";
1515
1541
  import remarkFrontmatter from "remark-frontmatter";
1516
1542
  import remarkGfm from "remark-gfm";
1517
1543
  import remarkMath from "remark-math";
1518
- import remarkParse from "remark-parse";
1544
+ import remarkParse2 from "remark-parse";
1519
1545
  import remarkRehype from "remark-rehype";
1520
- import { unified } from "unified";
1546
+ import { unified as unified2 } from "unified";
1521
1547
  import { matter as matter2 } from "vfile-matter";
1522
1548
 
1523
1549
  // src/content/content_dependency_tracker.ts
@@ -1652,8 +1678,8 @@ function warn(logger, message, key, error) {
1652
1678
 
1653
1679
  // src/content/content_persistent_cache.ts
1654
1680
  var CONTENT_CACHE_NAMESPACE = "content";
1655
- var CONTENT_CACHE_SCHEMA_VERSION = 3;
1656
- var CONTENT_CACHE_COMPATIBILITY_VERSION = 1;
1681
+ var CONTENT_CACHE_SCHEMA_VERSION = 5;
1682
+ var CONTENT_CACHE_COMPATIBILITY_VERSION = 3;
1657
1683
  function hash2(value) {
1658
1684
  return createHash4("sha256").update(value).digest("hex");
1659
1685
  }
@@ -1813,17 +1839,35 @@ function normalizeFrontmatterForCache(frontmatter) {
1813
1839
  }
1814
1840
  return normalized;
1815
1841
  }
1816
- function computePipelineFingerprint(config) {
1842
+ function computePipelineFingerprint(config, contentScopeFingerprint) {
1817
1843
  const plugins = config.plugins;
1818
1844
  const fingerprintData = {
1819
1845
  compatibilityVersion: CONTENT_CACHE_COMPATIBILITY_VERSION,
1820
1846
  plugins: plugins.map((p) => pluginFingerprint(p)),
1821
1847
  content: {
1822
1848
  filters: config.content.filters
1823
- }
1849
+ },
1850
+ contentScopeFingerprint
1824
1851
  };
1825
1852
  return hash2(stableStringify(fingerprintData));
1826
1853
  }
1854
+ function computeContentScopeFingerprint(locations) {
1855
+ return hash2(
1856
+ stableStringify(
1857
+ [...locations.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([slug, location]) => ({
1858
+ slug,
1859
+ permalink: location.permalink,
1860
+ ...location.language ? { language: location.language } : {},
1861
+ ...location.redirects ? {
1862
+ redirects: location.redirects.map((redirect) => ({
1863
+ ...redirect
1864
+ }))
1865
+ } : {},
1866
+ ...location.metadata ? { metadata: { ...location.metadata } } : {}
1867
+ }))
1868
+ )
1869
+ );
1870
+ }
1827
1871
  var pluginFingerprints = /* @__PURE__ */ new WeakMap();
1828
1872
  function pluginFingerprint(plugin) {
1829
1873
  const cached = pluginFingerprints.get(plugin);
@@ -1859,23 +1903,25 @@ function sanitizeForFingerprint(value) {
1859
1903
  }
1860
1904
  return value;
1861
1905
  }
1862
- function isPersistentlyCacheable(_slug, config) {
1863
- const hasL10n = config.plugins.some(
1864
- (p) => p.name === "l10n" && p.enabled !== false
1865
- );
1866
- if (hasL10n) {
1867
- return { cacheable: false, reason: "l10n plugin is enabled" };
1868
- }
1906
+ function isPersistentlyCacheable(slug, config, source) {
1907
+ const context = source === void 0 ? void 0 : { slug, source };
1869
1908
  const unsafePlugin = config.plugins.find((plugin) => {
1870
1909
  if (plugin.enabled === false) return false;
1871
1910
  if (!canAffectProcessedContent(plugin)) return false;
1872
- return plugin.processedContentCache?.dependencyMode !== "none" && plugin.processedContentCache?.dependencyMode !== "tracked";
1911
+ const contract = plugin.processedContentCache;
1912
+ if (!contract) return true;
1913
+ if (!context) return contract.dependencyMode === "unsafe";
1914
+ const applicability = getApplicability(contract.appliesToContent, context);
1915
+ if (applicability === "does-not-apply") return false;
1916
+ if (applicability === "unknown") return true;
1917
+ if (contract.dependencyMode === "unsafe") return true;
1918
+ return getContractDependencyFingerprint(contract, context) === void 0;
1873
1919
  });
1874
1920
  if (unsafePlugin) {
1875
1921
  if (unsafePlugin.processedContentCache?.dependencyMode === "unsafe") {
1876
1922
  return {
1877
1923
  cacheable: false,
1878
- reason: `plugin ${unsafePlugin.name} explicitly marks processed content cache as unsafe`
1924
+ reason: `plugin ${unsafePlugin.name} explicitly marks processed content cache as unsafe and cannot prove safety for this content`
1879
1925
  };
1880
1926
  }
1881
1927
  return {
@@ -1885,6 +1931,68 @@ function isPersistentlyCacheable(_slug, config) {
1885
1931
  }
1886
1932
  return { cacheable: true };
1887
1933
  }
1934
+ function computeContentCacheFingerprint(slug, source, config) {
1935
+ const context = { slug, source };
1936
+ const plugins = config.plugins.filter(
1937
+ (plugin) => plugin.enabled !== false && canAffectProcessedContent(plugin)
1938
+ ).map((plugin) => {
1939
+ const contract = plugin.processedContentCache;
1940
+ if (!contract) return { name: plugin.name, applicability: "unknown" };
1941
+ const applicability = getApplicability(
1942
+ contract.appliesToContent,
1943
+ context
1944
+ );
1945
+ return {
1946
+ name: plugin.name,
1947
+ applicability,
1948
+ dependencyFingerprint: applicability === "applies" ? getContractDependencyFingerprint(contract, context) : void 0
1949
+ };
1950
+ });
1951
+ return hash2(stableStringify(plugins));
1952
+ }
1953
+ function getApplicability(appliesToContent, context) {
1954
+ if (!context) return "unknown";
1955
+ if (!appliesToContent) return "applies";
1956
+ try {
1957
+ const applicability = appliesToContent(context);
1958
+ return applicability === "applies" || applicability === "does-not-apply" || applicability === "unknown" ? applicability : "unknown";
1959
+ } catch {
1960
+ return "unknown";
1961
+ }
1962
+ }
1963
+ function getContractDependencyFingerprint(contract, context) {
1964
+ if (!context) return void 0;
1965
+ try {
1966
+ const content = contract.contentDependencyFingerprint?.(context);
1967
+ if (contract.contentDependencyFingerprint && content === void 0)
1968
+ return void 0;
1969
+ const environment = contract.environmentFingerprint?.();
1970
+ if (contract.environmentFingerprint && environment === void 0)
1971
+ return void 0;
1972
+ return hash2(
1973
+ stableStringify({
1974
+ content: content ?? null,
1975
+ environment: environment ?? null
1976
+ })
1977
+ );
1978
+ } catch {
1979
+ return void 0;
1980
+ }
1981
+ }
1982
+ function hasGlobalContentDependency(config) {
1983
+ return config.plugins.some(
1984
+ (plugin) => plugin.enabled !== false && canAffectProcessedContent(plugin) && plugin.processedContentCache?.dependencyMode === "global"
1985
+ );
1986
+ }
1987
+ function requiresFullContentRegeneration(config) {
1988
+ return config.plugins.some((plugin) => {
1989
+ if (plugin.enabled === false || !canAffectProcessedContent(plugin)) {
1990
+ return false;
1991
+ }
1992
+ const contract = plugin.processedContentCache;
1993
+ return !contract || contract.dependencyMode === "unsafe" && !contract.appliesToContent;
1994
+ });
1995
+ }
1888
1996
  function canAffectProcessedContent(plugin) {
1889
1997
  return Boolean(
1890
1998
  plugin.remarkPlugins?.length || plugin.rehypePlugins?.length || plugin.extendMarkdownPipeline || plugin.extendHtmlPipeline || plugin.renderers?.length || plugin.onContentLoaded || plugin.onPostParsed || plugin.onPostProcessed
@@ -1896,6 +2004,117 @@ function extractFrontmatter(markdown) {
1896
2004
  return vfile.data.matter ?? {};
1897
2005
  }
1898
2006
 
2007
+ // src/content/markdown_image.ts
2008
+ import remarkParse from "remark-parse";
2009
+ import { unified } from "unified";
2010
+ function extractMarkdownImages(markdown, sourceSlug) {
2011
+ const tree = unified().use(remarkParse).parse(markdown);
2012
+ const definitions = /* @__PURE__ */ new Map();
2013
+ const references = [];
2014
+ const urls = [];
2015
+ visitMarkdownNodes(tree, (node) => {
2016
+ if (node.type === "definition" && typeof node.url === "string") {
2017
+ const identifier = normalizeIdentifier(node.identifier);
2018
+ if (identifier) definitions.set(identifier, node.url);
2019
+ }
2020
+ if (node.type === "image" && typeof node.url === "string") {
2021
+ urls.push(node.url);
2022
+ }
2023
+ if (node.type === "imageReference") {
2024
+ const identifier = normalizeIdentifier(node.identifier);
2025
+ if (identifier) references.push(identifier);
2026
+ }
2027
+ });
2028
+ for (const reference of references) {
2029
+ const url = definitions.get(reference);
2030
+ if (url) urls.push(url);
2031
+ }
2032
+ return uniqueImages(
2033
+ urls.map((url) => resolveMarkdownImage(url, sourceSlug)).filter((image) => image !== null)
2034
+ );
2035
+ }
2036
+ function normalizeMarkdownImages(sourceSlug) {
2037
+ return () => (tree) => {
2038
+ const definitions = /* @__PURE__ */ new Map();
2039
+ const references = /* @__PURE__ */ new Set();
2040
+ visitMarkdownNodes(tree, (node) => {
2041
+ if (node.type === "definition" && typeof node.url === "string") {
2042
+ const identifier = normalizeIdentifier(node.identifier);
2043
+ if (identifier) definitions.set(identifier, node);
2044
+ }
2045
+ if (node.type === "image" && typeof node.url === "string") {
2046
+ const image = resolveMarkdownImage(node.url, sourceSlug);
2047
+ if (image) node.url = image.url;
2048
+ }
2049
+ if (node.type === "imageReference") {
2050
+ const identifier = normalizeIdentifier(node.identifier);
2051
+ if (identifier) references.add(identifier);
2052
+ }
2053
+ });
2054
+ for (const identifier of references) {
2055
+ const definition = definitions.get(identifier);
2056
+ if (!definition || typeof definition.url !== "string") continue;
2057
+ const image = resolveMarkdownImage(definition.url, sourceSlug);
2058
+ if (image) definition.url = image.url;
2059
+ }
2060
+ };
2061
+ }
2062
+ function resolveMarkdownImage(input, sourceSlug) {
2063
+ if (input.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(input)) {
2064
+ return null;
2065
+ }
2066
+ const [pathAndQuery, hash4 = ""] = splitOnce(input, "#");
2067
+ const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
2068
+ const decodedPath = decodeUriPath(rawPath);
2069
+ if (!decodedPath || !isImagePath(decodedPath)) return null;
2070
+ const path6 = resolveContentAssetPath(sourceSlug, decodedPath);
2071
+ if (!path6) return null;
2072
+ const encodedPath = path6.split("/").map(encodeURIComponent).join("/");
2073
+ const suffix = `${query ? `?${query}` : ""}${hash4 ? `#${hash4}` : ""}`;
2074
+ return { path: path6, url: `/${encodedPath}${suffix}` };
2075
+ }
2076
+ function resolveContentAssetPath(sourceSlug, targetPath) {
2077
+ const sourceDirectory = sourceSlug.includes("/") ? sourceSlug.slice(0, sourceSlug.lastIndexOf("/")) : "";
2078
+ const parts = [
2079
+ ...targetPath.startsWith("/") ? [] : sourceDirectory.split("/"),
2080
+ ...normalizeContentPath(targetPath).replace(/^\/+/, "").split("/")
2081
+ ];
2082
+ const normalized = [];
2083
+ for (const part of parts) {
2084
+ if (!part || part === ".") continue;
2085
+ if (part === "..") {
2086
+ if (normalized.length === 0) return null;
2087
+ normalized.pop();
2088
+ continue;
2089
+ }
2090
+ normalized.push(part);
2091
+ }
2092
+ return normalized.length > 0 ? normalized.join("/").normalize("NFC") : null;
2093
+ }
2094
+ function visitMarkdownNodes(node, visitor) {
2095
+ const markdownNode = node;
2096
+ visitor(markdownNode);
2097
+ for (const child of markdownNode.children ?? [])
2098
+ visitMarkdownNodes(child, visitor);
2099
+ }
2100
+ function normalizeIdentifier(value) {
2101
+ return typeof value === "string" ? value.toLowerCase() : null;
2102
+ }
2103
+ function decodeUriPath(path6) {
2104
+ try {
2105
+ return decodeURI(path6);
2106
+ } catch {
2107
+ return null;
2108
+ }
2109
+ }
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
+ function uniqueImages(images) {
2115
+ return [...new Map(images.map((image) => [image.path, image])).values()];
2116
+ }
2117
+
1899
2118
  // src/plugin/plugin_cache.ts
1900
2119
  import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
1901
2120
  import fs3 from "node:fs/promises";
@@ -2086,7 +2305,8 @@ var Pipeline = class {
2086
2305
  tracer: this.options.observability?.tracer
2087
2306
  });
2088
2307
  this.pipelineFingerprint = computePipelineFingerprint(
2089
- this.options.config
2308
+ this.options.config,
2309
+ this.options.contentScopeFingerprint
2090
2310
  );
2091
2311
  }
2092
2312
  }
@@ -2113,9 +2333,11 @@ var Pipeline = class {
2113
2333
  const effectiveContentSource = dependencyTracker ? dependencyTracker.contentSource : this.options.contentSource;
2114
2334
  const sourceSlug = context.sourceSlug;
2115
2335
  if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && this.options.contentSource && this.options.config) {
2336
+ const persistentCache = this.persistentCache;
2116
2337
  const cacheable = isPersistentlyCacheable(
2117
2338
  sourceSlug,
2118
- this.options.config
2339
+ this.options.config,
2340
+ markDownContent
2119
2341
  );
2120
2342
  if (cacheable.cacheable) {
2121
2343
  const frontmatter = extractFrontmatter(markDownContent);
@@ -2123,12 +2345,21 @@ var Pipeline = class {
2123
2345
  slug: sourceSlug,
2124
2346
  source: markDownContent,
2125
2347
  frontmatter,
2126
- pipelineFingerprint: this.pipelineFingerprint
2348
+ pipelineFingerprint: this.contentPipelineFingerprint(
2349
+ sourceSlug,
2350
+ markDownContent
2351
+ )
2127
2352
  });
2128
- const lookup = await this.persistentCache.lookup(cacheKey);
2353
+ const lookup = await (this.options.observability ?? noopObservability).tracer.span(
2354
+ "persistentContentCache.lookup",
2355
+ {},
2356
+ () => persistentCache.lookup(cacheKey)
2357
+ );
2129
2358
  if (lookup.status === "hit") {
2130
- const invalidDependency = await this.findInvalidDependency(
2131
- lookup.entry.dependencies
2359
+ const invalidDependency = await (this.options.observability ?? noopObservability).tracer.span(
2360
+ "persistentContentCache.validation",
2361
+ { dependencyCount: lookup.entry.dependencies.length },
2362
+ () => this.findInvalidDependency(lookup.entry.dependencies)
2132
2363
  );
2133
2364
  if (!invalidDependency) {
2134
2365
  this.reportContentDependencies(
@@ -2178,9 +2409,9 @@ var Pipeline = class {
2178
2409
  this.options.onPersistentContentCacheResult?.("bypass");
2179
2410
  }
2180
2411
  }
2181
- const processor = unified();
2412
+ const processor = unified2();
2182
2413
  const plugins = this.resolvePlugins();
2183
- this.use(processor, remarkParse);
2414
+ this.use(processor, remarkParse2);
2184
2415
  this.use(processor, remarkDirective);
2185
2416
  this.use(processor, remarkFrontmatter, ["yaml", "toml"]);
2186
2417
  this.use(processor, function loadFrontmatter() {
@@ -2220,6 +2451,9 @@ var Pipeline = class {
2220
2451
  ...markdownPipelineContext,
2221
2452
  recordLinkResolution: dependencyTracker ? (id, value) => dependencyTracker.recordLinkResolution(id, value) : void 0
2222
2453
  });
2454
+ if (context.sourceSlug) {
2455
+ this.use(processor, normalizeMarkdownImages(context.sourceSlug));
2456
+ }
2223
2457
  this.use(processor, remarkRehype, { allowDangerousHtml: true });
2224
2458
  this.use(processor, rehypeRaw);
2225
2459
  for (const plugin of plugins) {
@@ -2236,7 +2470,11 @@ var Pipeline = class {
2236
2470
  this.use(processor, rehypeKatex, { output: "mathml", strict: false });
2237
2471
  this.use(processor, rehypeFormat);
2238
2472
  this.use(processor, rehypeStringify, { allowDangerousHtml: true });
2239
- const file = await processor.process(markDownContent.trim());
2473
+ const file = await (this.options.observability ?? noopObservability).tracer.span(
2474
+ "content.markdown",
2475
+ { embedded: embedDepth > 0 },
2476
+ async () => await processor.process(markDownContent.trim())
2477
+ );
2240
2478
  const result = {
2241
2479
  frontmatter: file.data.matter || {},
2242
2480
  html: String(file.value)
@@ -2253,7 +2491,8 @@ var Pipeline = class {
2253
2491
  if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && dependencyTracker && this.options.config) {
2254
2492
  const cacheable = isPersistentlyCacheable(
2255
2493
  sourceSlug,
2256
- this.options.config
2494
+ this.options.config,
2495
+ markDownContent
2257
2496
  );
2258
2497
  if (cacheable.cacheable) {
2259
2498
  const frontmatter = extractFrontmatter(markDownContent);
@@ -2261,7 +2500,10 @@ var Pipeline = class {
2261
2500
  slug: sourceSlug,
2262
2501
  source: markDownContent,
2263
2502
  frontmatter,
2264
- pipelineFingerprint: this.pipelineFingerprint
2503
+ pipelineFingerprint: this.contentPipelineFingerprint(
2504
+ sourceSlug,
2505
+ markDownContent
2506
+ )
2265
2507
  });
2266
2508
  const dependencies = dependencyTracker.dependencies();
2267
2509
  const entry = {
@@ -2278,6 +2520,11 @@ var Pipeline = class {
2278
2520
  reportContentDependencies(slug, dependencies) {
2279
2521
  this.options.onContentDependencies?.(slug, dependencies);
2280
2522
  }
2523
+ contentPipelineFingerprint(slug, source) {
2524
+ const config = this.options.config;
2525
+ if (!config || !this.pipelineFingerprint) return "";
2526
+ return `${this.pipelineFingerprint}:${computeContentCacheFingerprint(slug, source, config)}`;
2527
+ }
2281
2528
  async findInvalidDependency(dependencies) {
2282
2529
  const contentSource = this.options.contentSource;
2283
2530
  if (!contentSource) return null;
@@ -2464,9 +2711,9 @@ function visitMarkdownLinkNodes(node, visitor) {
2464
2711
  function normalizeMarkdownLinkUrl(url, context) {
2465
2712
  if (!context.sourceSlug) return null;
2466
2713
  if (!isMarkdownContentLink(url)) return null;
2467
- const [pathAndQuery, hash4 = ""] = splitOnce(url, "#");
2468
- const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
2469
- const decodedPath = decodeUriPath(rawPath);
2714
+ const [pathAndQuery, hash4 = ""] = splitOnce2(url, "#");
2715
+ const [rawPath, query = ""] = splitOnce2(pathAndQuery, "?");
2716
+ const decodedPath = decodeUriPath2(rawPath);
2470
2717
  if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
2471
2718
  const resolvedSlug = resolveRelativeMarkdownSlug(
2472
2719
  context.sourceSlug,
@@ -2527,14 +2774,14 @@ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
2527
2774
  }
2528
2775
  return normalizedParts.join("/").replace(/\.md$/i, "");
2529
2776
  }
2530
- function decodeUriPath(path6) {
2777
+ function decodeUriPath2(path6) {
2531
2778
  try {
2532
2779
  return decodeURI(path6);
2533
2780
  } catch {
2534
2781
  return null;
2535
2782
  }
2536
2783
  }
2537
- function splitOnce(value, separator) {
2784
+ function splitOnce2(value, separator) {
2538
2785
  const index = value.indexOf(separator);
2539
2786
  if (index < 0) return [value];
2540
2787
  return [value.slice(0, index), value.slice(index + separator.length)];
@@ -2563,18 +2810,141 @@ function escapeRegExp(value) {
2563
2810
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2564
2811
  }
2565
2812
 
2813
+ // src/content/manifest_hook_cache.ts
2814
+ import { createHash as createHash6 } from "node:crypto";
2815
+ function fingerprintManifestHookInput(manifest) {
2816
+ return fingerprintManifest(manifestFingerprintValue(manifest));
2817
+ }
2818
+ function createManifestHookCacheEntry(pluginName, cacheable, pipelineFingerprint, inputFingerprint, manifest) {
2819
+ const snapshot = {
2820
+ entries: JSON.parse(
2821
+ JSON.stringify(manifest.entries)
2822
+ )
2823
+ };
2824
+ return {
2825
+ pluginName,
2826
+ cacheable,
2827
+ pipelineFingerprint,
2828
+ inputFingerprint,
2829
+ outputFingerprint: fingerprintManifestHookSnapshot(snapshot, manifest),
2830
+ snapshot
2831
+ };
2832
+ }
2833
+ function fingerprintManifestHookSideEffects(manifest) {
2834
+ const value = {
2835
+ ...manifest,
2836
+ entries: []
2837
+ };
2838
+ return fingerprintManifest(manifestFingerprintValue(value));
2839
+ }
2840
+ function fingerprintManifestHookSnapshot(snapshot, manifest) {
2841
+ return fingerprintManifest(
2842
+ manifestFingerprintValue({
2843
+ ...manifest,
2844
+ entries: [...snapshot.entries]
2845
+ })
2846
+ );
2847
+ }
2848
+ function applyManifestHookSnapshot(manifest, snapshot) {
2849
+ 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])
2863
+ );
2864
+ manifest.byRoutablePermalink = new Map(
2865
+ manifest.publicEntries.map((entry) => [entry.permalink, entry])
2866
+ );
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
+ }
2896
+ }
2897
+ for (const entry of entries)
2898
+ entry.backlinks = manifest.incomingLinks.get(entry.slug) ?? [];
2899
+ return true;
2900
+ } catch {
2901
+ return false;
2902
+ }
2903
+ }
2904
+ function manifestFingerprintValue(manifest) {
2905
+ return {
2906
+ entries: manifest.entries,
2907
+ publicEntries: manifest.publicEntries.map((entry) => entry.slug),
2908
+ discoverableEntries: manifest.discoverableEntries.map(
2909
+ (entry) => entry.slug
2910
+ ),
2911
+ redirects: [...manifest.redirects.entries()].sort(
2912
+ ([left], [right]) => left.localeCompare(right)
2913
+ ),
2914
+ publicRedirects: [...manifest.publicRedirects.entries()].sort(
2915
+ ([left], [right]) => left.localeCompare(right)
2916
+ ),
2917
+ assets: manifest.assets,
2918
+ clientEntries: manifest.clientEntries,
2919
+ diagnostics: manifest.diagnostics,
2920
+ generatedOutputs: manifest.generatedOutputs,
2921
+ folderLocations: [...manifest.folderLocations.entries()].sort(
2922
+ ([left], [right]) => left.localeCompare(right)
2923
+ ),
2924
+ pageRoutes: manifest.pageRoutes,
2925
+ pagePaths: manifest.pagePaths
2926
+ };
2927
+ }
2928
+ function fingerprintManifest(value) {
2929
+ const normalized = JSON.parse(JSON.stringify(value));
2930
+ return createHash6("sha256").update(stableStringify(normalized)).digest("hex");
2931
+ }
2932
+ function isManifestEntry(value) {
2933
+ return typeof value === "object" && value !== null && typeof value.slug === "string" && typeof value.html === "string";
2934
+ }
2935
+
2566
2936
  // src/types/plugin_asset.ts
2567
- function createStyleAsset(pluginName) {
2937
+ function createStyleAsset(pluginName, moduleSpecifier) {
2568
2938
  return {
2569
2939
  pluginName,
2570
2940
  kind: "style",
2571
- moduleSpecifier: `@riebeckite/plugin-${pluginName}/style.css`
2941
+ moduleSpecifier
2572
2942
  };
2573
2943
  }
2574
- function createClientEntry(pluginName, exportName, publicConfig) {
2944
+ function createClientEntry(pluginName, moduleSpecifier, exportName, publicConfig) {
2575
2945
  return {
2576
2946
  pluginName,
2577
- moduleSpecifier: `@riebeckite/plugin-${pluginName}/client`,
2947
+ moduleSpecifier,
2578
2948
  ...exportName === void 0 ? {} : { exportName },
2579
2949
  ...publicConfig === void 0 ? {} : { publicConfig }
2580
2950
  };
@@ -2646,6 +3016,9 @@ var GeneratedOutputRegistry = class {
2646
3016
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
2647
3017
  );
2648
3018
  }
3019
+ count() {
3020
+ return this.outputs.length;
3021
+ }
2649
3022
  };
2650
3023
 
2651
3024
  // src/plugin/plugin_lifecycle.ts
@@ -2727,12 +3100,46 @@ var PluginRuntime = class {
2727
3100
  isBuildTime = false;
2728
3101
  resolvedPlugins = null;
2729
3102
  pageDeclarations = /* @__PURE__ */ new WeakMap();
3103
+ externalDependencies = /* @__PURE__ */ new Map();
3104
+ externalDependencyFailures = [];
3105
+ externalDependenciesResolved = false;
3106
+ manifestHooksExecuted = 0;
3107
+ manifestHookDurationMs = 0;
3108
+ manifestHooksReused = 0;
3109
+ manifestHookCache = [];
3110
+ manifestHookMetricsByPlugin = /* @__PURE__ */ new Map();
2730
3111
  getDiagnostics() {
2731
3112
  return this.diagnostics;
2732
3113
  }
2733
3114
  getBuildStage() {
2734
3115
  return this.buildStage;
2735
3116
  }
3117
+ 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
+ };
3126
+ }
3127
+ 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;
3139
+ }
3140
+ getManifestHookCache() {
3141
+ return this.manifestHookCache;
3142
+ }
2736
3143
  /**
2737
3144
  * Starts the plugin build lifecycle exactly once, regardless of which public
2738
3145
  * entry point (content processing or location resolution) reaches it first.
@@ -2751,6 +3158,43 @@ var PluginRuntime = class {
2751
3158
  markdown
2752
3159
  });
2753
3160
  }
3161
+ async resolveExternalDependencies(contentIndex) {
3162
+ if (this.externalDependenciesResolved) {
3163
+ return {
3164
+ snapshots: [...this.externalDependencies.values()],
3165
+ failures: this.externalDependencyFailures
3166
+ };
3167
+ }
3168
+ await this.startBuild(contentIndex);
3169
+ const snapshots = /* @__PURE__ */ new Map();
3170
+ const failures = [];
3171
+ for (const plugin of this.plugins()) {
3172
+ const resolve = plugin.resolveExternalDependencies;
3173
+ if (!resolve) continue;
3174
+ const results = await resolve({
3175
+ config: this.pipelineOptions.config,
3176
+ contentIndex,
3177
+ cache: this.cacheFor(plugin)
3178
+ });
3179
+ for (const result of results) {
3180
+ if (result.kind === "unavailable") {
3181
+ failures.push(result.failure);
3182
+ continue;
3183
+ }
3184
+ const snapshot = result.snapshot;
3185
+ if (snapshots.has(snapshot.id)) {
3186
+ throw new Error(
3187
+ `External dependency ID "${snapshot.id}" is declared more than once.`
3188
+ );
3189
+ }
3190
+ snapshots.set(snapshot.id, snapshot);
3191
+ }
3192
+ }
3193
+ this.externalDependencies = snapshots;
3194
+ this.externalDependencyFailures = failures;
3195
+ this.externalDependenciesResolved = true;
3196
+ return { snapshots: [...snapshots.values()], failures };
3197
+ }
2754
3198
  async resolveContentLocations(entries, contentIndex) {
2755
3199
  const context = { ...this.createContext(contentIndex), entries };
2756
3200
  const locations = [];
@@ -2797,15 +3241,86 @@ var PluginRuntime = class {
2797
3241
  }
2798
3242
  );
2799
3243
  }
2800
- async runManifestCreated(manifest, contentIndex) {
2801
- await this.runHook(
2802
- "onManifestCreated",
2803
- (plugin) => plugin.onManifestCreated,
2804
- {
2805
- ...this.createContext(contentIndex),
2806
- manifest
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;
2807
3270
  }
2808
- );
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
+ )
3304
+ );
3305
+ }
3306
+ }
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
3323
+ });
2809
3324
  }
2810
3325
  async runBuildEnd(manifest, contentIndex) {
2811
3326
  const context = {
@@ -2817,6 +3332,30 @@ var PluginRuntime = class {
2817
3332
  (plugin) => this.createPluginContext(plugin, context)
2818
3333
  );
2819
3334
  }
3335
+ async runGeneratedOutputPlans(manifest) {
3336
+ for (const plugin of this.plugins()) {
3337
+ const hook = plugin.planGeneratedOutputs;
3338
+ if (!hook) continue;
3339
+ const outputs = await hook(
3340
+ freezeGeneratedOutputPlanContext({
3341
+ manifest: {
3342
+ discoverableEntries: manifest.discoverableEntries,
3343
+ routableEntries: manifest.publicEntries
3344
+ },
3345
+ site: this.pipelineOptions.config?.site
3346
+ })
3347
+ );
3348
+ const sink = this.generatedOutputs.sinkFor(plugin.name);
3349
+ for (const output of outputs) {
3350
+ if (output.dependencies.length === 0) {
3351
+ throw new Error(
3352
+ `Generated output plan from "${plugin.name}" must declare at least one dependency.`
3353
+ );
3354
+ }
3355
+ sink.emit(output);
3356
+ }
3357
+ }
3358
+ }
2820
3359
  enableBuildTime() {
2821
3360
  this.isBuildTime = true;
2822
3361
  }
@@ -2857,6 +3396,13 @@ var PluginRuntime = class {
2857
3396
  collectGeneratedOutputs() {
2858
3397
  return this.generatedOutputs.all();
2859
3398
  }
3399
+ emitCoreAsset(path6, content) {
3400
+ this.generatedOutputs.sinkFor("core").emitAsset({
3401
+ path: path6,
3402
+ content,
3403
+ dependencies: [{ type: "file", path: path6 }]
3404
+ });
3405
+ }
2860
3406
  async resolvePage(pathname, manifest, contentIndex) {
2861
3407
  const matches = [];
2862
3408
  const requestedPathname = normalizePagePath(pathname);
@@ -2967,7 +3513,9 @@ var PluginRuntime = class {
2967
3513
  return declarations;
2968
3514
  }
2969
3515
  getContentOutputDependencies() {
2970
- return this.plugins().flatMap((plugin) => plugin.outputDependencies ?? []);
3516
+ return this.plugins().flatMap(
3517
+ (plugin) => plugin.contentOutputDependencies ?? []
3518
+ );
2971
3519
  }
2972
3520
  async collectDiagnostics(contentIndex) {
2973
3521
  const tracer = this.observability().tracer;
@@ -3030,7 +3578,8 @@ var PluginRuntime = class {
3030
3578
  config: this.pipelineOptions.config,
3031
3579
  contentIndex,
3032
3580
  contentSource: this.pipelineOptions.contentSource,
3033
- diagnostics: this.diagnostics
3581
+ diagnostics: this.diagnostics,
3582
+ externalDependencies: this.externalDependencies
3034
3583
  };
3035
3584
  }
3036
3585
  createPluginContext(plugin, context) {
@@ -3081,6 +3630,15 @@ var PluginRuntime = class {
3081
3630
  return this.resolvedPlugins;
3082
3631
  }
3083
3632
  };
3633
+ function freezeGeneratedOutputPlanContext(context) {
3634
+ return deepFreeze(structuredClone(context));
3635
+ }
3636
+ function deepFreeze(value) {
3637
+ if (value === null || typeof value !== "object") return value;
3638
+ Object.freeze(value);
3639
+ for (const property of Object.values(value)) deepFreeze(property);
3640
+ return value;
3641
+ }
3084
3642
  function collectPageTypeOwners(plugins) {
3085
3643
  const pageTypes = /* @__PURE__ */ new Map();
3086
3644
  for (const plugin of plugins) {
@@ -3143,6 +3701,16 @@ function normalizeDirectoryPagePath(pathname) {
3143
3701
  const path6 = normalizePagePath(pathname);
3144
3702
  return path6 === "/" ? path6 : `${path6}/`;
3145
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
+ }
3146
3714
  function normalizePagePath(pathname) {
3147
3715
  const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3148
3716
  return path6 === "/" ? path6 : path6.replace(/\/$/, "");
@@ -3450,34 +4018,46 @@ var ContentBuildCoordinator = class {
3450
4018
  this.dependencies = dependencies;
3451
4019
  }
3452
4020
  dependencies;
3453
- preparation = null;
3454
- getPreparation(incremental, pipelineFingerprint, fullContentRegenerationRequired = false) {
3455
- this.preparation ??= this.prepare(
4021
+ plan = null;
4022
+ getPlan(incremental, pipelineFingerprint, fullContentRegenerationRequired = false, externalDependencies = [], externalDependencyFailures = [], publishingBuildTime = /* @__PURE__ */ new Date()) {
4023
+ if (this.plan) return this.plan;
4024
+ const plan = this.prepare(
3456
4025
  incremental,
3457
4026
  pipelineFingerprint,
3458
- fullContentRegenerationRequired
4027
+ fullContentRegenerationRequired,
4028
+ externalDependencies,
4029
+ externalDependencyFailures,
4030
+ publishingBuildTime
3459
4031
  );
3460
- return this.preparation;
4032
+ this.plan = plan;
4033
+ void plan.catch(() => {
4034
+ if (this.plan === plan) this.plan = null;
4035
+ });
4036
+ return plan;
3461
4037
  }
3462
- async commit(preparation, manifest, pipelineFingerprint, outputs = [], manifestEntries = manifest.entries, trackedDependencies = /* @__PURE__ */ new Map()) {
4038
+ async commit(plan, manifest, pipelineFingerprint, outputs = [], manifestEntries = manifest.entries, trackedDependencies = /* @__PURE__ */ new Map(), externalDependencies = [], processedContentCacheFingerprints = /* @__PURE__ */ new Map(), manifestHookCache = [], publishingBuildTime) {
3463
4039
  const entriesBySlug = new Map(
3464
4040
  manifest.entries.map((entry) => [entry.slug, entry])
3465
4041
  );
4042
+ const nextChangeAt = nextPublishingChangeAt(manifest, publishingBuildTime);
3466
4043
  const state2 = {
3467
4044
  version: CONTENT_BUILD_STATE_VERSION,
3468
4045
  entries: Object.fromEntries(
3469
- preparation.currentEntries.map(({ entry, fingerprint }) => {
4046
+ plan.currentEntries.map(({ entry, fingerprint }) => {
3470
4047
  const manifestEntry = entriesBySlug.get(toSlug(entry.path));
3471
4048
  return [
3472
4049
  entry.path,
3473
4050
  {
3474
4051
  fingerprint,
3475
- aliases: preparation.currentContentAliases.get(entry.path) ?? [],
4052
+ aliases: plan.currentContentAliases.get(entry.path) ?? [],
3476
4053
  dependencies: collectDependencies2(
3477
4054
  manifestEntry,
3478
- trackedDependencies.get(toSlug(entry.path)) ?? preparation.previousState?.entries[entry.path]?.dependencies ?? []
4055
+ trackedDependencies.get(toSlug(entry.path)) ?? plan.previousState?.entries[entry.path]?.dependencies ?? []
3479
4056
  ),
3480
- linkTargets: collectLinkTargets(manifestEntry)
4057
+ linkTargets: collectLinkTargets(manifestEntry),
4058
+ ...processedContentCacheFingerprints.get(toSlug(entry.path)) ? {
4059
+ processedContentCacheFingerprint: processedContentCacheFingerprints.get(toSlug(entry.path))
4060
+ } : {}
3481
4061
  }
3482
4062
  ];
3483
4063
  })
@@ -3489,7 +4069,19 @@ var ContentBuildCoordinator = class {
3489
4069
  ),
3490
4070
  pipelineFingerprint,
3491
4071
  manifestEntries,
3492
- outputs
4072
+ outputs,
4073
+ externalDependencies: externalDependencies.map(
4074
+ ({ id, fingerprint, scope, expiresAt }) => ({
4075
+ id,
4076
+ fingerprint,
4077
+ scope,
4078
+ ...expiresAt === void 0 ? {} : { expiresAt }
4079
+ })
4080
+ ),
4081
+ manifestHookCache,
4082
+ ...nextChangeAt ? {
4083
+ nextPublishingChangeAt: nextChangeAt
4084
+ } : {}
3493
4085
  };
3494
4086
  try {
3495
4087
  await saveContentBuildState(this.dependencies.buildStatePath, state2);
@@ -3503,7 +4095,7 @@ var ContentBuildCoordinator = class {
3503
4095
  );
3504
4096
  }
3505
4097
  }
3506
- async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired) {
4098
+ async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired, externalDependencies, externalDependencyFailures, publishingBuildTime) {
3507
4099
  const entries = await this.dependencies.observability.tracer.span(
3508
4100
  "content.discovery",
3509
4101
  {},
@@ -3517,12 +4109,20 @@ var ContentBuildCoordinator = class {
3517
4109
  (entry) => this.dependencies.readEntry(entry)
3518
4110
  )
3519
4111
  );
3520
- const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
3521
- const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
3522
- const currentContentAliases = await this.getContentAliases(
3523
- currentEntries,
3524
- previousState,
3525
- changeSet
4112
+ const previousState = await this.dependencies.observability.tracer.span(
4113
+ "content.build-state.load",
4114
+ { incremental },
4115
+ async () => incremental ? await loadContentBuildState(this.dependencies.buildStatePath) : void 0
4116
+ );
4117
+ const changeSet = await this.dependencies.observability.tracer.span(
4118
+ "content.change-set",
4119
+ {},
4120
+ () => previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries)
4121
+ );
4122
+ const currentContentAliases = await this.dependencies.observability.tracer.span(
4123
+ "content.aliases",
4124
+ {},
4125
+ () => this.getContentAliases(currentEntries, previousState, changeSet)
3526
4126
  );
3527
4127
  const {
3528
4128
  index: currentContentIndex,
@@ -3535,26 +4135,54 @@ var ContentBuildCoordinator = class {
3535
4135
  currentContentAliases
3536
4136
  )
3537
4137
  );
3538
- const affected = !fullContentRegenerationRequired && previousState?.pipelineFingerprint === pipelineFingerprint ? determineAffectedContent(
3539
- changeSet,
4138
+ const publishingStateExpired = isPublishingStateExpired(
3540
4139
  previousState,
3541
- currentEntries.map(({ entry }) => entry.path),
3542
- currentContentIndex
3543
- ) : determineAffectedContent(
3544
- allContentChanged(currentEntries),
3545
- void 0,
3546
- currentEntries.map(({ entry }) => entry.path),
3547
- currentContentIndex
4140
+ publishingBuildTime
4141
+ );
4142
+ const affected = await this.dependencies.observability.tracer.span(
4143
+ "content.affected",
4144
+ { fullContentRegenerationRequired },
4145
+ () => !fullContentRegenerationRequired && !publishingStateExpired && previousState?.pipelineFingerprint === pipelineFingerprint ? determineAffectedContent(
4146
+ changeSet,
4147
+ previousState,
4148
+ currentEntries.map(({ entry }) => entry.path),
4149
+ currentContentIndex
4150
+ ) : determineAffectedContent(
4151
+ allContentChanged(currentEntries),
4152
+ void 0,
4153
+ currentEntries.map(({ entry }) => entry.path),
4154
+ currentContentIndex
4155
+ )
3548
4156
  );
4157
+ const fullRegenerationReasons = [];
4158
+ if (!incremental) fullRegenerationReasons.push("incremental-disabled");
4159
+ else if (!previousState)
4160
+ fullRegenerationReasons.push("cache-missing-or-invalid");
4161
+ if (previousState && previousState.pipelineFingerprint !== pipelineFingerprint)
4162
+ fullRegenerationReasons.push("pipeline-fingerprint-changed");
4163
+ if (fullContentRegenerationRequired)
4164
+ fullRegenerationReasons.push("content-plugin-uncacheable");
4165
+ if (publishingStateExpired)
4166
+ fullRegenerationReasons.push("publishing-state-expired");
4167
+ if (externalDependencyFailures.length > 0)
4168
+ fullRegenerationReasons.push("external-dependency-unavailable");
3549
4169
  this.dependencies.observability.tracer.event("build.incremental", {
3550
- incremental: incremental !== false,
4170
+ incremental,
4171
+ cacheValid: previousState !== void 0,
4172
+ fullRegenerationReasons: fullRegenerationReasons.join(","),
4173
+ externalDependencyFailures: externalDependencyFailures.length,
3551
4174
  added: changeSet.added.length,
3552
4175
  changed: changeSet.changed.length,
3553
4176
  removed: changeSet.removed.length,
3554
4177
  unchanged: changeSet.unchanged.length,
3555
4178
  affected: affected.direct.size + affected.dependent.size
3556
4179
  });
4180
+ const reusableOutputs = incremental && previousState?.pipelineFingerprint === pipelineFingerprint && externalDependencyFailures.length === 0 && !publishingStateExpired && externalDependenciesEqual(
4181
+ previousState?.externalDependencies ?? [],
4182
+ externalDependencies
4183
+ ) && changeSet.added.length === 0 && changeSet.changed.length === 0 && changeSet.removed.length === 0 ? previousState?.outputs : void 0;
3557
4184
  return {
4185
+ incremental,
3558
4186
  previousState,
3559
4187
  previousManifestEntriesBySlug: new Map(
3560
4188
  (previousState?.manifestEntries ?? []).map((entry) => [
@@ -3567,7 +4195,12 @@ var ContentBuildCoordinator = class {
3567
4195
  currentContentIndex,
3568
4196
  currentContentIndexAmbiguities,
3569
4197
  changeSet,
3570
- affectedContent: affected
4198
+ affectedContent: affected,
4199
+ cacheValid: previousState !== void 0,
4200
+ fullRegenerationReasons,
4201
+ reusableOutputs,
4202
+ externalDependencies,
4203
+ externalDependencyFailures
3571
4204
  };
3572
4205
  }
3573
4206
  async getContentAliases(currentEntries, previousState, changeSet) {
@@ -3590,6 +4223,31 @@ var ContentBuildCoordinator = class {
3590
4223
  return aliases;
3591
4224
  }
3592
4225
  };
4226
+ function externalDependenciesEqual(previous, current) {
4227
+ if (previous.length !== current.length) return false;
4228
+ const currentById = new Map(
4229
+ current.map((dependency) => [dependency.id, dependency])
4230
+ );
4231
+ return previous.every((dependency) => {
4232
+ const candidate = currentById.get(dependency.id);
4233
+ return candidate !== void 0 && candidate.fingerprint === dependency.fingerprint && candidate.expiresAt === dependency.expiresAt && !isExpired(candidate.expiresAt);
4234
+ });
4235
+ }
4236
+ function isExpired(expiresAt) {
4237
+ return expiresAt !== void 0 && Date.parse(expiresAt) <= Date.now();
4238
+ }
4239
+ function isPublishingStateExpired(state2, buildTime) {
4240
+ return state2?.nextPublishingChangeAt !== void 0 && Date.parse(state2.nextPublishingChangeAt) <= buildTime.getTime();
4241
+ }
4242
+ function nextPublishingChangeAt(manifest, buildTime) {
4243
+ const next = manifest.entries.filter((entry) => entry.publishing.visibility === "scheduled").map((entry) => entry.publishing.publishAt?.getTime()).filter(
4244
+ (time) => time !== void 0 && time > buildTime.getTime()
4245
+ ).reduce(
4246
+ (earliest, time) => earliest === void 0 || time < earliest ? time : earliest,
4247
+ void 0
4248
+ );
4249
+ return next === void 0 ? void 0 : new Date(next).toISOString();
4250
+ }
3593
4251
  function readText2(content) {
3594
4252
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3595
4253
  }
@@ -3642,7 +4300,27 @@ function collectLinkTargets(manifestEntry) {
3642
4300
  ].sort();
3643
4301
  }
3644
4302
 
4303
+ // src/content/content_build_plan.ts
4304
+ function reusableOutputChangeSet(outputs) {
4305
+ const unchanged = [...outputs].sort(
4306
+ (left, right) => left.path.localeCompare(right.path)
4307
+ );
4308
+ return {
4309
+ affected: [],
4310
+ removed: [],
4311
+ unchanged,
4312
+ fullRegenerationRequired: unchanged.some(
4313
+ (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
4314
+ ),
4315
+ candidateOutputCount: unchanged.length,
4316
+ affectedOutputCount: 0,
4317
+ removedOutputCount: 0,
4318
+ unchangedOutputCount: unchanged.length
4319
+ };
4320
+ }
4321
+
3645
4322
  // src/content/content_entry_reader.ts
4323
+ var textDecoder = new TextDecoder();
3646
4324
  var ContentEntryReader = class {
3647
4325
  constructor(source, observability) {
3648
4326
  this.source = source;
@@ -3681,8 +4359,18 @@ var ContentEntryReader = class {
3681
4359
  read(entry) {
3682
4360
  const cached = this.texts.get(entry.path);
3683
4361
  if (cached) return cached;
3684
- const content = this.source.read(entry).then(
3685
- (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
4362
+ const content = this.observability.tracer.span(
4363
+ "content.read",
4364
+ { extension: entry.path.endsWith(".md") ? "markdown" : "asset" },
4365
+ async () => {
4366
+ const value = await this.source.read(entry);
4367
+ const text = typeof value === "string" ? value : textDecoder.decode(value);
4368
+ this.observability.tracer.event("content.read.bytes", {
4369
+ bytes: Buffer.byteLength(text, "utf8"),
4370
+ extension: entry.path.endsWith(".md") ? "markdown" : "asset"
4371
+ });
4372
+ return text;
4373
+ }
3686
4374
  );
3687
4375
  const retain = entry.path.endsWith(".md");
3688
4376
  const tracked = retain ? content : content.finally(() => this.texts.delete(entry.path));
@@ -3809,6 +4497,8 @@ async function mapConcurrent2(values, concurrency, map) {
3809
4497
  import fs4 from "node:fs/promises";
3810
4498
  import path4 from "node:path";
3811
4499
  var INTERNAL_CONTENT_IGNORE_PATTERNS = [
4500
+ ".riebeckite",
4501
+ ".riebeckite/**",
3812
4502
  ".git",
3813
4503
  ".git/**",
3814
4504
  ".github",
@@ -3858,9 +4548,9 @@ var FileSystemContentSource = class {
3858
4548
  return await fs4.readFile(filePath);
3859
4549
  }
3860
4550
  async scanDirectory(directory, parentPath2, exclusions, templaterTemplateFolder) {
3861
- const directoryEntries = await fs4.readdir(directory, {
4551
+ const directoryEntries = (await fs4.readdir(directory, {
3862
4552
  withFileTypes: true
3863
- });
4553
+ })).toSorted((left, right) => left.name.localeCompare(right.name, "en"));
3864
4554
  const entries = [];
3865
4555
  for (const directoryEntry of directoryEntries) {
3866
4556
  if (directoryEntry.isSymbolicLink()) continue;
@@ -4008,9 +4698,14 @@ function readOptionalStableId(value) {
4008
4698
  var ManifestBuilder = class {
4009
4699
  createEntry(slug, markdown, processed, contentIndex, location, publishing) {
4010
4700
  const links = extractContentLinks(markdown, contentIndex);
4011
- const assets = links.filter(
4012
- (link) => (link.kind === "image" || link.kind === "attachment") && link.slug !== null
4013
- ).map((link) => ({ path: link.slug }));
4701
+ const assets = [
4702
+ ...links.filter(
4703
+ (link) => (link.kind === "image" || link.kind === "attachment") && link.slug !== null
4704
+ ).map((link) => ({ path: link.slug })),
4705
+ ...extractMarkdownImages(markdown, slug).map((image) => ({
4706
+ path: image.path
4707
+ }))
4708
+ ];
4014
4709
  const contentId = resolveContentStableId(processed.frontmatter);
4015
4710
  return {
4016
4711
  slug,
@@ -4139,6 +4834,11 @@ function determineOutputChanges(input) {
4139
4834
  (input.previousState?.outputs ?? []).map((output) => [output.path, output])
4140
4835
  );
4141
4836
  const changedContent = collectChangedContent(input);
4837
+ const changedFiles = /* @__PURE__ */ new Set([
4838
+ ...input.changeSet.added,
4839
+ ...input.changeSet.changed,
4840
+ ...input.changeSet.removed
4841
+ ]);
4142
4842
  const changedTags = collectChangedTags(
4143
4843
  input.previousState,
4144
4844
  input.manifest,
@@ -4149,10 +4849,20 @@ function determineOutputChanges(input) {
4149
4849
  input.manifest,
4150
4850
  changedContent
4151
4851
  );
4852
+ const changedExternal = changedExternalDependencies(input);
4853
+ const changedExternalScopes = changedExternalDependencyScopes(input);
4152
4854
  const affected = /* @__PURE__ */ new Map();
4153
4855
  const unchanged = [];
4154
4856
  for (const output of current) {
4155
- if (isOutputAffected(output, changedContent, changedTags, changedFolders)) {
4857
+ if (isOutputAffected(
4858
+ output,
4859
+ changedContent,
4860
+ changedTags,
4861
+ changedFolders,
4862
+ changedFiles,
4863
+ changedExternal,
4864
+ changedExternalScopes
4865
+ )) {
4156
4866
  affected.set(output.path, output);
4157
4867
  } else {
4158
4868
  unchanged.push(output);
@@ -4168,7 +4878,7 @@ function determineOutputChanges(input) {
4168
4878
  const removed = [...previous.values()].filter(
4169
4879
  (output) => !currentPaths.has(output.path)
4170
4880
  );
4171
- const fullRegenerationRequired = current.some(
4881
+ const fullRegenerationRequired = (input.externalDependencyFailures?.length ?? 0) > 0 || current.some(
4172
4882
  (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
4173
4883
  );
4174
4884
  const affectedValues = [...affected.values()].sort(compareOutput);
@@ -4198,7 +4908,10 @@ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDep
4198
4908
  });
4199
4909
  }
4200
4910
  for (const output of pluginPageOutputs) {
4201
- addOutput(outputs, { ...output, path: htmlOutputPath(output.path) });
4911
+ addOutput(outputs, {
4912
+ ...output,
4913
+ path: htmlOutputPath(output.path)
4914
+ });
4202
4915
  }
4203
4916
  for (const output of manifest.generatedOutputs) {
4204
4917
  addOutput(outputs, {
@@ -4225,10 +4938,22 @@ function contentOutput(entry, dependencies) {
4225
4938
  dependencies: [{ type: "content", slug: entry.slug }, ...dependencies]
4226
4939
  };
4227
4940
  }
4228
- function isOutputAffected(output, changedContent, changedTags, changedFolders) {
4941
+ function isOutputAffected(output, changedContent, changedTags, changedFolders, changedFiles, changedExternal, changedExternalScopes) {
4942
+ if (isAffectedByExternalScope(output, changedExternalScopes)) return true;
4229
4943
  for (const dependency of output.dependencies) {
4230
4944
  if (dependency.type === "unknown") return true;
4945
+ if (dependency.type === "external" && changedExternal.has(dependency.id))
4946
+ return true;
4231
4947
  if (dependency.type === "global") return changedContent.size > 0;
4948
+ if (dependency.type === "projection" && (dependency.projection === "navigation" || dependency.projection === "recent-posts")) {
4949
+ return changedContent.size > 0;
4950
+ }
4951
+ if (dependency.type === "projection" && dependency.projection === "taxonomy-tags") {
4952
+ return changedTags.size > 0;
4953
+ }
4954
+ if (dependency.type === "projection" && dependency.projection === "taxonomy-folders") {
4955
+ return changedFolders.size > 0;
4956
+ }
4232
4957
  if (dependency.type === "content" && changedContent.has(dependency.slug)) {
4233
4958
  return true;
4234
4959
  }
@@ -4237,9 +4962,58 @@ function isOutputAffected(output, changedContent, changedTags, changedFolders) {
4237
4962
  if (dependency.type === "folder" && changedFolders.has(dependency.folder)) {
4238
4963
  return true;
4239
4964
  }
4965
+ if (dependency.type === "file" && changedFiles.has(dependency.path)) {
4966
+ return true;
4967
+ }
4240
4968
  }
4241
4969
  return false;
4242
4970
  }
4971
+ function isAffectedByExternalScope(output, changedExternalScopes) {
4972
+ for (const scope of changedExternalScopes.values()) {
4973
+ if (scope.type === "all") return true;
4974
+ if (scope.type === "content" && output.kind === "content") return true;
4975
+ if (scope.type === "output" && scope.paths.includes(output.path))
4976
+ return true;
4977
+ }
4978
+ return false;
4979
+ }
4980
+ function changedExternalDependencies(input) {
4981
+ const previous = new Map(
4982
+ (input.previousState?.externalDependencies ?? []).map((dependency) => [
4983
+ dependency.id,
4984
+ dependency
4985
+ ])
4986
+ );
4987
+ const changed = /* @__PURE__ */ new Set();
4988
+ for (const dependency of input.externalDependencies ?? []) {
4989
+ const prior = previous.get(dependency.id);
4990
+ if (!prior || prior.fingerprint !== dependency.fingerprint || prior.expiresAt !== dependency.expiresAt || isExpired2(dependency.expiresAt))
4991
+ changed.add(dependency.id);
4992
+ previous.delete(dependency.id);
4993
+ }
4994
+ for (const id of previous.keys()) changed.add(id);
4995
+ return changed;
4996
+ }
4997
+ function changedExternalDependencyScopes(input) {
4998
+ const previous = new Map(
4999
+ (input.previousState?.externalDependencies ?? []).map((dependency) => [
5000
+ dependency.id,
5001
+ dependency
5002
+ ])
5003
+ );
5004
+ const changed = /* @__PURE__ */ new Map();
5005
+ for (const dependency of input.externalDependencies ?? []) {
5006
+ const prior = previous.get(dependency.id);
5007
+ if (!prior || prior.fingerprint !== dependency.fingerprint || prior.expiresAt !== dependency.expiresAt || isExpired2(dependency.expiresAt)) {
5008
+ changed.set(dependency.id, dependency.scope);
5009
+ }
5010
+ previous.delete(dependency.id);
5011
+ }
5012
+ for (const dependency of previous.values()) {
5013
+ changed.set(dependency.id, dependency.scope);
5014
+ }
5015
+ return changed;
5016
+ }
4243
5017
  function collectChangedContent(input) {
4244
5018
  const changed = /* @__PURE__ */ new Set([
4245
5019
  ...input.affectedContent.direct,
@@ -4262,6 +5036,9 @@ function collectChangedContent(input) {
4262
5036
  }
4263
5037
  return changed;
4264
5038
  }
5039
+ function isExpired2(expiresAt) {
5040
+ return expiresAt !== void 0 && Date.parse(expiresAt) <= Date.now();
5041
+ }
4265
5042
  function collectChangedTags(previousState, manifest, changedContent) {
4266
5043
  const previousBySlug = new Map(
4267
5044
  (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
@@ -4408,7 +5185,12 @@ var ContentManager = class {
4408
5185
  processedContentCount = 0;
4409
5186
  reusedContentCount = 0;
4410
5187
  outputChangeSet = null;
5188
+ buildPlan = null;
5189
+ buildStateCommitRequested = false;
5190
+ buildStateCommitted = false;
5191
+ cacheManifestEntries = [];
4411
5192
  trackedContentDependencies = /* @__PURE__ */ new Map();
5193
+ processedContentCacheFingerprints = /* @__PURE__ */ new Map();
4412
5194
  async getAllPosts() {
4413
5195
  return (await this.scan()).filter((entry) => entry.path.endsWith(".md")).map((entry) => ({ slug: entry.path.replace(/\.md$/, "") }));
4414
5196
  }
@@ -4418,13 +5200,13 @@ var ContentManager = class {
4418
5200
  async getPost(slug) {
4419
5201
  return await this.entryReader.readText(`${slug}.md`);
4420
5202
  }
4421
- async getContentIndex(preparation) {
5203
+ async getContentIndex(plan) {
4422
5204
  if (this.contentIndex) return this.contentIndex;
4423
- if (!preparation && this.contentIndexPromise)
5205
+ if (!plan && this.contentIndexPromise)
4424
5206
  return (await this.contentIndexPromise).index;
4425
- if (preparation) {
4426
- this.contentIndex = preparation.currentContentIndex;
4427
- this.contentIndexAmbiguities = preparation.currentContentIndexAmbiguities;
5207
+ if (plan) {
5208
+ this.contentIndex = plan.currentContentIndex;
5209
+ this.contentIndexAmbiguities = plan.currentContentIndexAmbiguities;
4428
5210
  return this.contentIndex;
4429
5211
  }
4430
5212
  const promise = this.observability().tracer.span(
@@ -4435,14 +5217,14 @@ var ContentManager = class {
4435
5217
  (entry) => this.entryReader.read(entry)
4436
5218
  )
4437
5219
  );
4438
- if (!preparation) this.contentIndexPromise = promise;
5220
+ if (!plan) this.contentIndexPromise = promise;
4439
5221
  try {
4440
5222
  const built = await promise;
4441
5223
  this.contentIndex = built.index;
4442
5224
  this.contentIndexAmbiguities = built.ambiguities;
4443
5225
  return this.contentIndex;
4444
5226
  } finally {
4445
- if (!preparation) this.contentIndexPromise = null;
5227
+ if (!plan) this.contentIndexPromise = null;
4446
5228
  }
4447
5229
  }
4448
5230
  async getProcessedContent(slug) {
@@ -4467,14 +5249,27 @@ var ContentManager = class {
4467
5249
  this.getContentIndex(),
4468
5250
  this.getPost(slug)
4469
5251
  ]);
5252
+ this.getProcessedContentCacheFingerprint(slug, rawPost);
4470
5253
  await this.pluginRuntime.startBuild(contentIndex);
4471
5254
  await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4472
5255
  await this.ensureRoutableSlugs();
5256
+ const locations = await this.getContentLocations();
5257
+ if (this.pipelineOptions.config) {
5258
+ this.pipelineFingerprint = this.computePipelineFingerprint(locations);
5259
+ }
4473
5260
  this.pipeline ??= new Pipeline(
4474
5261
  contentIndex,
4475
- await this.locationResolver.getPermalinks(),
5262
+ new Map(
5263
+ [...locations.entries()].map(([locationSlug, location]) => [
5264
+ locationSlug,
5265
+ location.permalink
5266
+ ])
5267
+ ),
4476
5268
  (postSlug) => this.getPost(postSlug),
4477
- this.pipelineOptions,
5269
+ {
5270
+ ...this.pipelineOptions,
5271
+ contentScopeFingerprint: this.pipelineFingerprint ?? void 0
5272
+ },
4478
5273
  this.isBuildTime
4479
5274
  );
4480
5275
  const content = await this.pipeline.execute(rawPost, {
@@ -4504,6 +5299,7 @@ var ContentManager = class {
4504
5299
  return content;
4505
5300
  }
4506
5301
  async getManifest(options) {
5302
+ this.requestBuildStateCommit(options);
4507
5303
  if (this.manifest) return this.manifest;
4508
5304
  if (this.manifestPromise) return await this.manifestPromise;
4509
5305
  this.manifestPromise = this.buildManifest(options);
@@ -4517,19 +5313,19 @@ var ContentManager = class {
4517
5313
  async buildManifest(options) {
4518
5314
  if (this.manifest) return this.manifest;
4519
5315
  this.enableBuildTime();
4520
- const preparation = options ? await this.buildCoordinator.getPreparation(
4521
- options.incremental,
4522
- this.getPipelineFingerprint(),
4523
- this.requiresFullContentRegeneration()
4524
- ) : void 0;
5316
+ const config = this.pipelineOptions.config;
5317
+ const plan = await this.getBuildPlan(options);
5318
+ const locations = await this.getContentLocations();
5319
+ if (config) {
5320
+ this.pipelineFingerprint = this.computePipelineFingerprint(locations);
5321
+ }
4525
5322
  return await this.observability().tracer.span(
4526
5323
  "content.manifest",
4527
5324
  {},
4528
5325
  async () => {
4529
- const [posts, contentIndex, locations] = await Promise.all([
5326
+ const [posts, contentIndex] = await Promise.all([
4530
5327
  this.getAllPosts(),
4531
- this.getContentIndex(preparation),
4532
- this.getContentLocations()
5328
+ this.getContentIndex(plan)
4533
5329
  ]);
4534
5330
  const entries = await this.observability().tracer.span(
4535
5331
  "content.manifest.entries",
@@ -4538,12 +5334,14 @@ var ContentManager = class {
4538
5334
  posts,
4539
5335
  CONTENT_PROCESSING_CONCURRENCY,
4540
5336
  async (post) => {
4541
- const reused = this.reuseManifestEntry(post.slug, preparation);
5337
+ const rawPost = await this.getPost(post.slug);
5338
+ const reused = this.reuseManifestEntry(
5339
+ post.slug,
5340
+ rawPost,
5341
+ plan
5342
+ );
4542
5343
  if (reused) return reused;
4543
- const [rawPost, processed] = await Promise.all([
4544
- this.getPost(post.slug),
4545
- this.getProcessedContent(post.slug)
4546
- ]);
5344
+ const processed = await this.getProcessedContent(post.slug);
4547
5345
  const location = locations.get(post.slug);
4548
5346
  if (!location) {
4549
5347
  throw new Error(
@@ -4583,7 +5381,12 @@ var ContentManager = class {
4583
5381
  manifest.pageRoutes = [
4584
5382
  ...await this.pluginRuntime.getPageRoutes(manifest, contentIndex)
4585
5383
  ];
4586
- await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
5384
+ await this.pluginRuntime.runManifestCreated(
5385
+ manifest,
5386
+ contentIndex,
5387
+ plan.previousState?.manifestHookCache,
5388
+ this.getPipelineFingerprint()
5389
+ );
4587
5390
  this.synchronizeManifestHtml(manifest, cacheManifestEntries);
4588
5391
  manifest.assets = this.pluginRuntime.collectAssets();
4589
5392
  manifest.clientEntries = this.pluginRuntime.collectClientEntries();
@@ -4591,20 +5394,29 @@ var ContentManager = class {
4591
5394
  ...this.pluginRuntime.getDiagnostics(),
4592
5395
  ...await this.pluginRuntime.collectDiagnostics(contentIndex)
4593
5396
  ];
5397
+ await this.pluginRuntime.runGeneratedOutputPlans(manifest);
4594
5398
  await this.pluginRuntime.runBuildEnd(manifest, contentIndex);
5399
+ await this.emitPublicImageAssets(manifest);
4595
5400
  manifest.generatedOutputs = this.pluginRuntime.collectGeneratedOutputs();
4596
- const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4597
- manifest,
4598
- contentIndex
5401
+ const pluginPageOutputs = await this.observability().tracer.span(
5402
+ "output.dependencies.plugin",
5403
+ {},
5404
+ () => this.pluginRuntime.getPageOutputs(manifest, contentIndex)
5405
+ );
5406
+ this.outputChangeSet = await this.observability().tracer.span(
5407
+ "output.dependencies.resolve",
5408
+ {},
5409
+ () => determineOutputChanges({
5410
+ manifest,
5411
+ previousState: plan.previousState,
5412
+ changeSet: plan.changeSet,
5413
+ affectedContent: plan.affectedContent,
5414
+ pluginPageOutputs,
5415
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies(),
5416
+ externalDependencies: plan.externalDependencies,
5417
+ externalDependencyFailures: plan.externalDependencyFailures
5418
+ })
4599
5419
  );
4600
- this.outputChangeSet = preparation ? determineOutputChanges({
4601
- manifest,
4602
- previousState: preparation.previousState,
4603
- changeSet: preparation.changeSet,
4604
- affectedContent: preparation.affectedContent,
4605
- pluginPageOutputs,
4606
- contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4607
- }) : null;
4608
5420
  if (this.outputChangeSet) {
4609
5421
  this.observability().tracer.event("build.incremental.outputs", {
4610
5422
  candidateOutputCount: this.outputChangeSet.candidateOutputCount,
@@ -4614,19 +5426,9 @@ var ContentManager = class {
4614
5426
  fullRegenerationRequired: this.outputChangeSet.fullRegenerationRequired
4615
5427
  });
4616
5428
  }
4617
- if (preparation)
4618
- await this.buildCoordinator.commit(
4619
- preparation,
4620
- manifest,
4621
- this.getPipelineFingerprint(),
4622
- this.outputChangeSet ? [
4623
- ...this.outputChangeSet.affected,
4624
- ...this.outputChangeSet.unchanged
4625
- ] : [],
4626
- cacheManifestEntries,
4627
- this.trackedContentDependencies
4628
- );
4629
5429
  this.manifest = manifest;
5430
+ this.cacheManifestEntries = cacheManifestEntries;
5431
+ await this.commitBuildState();
4630
5432
  return manifest;
4631
5433
  }
4632
5434
  );
@@ -4640,31 +5442,72 @@ var ContentManager = class {
4640
5442
  getReusedContentCount() {
4641
5443
  return this.reusedContentCount;
4642
5444
  }
5445
+ getManifestHookMetrics() {
5446
+ return this.pluginRuntime.getManifestHookMetrics();
5447
+ }
5448
+ async getNoChangeFastPathBlockReason(options = {}) {
5449
+ const plan = await this.getBuildPlan(options);
5450
+ if (plan.fullRegenerationReasons.includes("publishing-state-expired"))
5451
+ return "publishing-state-expired";
5452
+ if (plan.reusableOutputs?.some(
5453
+ (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
5454
+ ))
5455
+ return "output-dependency-unknown";
5456
+ return this.pluginRuntime.getNoChangeFastPathBlockReason(
5457
+ plan.previousState?.manifestHookCache ?? [],
5458
+ this.getPipelineFingerprint()
5459
+ );
5460
+ }
4643
5461
  async getOutputChangeSet(options = {}) {
4644
- await this.getManifest(options);
5462
+ this.requestBuildStateCommit(options);
5463
+ if (this.outputChangeSet) {
5464
+ await this.commitBuildState();
5465
+ return this.outputChangeSet;
5466
+ }
5467
+ const plan = await this.getBuildPlan(options);
5468
+ if (plan.reusableOutputs) {
5469
+ this.outputChangeSet = await this.observability().tracer.span(
5470
+ "output.dependencies.reuse",
5471
+ { outputCount: plan.reusableOutputs.length },
5472
+ async () => reusableOutputChangeSet(plan.reusableOutputs ?? [])
5473
+ );
5474
+ return this.outputChangeSet;
5475
+ }
5476
+ await this.observability().tracer.span(
5477
+ "output.dependencies.manifest",
5478
+ {},
5479
+ () => this.getManifest(options)
5480
+ );
4645
5481
  if (!this.outputChangeSet) {
4646
5482
  const manifest = await this.getManifest();
4647
5483
  const contentIndex = await this.getContentIndex();
4648
- const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4649
- manifest,
4650
- contentIndex
5484
+ const pluginPageOutputs = await this.observability().tracer.span(
5485
+ "output.dependencies.plugin",
5486
+ {},
5487
+ () => this.pluginRuntime.getPageOutputs(manifest, contentIndex)
5488
+ );
5489
+ return await this.observability().tracer.span(
5490
+ "output.dependencies.resolve",
5491
+ {},
5492
+ () => determineOutputChanges({
5493
+ manifest,
5494
+ previousState: void 0,
5495
+ changeSet: {
5496
+ added: manifest.entries.map((entry) => `${entry.slug}.md`),
5497
+ changed: [],
5498
+ removed: [],
5499
+ unchanged: []
5500
+ },
5501
+ affectedContent: {
5502
+ direct: new Set(manifest.entries.map((entry) => entry.slug)),
5503
+ dependent: /* @__PURE__ */ new Set()
5504
+ },
5505
+ pluginPageOutputs,
5506
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies(),
5507
+ externalDependencies: plan.externalDependencies,
5508
+ externalDependencyFailures: plan.externalDependencyFailures
5509
+ })
4651
5510
  );
4652
- return determineOutputChanges({
4653
- manifest,
4654
- previousState: void 0,
4655
- changeSet: {
4656
- added: manifest.entries.map((entry) => `${entry.slug}.md`),
4657
- changed: [],
4658
- removed: [],
4659
- unchanged: []
4660
- },
4661
- affectedContent: {
4662
- direct: new Set(manifest.entries.map((entry) => entry.slug)),
4663
- dependent: /* @__PURE__ */ new Set()
4664
- },
4665
- pluginPageOutputs,
4666
- contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4667
- });
4668
5511
  }
4669
5512
  return this.outputChangeSet;
4670
5513
  }
@@ -4769,29 +5612,113 @@ var ContentManager = class {
4769
5612
  publishStrategy() {
4770
5613
  return this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4771
5614
  }
4772
- reuseManifestEntry(slug, preparation) {
4773
- if (!preparation?.previousState?.manifestEntries) return null;
4774
- if (preparation.previousState.pipelineFingerprint !== this.getPipelineFingerprint()) {
5615
+ 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(
5623
+ slug,
5624
+ source
5625
+ );
5626
+ if (!contentCacheFingerprint) return null;
5627
+ if (plan.previousState.entries[`${slug}.md`]?.processedContentCacheFingerprint !== contentCacheFingerprint) {
4775
5628
  return null;
4776
5629
  }
4777
- if (preparation.affectedContent.direct.has(slug)) return null;
4778
- if (preparation.affectedContent.dependent.has(slug)) return null;
4779
- const entry = preparation.previousManifestEntriesBySlug.get(slug);
5630
+ const entry = plan.previousManifestEntriesBySlug.get(slug);
4780
5631
  if (!entry) return null;
4781
5632
  this.reusedContentCount += 1;
5633
+ this.processedContentCacheFingerprints.set(slug, contentCacheFingerprint);
4782
5634
  this.observability().tracer.event("content.reuse", { slug });
4783
5635
  return cloneManifestEntry(entry);
4784
5636
  }
4785
5637
  getPipelineFingerprint() {
4786
5638
  if (!this.pipelineOptions.config) return void 0;
4787
- this.pipelineFingerprint ??= computePipelineFingerprint(
4788
- this.pipelineOptions.config
4789
- );
5639
+ this.pipelineFingerprint ??= this.computePipelineFingerprint();
4790
5640
  return this.pipelineFingerprint;
4791
5641
  }
5642
+ requestBuildStateCommit(options) {
5643
+ if (this.pipelineOptions.commitBuildState === false) return;
5644
+ if (options?.incremental === true) this.buildStateCommitRequested = true;
5645
+ }
5646
+ async commitBuildState() {
5647
+ if (this.buildStateCommitted || !this.buildStateCommitRequested || !this.manifest || !this.outputChangeSet) {
5648
+ return;
5649
+ }
5650
+ const manifest = this.manifest;
5651
+ const outputChangeSet = this.outputChangeSet;
5652
+ const plan = await this.getBuildPlan();
5653
+ if (!plan.incremental) return;
5654
+ await this.observability().tracer.span(
5655
+ "content.build-state.save",
5656
+ {},
5657
+ () => this.buildCoordinator.commit(
5658
+ plan,
5659
+ manifest,
5660
+ this.getPipelineFingerprint(),
5661
+ [...outputChangeSet.affected, ...outputChangeSet.unchanged],
5662
+ this.cacheManifestEntries,
5663
+ this.trackedContentDependencies,
5664
+ plan.externalDependencies,
5665
+ this.processedContentCacheFingerprints,
5666
+ this.pluginRuntime.getManifestHookCache(),
5667
+ this.publishingBuildTime
5668
+ )
5669
+ );
5670
+ this.buildStateCommitted = true;
5671
+ }
5672
+ getBuildPlan(options) {
5673
+ if (this.buildPlan) return this.buildPlan;
5674
+ const plan = this.createBuildPlan(options);
5675
+ this.buildPlan = plan;
5676
+ void plan.catch(() => {
5677
+ if (this.buildPlan === plan) this.buildPlan = null;
5678
+ });
5679
+ return plan;
5680
+ }
5681
+ async createBuildPlan(options) {
5682
+ const config = this.pipelineOptions.config;
5683
+ if (config && hasGlobalContentDependency(config)) {
5684
+ this.pipelineFingerprint = this.computePipelineFingerprint(
5685
+ await this.getContentLocations()
5686
+ );
5687
+ }
5688
+ const contentIndex = await this.getContentIndex();
5689
+ const external = await this.observability().tracer.span(
5690
+ "external.dependencies.resolve",
5691
+ {},
5692
+ () => this.pluginRuntime.resolveExternalDependencies(contentIndex)
5693
+ );
5694
+ return this.buildCoordinator.getPlan(
5695
+ options?.incremental !== false,
5696
+ this.getPipelineFingerprint(),
5697
+ this.requiresFullContentRegeneration(),
5698
+ external.snapshots,
5699
+ external.failures,
5700
+ this.publishingBuildTime
5701
+ );
5702
+ }
5703
+ computePipelineFingerprint(locations) {
5704
+ const config = this.pipelineOptions.config;
5705
+ if (!config) return "";
5706
+ const contentScopeFingerprint = locations && hasGlobalContentDependency(config) ? computeContentScopeFingerprint(locations) : void 0;
5707
+ return computePipelineFingerprint(config, contentScopeFingerprint);
5708
+ }
4792
5709
  requiresFullContentRegeneration() {
4793
5710
  const config = this.pipelineOptions.config;
4794
- return config ? !isPersistentlyCacheable("", config).cacheable : false;
5711
+ return config ? requiresFullContentRegeneration(config) : false;
5712
+ }
5713
+ getProcessedContentCacheFingerprint(slug, source) {
5714
+ const config = this.pipelineOptions.config;
5715
+ if (!config || !isPersistentlyCacheable(slug, config, source).cacheable) {
5716
+ this.processedContentCacheFingerprints.set(slug, void 0);
5717
+ return void 0;
5718
+ }
5719
+ const fingerprint = computeContentCacheFingerprint(slug, source, config);
5720
+ this.processedContentCacheFingerprints.set(slug, fingerprint);
5721
+ return fingerprint;
4795
5722
  }
4796
5723
  isRoutable(slug) {
4797
5724
  if (!this.routableSlugs) {
@@ -4834,6 +5761,21 @@ var ContentManager = class {
4834
5761
  this.isBuildTime = true;
4835
5762
  this.pluginRuntime.enableBuildTime();
4836
5763
  }
5764
+ async emitPublicImageAssets(manifest) {
5765
+ const imagePaths = new Set(
5766
+ manifest.publicEntries.flatMap(
5767
+ (entry) => entry.assets.map((asset) => normalizeContentPath(asset.path)).filter(isImagePath)
5768
+ )
5769
+ );
5770
+ const entriesByPath = new Map(
5771
+ (await this.source.scan()).map((entry) => [entry.path, entry])
5772
+ );
5773
+ for (const path6 of imagePaths) {
5774
+ const entry = entriesByPath.get(path6);
5775
+ if (entry)
5776
+ this.pluginRuntime.emitCoreAsset(path6, await this.source.read(entry));
5777
+ }
5778
+ }
4837
5779
  observability() {
4838
5780
  return this.pipelineOptions.observability ?? noopObservability;
4839
5781
  }
@@ -4853,8 +5795,7 @@ function cloneManifestEntry(entry) {
4853
5795
  links: entry.links.map((link) => ({ ...link })),
4854
5796
  backlinks: [...entry.backlinks],
4855
5797
  assets: entry.assets.map((asset) => ({ ...asset })),
4856
- ...entry.headTags ? { headTags: [...entry.headTags] } : {},
4857
- ...entry.bodySlots ? { bodySlots: { ...entry.bodySlots } } : {}
5798
+ ...entry.headTags ? { headTags: [...entry.headTags] } : {}
4858
5799
  };
4859
5800
  }
4860
5801
  async function mapConcurrent3(values, concurrency, map) {
@@ -4886,6 +5827,12 @@ async function getContentSourceEntry(source, logicalPath) {
4886
5827
  return (await source.scan()).find((candidate) => candidate.path === logicalPath) ?? null;
4887
5828
  }
4888
5829
 
5830
+ // src/content/external_fingerprint.ts
5831
+ import { createHash as createHash7 } from "node:crypto";
5832
+ function fingerprintExternalValue(value) {
5833
+ return createHash7("sha256").update(stableStringify(value)).digest("hex");
5834
+ }
5835
+
4889
5836
  // src/content/folder_entry.ts
4890
5837
  function resolveFolderEntry(manifest, folder, options = {}) {
4891
5838
  const base = normalizeFolder2(folder);
@@ -5151,6 +6098,105 @@ function clampFinite(value, min, max) {
5151
6098
  return clamp(value, min, max);
5152
6099
  }
5153
6100
 
6101
+ // src/content/public_content_view.ts
6102
+ var emptyEntries = Object.freeze([]);
6103
+ function createPublicContentView(manifest) {
6104
+ const routableEntries = manifest.entries.filter(
6105
+ (entry) => entry.publishing.routable
6106
+ );
6107
+ const discoverableEntries = routableEntries.filter(
6108
+ (entry) => entry.publishing.discoverable
6109
+ );
6110
+ const routableSlugs = new Set(routableEntries.map((entry) => entry.slug));
6111
+ const copiesBySlug = new Map(
6112
+ routableEntries.map((entry) => [
6113
+ entry.slug,
6114
+ copyEntry(entry, routableSlugs)
6115
+ ])
6116
+ );
6117
+ const discoverableSlugs = new Set(
6118
+ discoverableEntries.map((entry) => entry.slug)
6119
+ );
6120
+ const routableCopies = routableEntries.map((entry) => copiesBySlug.get(entry.slug)).filter(isDefined);
6121
+ const discoverableCopies = discoverableEntries.map((entry) => copiesBySlug.get(entry.slug)).filter(isDefined);
6122
+ const aliases = createAliasIndex2(routableCopies);
6123
+ const tags = createTagIndex(discoverableCopies);
6124
+ const getDiscoverableRelated = (relatedSlugs) => Object.freeze(
6125
+ relatedSlugs.filter((relatedSlug) => discoverableSlugs.has(relatedSlug)).map((relatedSlug) => copiesBySlug.get(relatedSlug)).filter(isDefined)
6126
+ );
6127
+ return Object.freeze({
6128
+ routableEntries: Object.freeze(routableCopies),
6129
+ discoverableEntries: Object.freeze(discoverableCopies),
6130
+ findRoutableBySlug: (slug) => copiesBySlug.get(slug),
6131
+ findDiscoverableBySlug: (slug) => discoverableSlugs.has(slug) ? copiesBySlug.get(slug) : void 0,
6132
+ findRoutableByAlias: (alias) => aliases.get(alias.toLowerCase()) ?? emptyEntries,
6133
+ getDiscoverableEntriesByTag: (tag) => tags.get(tag) ?? emptyEntries,
6134
+ getDiscoverableOutgoing: (slug) => getDiscoverableRelated(manifest.graph.outgoingSlugs(slug)),
6135
+ getDiscoverableIncoming: (slug) => getDiscoverableRelated(manifest.graph.incomingSlugs(slug)),
6136
+ getDiscoverableNeighbors: (slug) => getDiscoverableRelated(manifest.graph.neighborSlugs(slug))
6137
+ });
6138
+ }
6139
+ function copyEntry(entry, routableSlugs) {
6140
+ return deepFreeze2(
6141
+ structuredClone({
6142
+ slug: entry.slug,
6143
+ ...entry.contentId === void 0 ? {} : { contentId: entry.contentId },
6144
+ permalink: entry.permalink,
6145
+ publicLocation: entry.publicLocation,
6146
+ title: entry.title,
6147
+ ...entry.aliases === void 0 ? {} : { aliases: entry.aliases },
6148
+ publishing: {
6149
+ visibility: entry.publishing.visibility,
6150
+ routable: entry.publishing.routable,
6151
+ discoverable: entry.publishing.discoverable
6152
+ },
6153
+ tags: entry.tags,
6154
+ links: entry.links.filter(
6155
+ (link) => link.kind !== "note" || link.slug === null || routableSlugs.has(link.slug)
6156
+ ),
6157
+ assets: entry.assets
6158
+ })
6159
+ );
6160
+ }
6161
+ function createAliasIndex2(entries) {
6162
+ const aliases = /* @__PURE__ */ new Map();
6163
+ for (const entry of entries) {
6164
+ for (const alias of entry.aliases ?? []) {
6165
+ const matches = aliases.get(alias.toLowerCase()) ?? [];
6166
+ matches.push(entry);
6167
+ aliases.set(alias.toLowerCase(), matches);
6168
+ }
6169
+ }
6170
+ return new Map(
6171
+ [...aliases].map(([alias, entries2]) => [alias, Object.freeze(entries2)])
6172
+ );
6173
+ }
6174
+ function createTagIndex(entries) {
6175
+ const tags = /* @__PURE__ */ new Map();
6176
+ for (const entry of entries) {
6177
+ for (const tag of entry.tags) {
6178
+ const taggedEntries = tags.get(tag) ?? [];
6179
+ taggedEntries.push(entry);
6180
+ tags.set(tag, taggedEntries);
6181
+ }
6182
+ }
6183
+ return new Map(
6184
+ [...tags].map(([tag, entries2]) => [tag, Object.freeze(entries2)])
6185
+ );
6186
+ }
6187
+ function deepFreeze2(value) {
6188
+ if (value === null || typeof value !== "object") {
6189
+ return value;
6190
+ }
6191
+ for (const child of Object.values(value)) {
6192
+ deepFreeze2(child);
6193
+ }
6194
+ return Object.freeze(value);
6195
+ }
6196
+ function isDefined(value) {
6197
+ return value !== void 0;
6198
+ }
6199
+
5154
6200
  // src/content/read_only_content_graph.ts
5155
6201
  async function readOnlyContentGraph(source, locations) {
5156
6202
  const entries = await source.scan();
@@ -5214,25 +6260,6 @@ function resolveBuildOutputDirectory(config) {
5214
6260
  return directory ? path5.resolve(directory) : void 0;
5215
6261
  }
5216
6262
 
5217
- // src/types/content_manifest.ts
5218
- var ARTICLE_SLOT = {
5219
- header: "article.header",
5220
- metadata: "article.metadata",
5221
- aside: "article.aside",
5222
- beforeContent: "article.before-content",
5223
- afterContent: "article.after-content",
5224
- footer: "article.footer"
5225
- };
5226
- function appendContentBodySlot(entry, slot, html) {
5227
- if (!html.trim()) return;
5228
- const previous = entry.bodySlots?.[slot];
5229
- entry.bodySlots = {
5230
- ...entry.bodySlots,
5231
- [slot]: previous ? `${previous}
5232
- ${html}` : html
5233
- };
5234
- }
5235
-
5236
6263
  // src/types/plugin_endpoint.ts
5237
6264
  function defineEndpoint(path6, handler, options = {}) {
5238
6265
  if (options.cacheControl === void 0) {
@@ -5300,8 +6327,8 @@ function calculateReadingTime(html) {
5300
6327
  return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
5301
6328
  }
5302
6329
  export {
5303
- ARTICLE_SLOT,
5304
6330
  ATTACHMENTS_BASE_PATH,
6331
+ CONTENT_BUILD_STATE_VERSION,
5305
6332
  CompositeTraceSink,
5306
6333
  ConfigValidationError,
5307
6334
  ConsoleLogger,
@@ -5315,7 +6342,6 @@ export {
5315
6342
  PluginDependencyError,
5316
6343
  PluginHookError,
5317
6344
  SinkTracer,
5318
- appendContentBodySlot,
5319
6345
  attachErrorPath,
5320
6346
  attachmentUrl,
5321
6347
  buildContentCollections,
@@ -5326,6 +6352,7 @@ export {
5326
6352
  createClientEntry,
5327
6353
  createContentGraph,
5328
6354
  createPluginMemo,
6355
+ createPublicContentView,
5329
6356
  createStyleAsset,
5330
6357
  createUnavailableGeneratedOutputSink,
5331
6358
  defineConfig,
@@ -5338,6 +6365,7 @@ export {
5338
6365
  escapeScriptJson,
5339
6366
  extractFrontmatterAliases,
5340
6367
  fingerprintContentEntries,
6368
+ fingerprintExternalValue,
5341
6369
  getContentSourceEntry,
5342
6370
  getEntryLanguage,
5343
6371
  getExtension,