@riebeckite/core 0.0.22 → 0.0.24

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);
@@ -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
@@ -2197,7 +2305,8 @@ var Pipeline = class {
2197
2305
  tracer: this.options.observability?.tracer
2198
2306
  });
2199
2307
  this.pipelineFingerprint = computePipelineFingerprint(
2200
- this.options.config
2308
+ this.options.config,
2309
+ this.options.contentScopeFingerprint
2201
2310
  );
2202
2311
  }
2203
2312
  }
@@ -2224,9 +2333,11 @@ var Pipeline = class {
2224
2333
  const effectiveContentSource = dependencyTracker ? dependencyTracker.contentSource : this.options.contentSource;
2225
2334
  const sourceSlug = context.sourceSlug;
2226
2335
  if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && this.options.contentSource && this.options.config) {
2336
+ const persistentCache = this.persistentCache;
2227
2337
  const cacheable = isPersistentlyCacheable(
2228
2338
  sourceSlug,
2229
- this.options.config
2339
+ this.options.config,
2340
+ markDownContent
2230
2341
  );
2231
2342
  if (cacheable.cacheable) {
2232
2343
  const frontmatter = extractFrontmatter(markDownContent);
@@ -2234,12 +2345,21 @@ var Pipeline = class {
2234
2345
  slug: sourceSlug,
2235
2346
  source: markDownContent,
2236
2347
  frontmatter,
2237
- pipelineFingerprint: this.pipelineFingerprint
2348
+ pipelineFingerprint: this.contentPipelineFingerprint(
2349
+ sourceSlug,
2350
+ markDownContent
2351
+ )
2238
2352
  });
2239
- 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
+ );
2240
2358
  if (lookup.status === "hit") {
2241
- const invalidDependency = await this.findInvalidDependency(
2242
- 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)
2243
2363
  );
2244
2364
  if (!invalidDependency) {
2245
2365
  this.reportContentDependencies(
@@ -2350,7 +2470,11 @@ var Pipeline = class {
2350
2470
  this.use(processor, rehypeKatex, { output: "mathml", strict: false });
2351
2471
  this.use(processor, rehypeFormat);
2352
2472
  this.use(processor, rehypeStringify, { allowDangerousHtml: true });
2353
- 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
+ );
2354
2478
  const result = {
2355
2479
  frontmatter: file.data.matter || {},
2356
2480
  html: String(file.value)
@@ -2367,7 +2491,8 @@ var Pipeline = class {
2367
2491
  if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && dependencyTracker && this.options.config) {
2368
2492
  const cacheable = isPersistentlyCacheable(
2369
2493
  sourceSlug,
2370
- this.options.config
2494
+ this.options.config,
2495
+ markDownContent
2371
2496
  );
2372
2497
  if (cacheable.cacheable) {
2373
2498
  const frontmatter = extractFrontmatter(markDownContent);
@@ -2375,7 +2500,10 @@ var Pipeline = class {
2375
2500
  slug: sourceSlug,
2376
2501
  source: markDownContent,
2377
2502
  frontmatter,
2378
- pipelineFingerprint: this.pipelineFingerprint
2503
+ pipelineFingerprint: this.contentPipelineFingerprint(
2504
+ sourceSlug,
2505
+ markDownContent
2506
+ )
2379
2507
  });
2380
2508
  const dependencies = dependencyTracker.dependencies();
2381
2509
  const entry = {
@@ -2392,6 +2520,11 @@ var Pipeline = class {
2392
2520
  reportContentDependencies(slug, dependencies) {
2393
2521
  this.options.onContentDependencies?.(slug, dependencies);
2394
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
+ }
2395
2528
  async findInvalidDependency(dependencies) {
2396
2529
  const contentSource = this.options.contentSource;
2397
2530
  if (!contentSource) return null;
@@ -2677,18 +2810,141 @@ function escapeRegExp(value) {
2677
2810
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2678
2811
  }
2679
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
+
2680
2936
  // src/types/plugin_asset.ts
2681
- function createStyleAsset(pluginName) {
2937
+ function createStyleAsset(pluginName, moduleSpecifier) {
2682
2938
  return {
2683
2939
  pluginName,
2684
2940
  kind: "style",
2685
- moduleSpecifier: `@riebeckite/plugin-${pluginName}/style.css`
2941
+ moduleSpecifier
2686
2942
  };
2687
2943
  }
2688
- function createClientEntry(pluginName, exportName, publicConfig) {
2944
+ function createClientEntry(pluginName, moduleSpecifier, exportName, publicConfig) {
2689
2945
  return {
2690
2946
  pluginName,
2691
- moduleSpecifier: `@riebeckite/plugin-${pluginName}/client`,
2947
+ moduleSpecifier,
2692
2948
  ...exportName === void 0 ? {} : { exportName },
2693
2949
  ...publicConfig === void 0 ? {} : { publicConfig }
2694
2950
  };
@@ -2760,6 +3016,9 @@ var GeneratedOutputRegistry = class {
2760
3016
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
2761
3017
  );
2762
3018
  }
3019
+ count() {
3020
+ return this.outputs.length;
3021
+ }
2763
3022
  };
2764
3023
 
2765
3024
  // src/plugin/plugin_lifecycle.ts
@@ -2841,12 +3100,46 @@ var PluginRuntime = class {
2841
3100
  isBuildTime = false;
2842
3101
  resolvedPlugins = null;
2843
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();
2844
3111
  getDiagnostics() {
2845
3112
  return this.diagnostics;
2846
3113
  }
2847
3114
  getBuildStage() {
2848
3115
  return this.buildStage;
2849
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
+ }
2850
3143
  /**
2851
3144
  * Starts the plugin build lifecycle exactly once, regardless of which public
2852
3145
  * entry point (content processing or location resolution) reaches it first.
@@ -2865,6 +3158,43 @@ var PluginRuntime = class {
2865
3158
  markdown
2866
3159
  });
2867
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
+ }
2868
3198
  async resolveContentLocations(entries, contentIndex) {
2869
3199
  const context = { ...this.createContext(contentIndex), entries };
2870
3200
  const locations = [];
@@ -2911,15 +3241,86 @@ var PluginRuntime = class {
2911
3241
  }
2912
3242
  );
2913
3243
  }
2914
- async runManifestCreated(manifest, contentIndex) {
2915
- await this.runHook(
2916
- "onManifestCreated",
2917
- (plugin) => plugin.onManifestCreated,
2918
- {
2919
- ...this.createContext(contentIndex),
2920
- 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;
2921
3270
  }
2922
- );
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
+ });
2923
3324
  }
2924
3325
  async runBuildEnd(manifest, contentIndex) {
2925
3326
  const context = {
@@ -2931,6 +3332,30 @@ var PluginRuntime = class {
2931
3332
  (plugin) => this.createPluginContext(plugin, context)
2932
3333
  );
2933
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
+ }
2934
3359
  enableBuildTime() {
2935
3360
  this.isBuildTime = true;
2936
3361
  }
@@ -3088,7 +3513,9 @@ var PluginRuntime = class {
3088
3513
  return declarations;
3089
3514
  }
3090
3515
  getContentOutputDependencies() {
3091
- return this.plugins().flatMap((plugin) => plugin.outputDependencies ?? []);
3516
+ return this.plugins().flatMap(
3517
+ (plugin) => plugin.contentOutputDependencies ?? []
3518
+ );
3092
3519
  }
3093
3520
  async collectDiagnostics(contentIndex) {
3094
3521
  const tracer = this.observability().tracer;
@@ -3151,7 +3578,8 @@ var PluginRuntime = class {
3151
3578
  config: this.pipelineOptions.config,
3152
3579
  contentIndex,
3153
3580
  contentSource: this.pipelineOptions.contentSource,
3154
- diagnostics: this.diagnostics
3581
+ diagnostics: this.diagnostics,
3582
+ externalDependencies: this.externalDependencies
3155
3583
  };
3156
3584
  }
3157
3585
  createPluginContext(plugin, context) {
@@ -3202,6 +3630,15 @@ var PluginRuntime = class {
3202
3630
  return this.resolvedPlugins;
3203
3631
  }
3204
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
+ }
3205
3642
  function collectPageTypeOwners(plugins) {
3206
3643
  const pageTypes = /* @__PURE__ */ new Map();
3207
3644
  for (const plugin of plugins) {
@@ -3264,6 +3701,16 @@ function normalizeDirectoryPagePath(pathname) {
3264
3701
  const path6 = normalizePagePath(pathname);
3265
3702
  return path6 === "/" ? path6 : `${path6}/`;
3266
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
+ }
3267
3714
  function normalizePagePath(pathname) {
3268
3715
  const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3269
3716
  return path6 === "/" ? path6 : path6.replace(/\/$/, "");
@@ -3571,34 +4018,46 @@ var ContentBuildCoordinator = class {
3571
4018
  this.dependencies = dependencies;
3572
4019
  }
3573
4020
  dependencies;
3574
- preparation = null;
3575
- getPreparation(incremental, pipelineFingerprint, fullContentRegenerationRequired = false) {
3576
- 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(
3577
4025
  incremental,
3578
4026
  pipelineFingerprint,
3579
- fullContentRegenerationRequired
4027
+ fullContentRegenerationRequired,
4028
+ externalDependencies,
4029
+ externalDependencyFailures,
4030
+ publishingBuildTime
3580
4031
  );
3581
- return this.preparation;
4032
+ this.plan = plan;
4033
+ void plan.catch(() => {
4034
+ if (this.plan === plan) this.plan = null;
4035
+ });
4036
+ return plan;
3582
4037
  }
3583
- 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) {
3584
4039
  const entriesBySlug = new Map(
3585
4040
  manifest.entries.map((entry) => [entry.slug, entry])
3586
4041
  );
4042
+ const nextChangeAt = nextPublishingChangeAt(manifest, publishingBuildTime);
3587
4043
  const state2 = {
3588
4044
  version: CONTENT_BUILD_STATE_VERSION,
3589
4045
  entries: Object.fromEntries(
3590
- preparation.currentEntries.map(({ entry, fingerprint }) => {
4046
+ plan.currentEntries.map(({ entry, fingerprint }) => {
3591
4047
  const manifestEntry = entriesBySlug.get(toSlug(entry.path));
3592
4048
  return [
3593
4049
  entry.path,
3594
4050
  {
3595
4051
  fingerprint,
3596
- aliases: preparation.currentContentAliases.get(entry.path) ?? [],
4052
+ aliases: plan.currentContentAliases.get(entry.path) ?? [],
3597
4053
  dependencies: collectDependencies2(
3598
4054
  manifestEntry,
3599
- trackedDependencies.get(toSlug(entry.path)) ?? preparation.previousState?.entries[entry.path]?.dependencies ?? []
4055
+ trackedDependencies.get(toSlug(entry.path)) ?? plan.previousState?.entries[entry.path]?.dependencies ?? []
3600
4056
  ),
3601
- linkTargets: collectLinkTargets(manifestEntry)
4057
+ linkTargets: collectLinkTargets(manifestEntry),
4058
+ ...processedContentCacheFingerprints.get(toSlug(entry.path)) ? {
4059
+ processedContentCacheFingerprint: processedContentCacheFingerprints.get(toSlug(entry.path))
4060
+ } : {}
3602
4061
  }
3603
4062
  ];
3604
4063
  })
@@ -3610,7 +4069,19 @@ var ContentBuildCoordinator = class {
3610
4069
  ),
3611
4070
  pipelineFingerprint,
3612
4071
  manifestEntries,
3613
- 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
+ } : {}
3614
4085
  };
3615
4086
  try {
3616
4087
  await saveContentBuildState(this.dependencies.buildStatePath, state2);
@@ -3624,7 +4095,7 @@ var ContentBuildCoordinator = class {
3624
4095
  );
3625
4096
  }
3626
4097
  }
3627
- async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired) {
4098
+ async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired, externalDependencies, externalDependencyFailures, publishingBuildTime) {
3628
4099
  const entries = await this.dependencies.observability.tracer.span(
3629
4100
  "content.discovery",
3630
4101
  {},
@@ -3638,12 +4109,20 @@ var ContentBuildCoordinator = class {
3638
4109
  (entry) => this.dependencies.readEntry(entry)
3639
4110
  )
3640
4111
  );
3641
- const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
3642
- const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
3643
- const currentContentAliases = await this.getContentAliases(
3644
- currentEntries,
3645
- previousState,
3646
- 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)
3647
4126
  );
3648
4127
  const {
3649
4128
  index: currentContentIndex,
@@ -3656,26 +4135,54 @@ var ContentBuildCoordinator = class {
3656
4135
  currentContentAliases
3657
4136
  )
3658
4137
  );
3659
- const affected = !fullContentRegenerationRequired && previousState?.pipelineFingerprint === pipelineFingerprint ? determineAffectedContent(
3660
- changeSet,
4138
+ const publishingStateExpired = isPublishingStateExpired(
3661
4139
  previousState,
3662
- currentEntries.map(({ entry }) => entry.path),
3663
- currentContentIndex
3664
- ) : determineAffectedContent(
3665
- allContentChanged(currentEntries),
3666
- void 0,
3667
- currentEntries.map(({ entry }) => entry.path),
3668
- 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
+ )
3669
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");
3670
4169
  this.dependencies.observability.tracer.event("build.incremental", {
3671
- incremental: incremental !== false,
4170
+ incremental,
4171
+ cacheValid: previousState !== void 0,
4172
+ fullRegenerationReasons: fullRegenerationReasons.join(","),
4173
+ externalDependencyFailures: externalDependencyFailures.length,
3672
4174
  added: changeSet.added.length,
3673
4175
  changed: changeSet.changed.length,
3674
4176
  removed: changeSet.removed.length,
3675
4177
  unchanged: changeSet.unchanged.length,
3676
4178
  affected: affected.direct.size + affected.dependent.size
3677
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;
3678
4184
  return {
4185
+ incremental,
3679
4186
  previousState,
3680
4187
  previousManifestEntriesBySlug: new Map(
3681
4188
  (previousState?.manifestEntries ?? []).map((entry) => [
@@ -3688,7 +4195,12 @@ var ContentBuildCoordinator = class {
3688
4195
  currentContentIndex,
3689
4196
  currentContentIndexAmbiguities,
3690
4197
  changeSet,
3691
- affectedContent: affected
4198
+ affectedContent: affected,
4199
+ cacheValid: previousState !== void 0,
4200
+ fullRegenerationReasons,
4201
+ reusableOutputs,
4202
+ externalDependencies,
4203
+ externalDependencyFailures
3692
4204
  };
3693
4205
  }
3694
4206
  async getContentAliases(currentEntries, previousState, changeSet) {
@@ -3711,6 +4223,31 @@ var ContentBuildCoordinator = class {
3711
4223
  return aliases;
3712
4224
  }
3713
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
+ }
3714
4251
  function readText2(content) {
3715
4252
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3716
4253
  }
@@ -3763,7 +4300,27 @@ function collectLinkTargets(manifestEntry) {
3763
4300
  ].sort();
3764
4301
  }
3765
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
+
3766
4322
  // src/content/content_entry_reader.ts
4323
+ var textDecoder = new TextDecoder();
3767
4324
  var ContentEntryReader = class {
3768
4325
  constructor(source, observability) {
3769
4326
  this.source = source;
@@ -3802,8 +4359,18 @@ var ContentEntryReader = class {
3802
4359
  read(entry) {
3803
4360
  const cached = this.texts.get(entry.path);
3804
4361
  if (cached) return cached;
3805
- const content = this.source.read(entry).then(
3806
- (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
+ }
3807
4374
  );
3808
4375
  const retain = entry.path.endsWith(".md");
3809
4376
  const tracked = retain ? content : content.finally(() => this.texts.delete(entry.path));
@@ -3930,6 +4497,8 @@ async function mapConcurrent2(values, concurrency, map) {
3930
4497
  import fs4 from "node:fs/promises";
3931
4498
  import path4 from "node:path";
3932
4499
  var INTERNAL_CONTENT_IGNORE_PATTERNS = [
4500
+ ".riebeckite",
4501
+ ".riebeckite/**",
3933
4502
  ".git",
3934
4503
  ".git/**",
3935
4504
  ".github",
@@ -3979,9 +4548,9 @@ var FileSystemContentSource = class {
3979
4548
  return await fs4.readFile(filePath);
3980
4549
  }
3981
4550
  async scanDirectory(directory, parentPath2, exclusions, templaterTemplateFolder) {
3982
- const directoryEntries = await fs4.readdir(directory, {
4551
+ const directoryEntries = (await fs4.readdir(directory, {
3983
4552
  withFileTypes: true
3984
- });
4553
+ })).toSorted((left, right) => left.name.localeCompare(right.name, "en"));
3985
4554
  const entries = [];
3986
4555
  for (const directoryEntry of directoryEntries) {
3987
4556
  if (directoryEntry.isSymbolicLink()) continue;
@@ -4280,6 +4849,8 @@ function determineOutputChanges(input) {
4280
4849
  input.manifest,
4281
4850
  changedContent
4282
4851
  );
4852
+ const changedExternal = changedExternalDependencies(input);
4853
+ const changedExternalScopes = changedExternalDependencyScopes(input);
4283
4854
  const affected = /* @__PURE__ */ new Map();
4284
4855
  const unchanged = [];
4285
4856
  for (const output of current) {
@@ -4288,7 +4859,9 @@ function determineOutputChanges(input) {
4288
4859
  changedContent,
4289
4860
  changedTags,
4290
4861
  changedFolders,
4291
- changedFiles
4862
+ changedFiles,
4863
+ changedExternal,
4864
+ changedExternalScopes
4292
4865
  )) {
4293
4866
  affected.set(output.path, output);
4294
4867
  } else {
@@ -4305,7 +4878,7 @@ function determineOutputChanges(input) {
4305
4878
  const removed = [...previous.values()].filter(
4306
4879
  (output) => !currentPaths.has(output.path)
4307
4880
  );
4308
- const fullRegenerationRequired = current.some(
4881
+ const fullRegenerationRequired = (input.externalDependencyFailures?.length ?? 0) > 0 || current.some(
4309
4882
  (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
4310
4883
  );
4311
4884
  const affectedValues = [...affected.values()].sort(compareOutput);
@@ -4335,7 +4908,10 @@ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDep
4335
4908
  });
4336
4909
  }
4337
4910
  for (const output of pluginPageOutputs) {
4338
- addOutput(outputs, { ...output, path: htmlOutputPath(output.path) });
4911
+ addOutput(outputs, {
4912
+ ...output,
4913
+ path: htmlOutputPath(output.path)
4914
+ });
4339
4915
  }
4340
4916
  for (const output of manifest.generatedOutputs) {
4341
4917
  addOutput(outputs, {
@@ -4362,10 +4938,22 @@ function contentOutput(entry, dependencies) {
4362
4938
  dependencies: [{ type: "content", slug: entry.slug }, ...dependencies]
4363
4939
  };
4364
4940
  }
4365
- function isOutputAffected(output, changedContent, changedTags, changedFolders, changedFiles) {
4941
+ function isOutputAffected(output, changedContent, changedTags, changedFolders, changedFiles, changedExternal, changedExternalScopes) {
4942
+ if (isAffectedByExternalScope(output, changedExternalScopes)) return true;
4366
4943
  for (const dependency of output.dependencies) {
4367
4944
  if (dependency.type === "unknown") return true;
4945
+ if (dependency.type === "external" && changedExternal.has(dependency.id))
4946
+ return true;
4368
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
+ }
4369
4957
  if (dependency.type === "content" && changedContent.has(dependency.slug)) {
4370
4958
  return true;
4371
4959
  }
@@ -4380,6 +4968,52 @@ function isOutputAffected(output, changedContent, changedTags, changedFolders, c
4380
4968
  }
4381
4969
  return false;
4382
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
+ }
4383
5017
  function collectChangedContent(input) {
4384
5018
  const changed = /* @__PURE__ */ new Set([
4385
5019
  ...input.affectedContent.direct,
@@ -4402,6 +5036,9 @@ function collectChangedContent(input) {
4402
5036
  }
4403
5037
  return changed;
4404
5038
  }
5039
+ function isExpired2(expiresAt) {
5040
+ return expiresAt !== void 0 && Date.parse(expiresAt) <= Date.now();
5041
+ }
4405
5042
  function collectChangedTags(previousState, manifest, changedContent) {
4406
5043
  const previousBySlug = new Map(
4407
5044
  (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
@@ -4548,7 +5185,12 @@ var ContentManager = class {
4548
5185
  processedContentCount = 0;
4549
5186
  reusedContentCount = 0;
4550
5187
  outputChangeSet = null;
5188
+ buildPlan = null;
5189
+ buildStateCommitRequested = false;
5190
+ buildStateCommitted = false;
5191
+ cacheManifestEntries = [];
4551
5192
  trackedContentDependencies = /* @__PURE__ */ new Map();
5193
+ processedContentCacheFingerprints = /* @__PURE__ */ new Map();
4552
5194
  async getAllPosts() {
4553
5195
  return (await this.scan()).filter((entry) => entry.path.endsWith(".md")).map((entry) => ({ slug: entry.path.replace(/\.md$/, "") }));
4554
5196
  }
@@ -4558,13 +5200,13 @@ var ContentManager = class {
4558
5200
  async getPost(slug) {
4559
5201
  return await this.entryReader.readText(`${slug}.md`);
4560
5202
  }
4561
- async getContentIndex(preparation) {
5203
+ async getContentIndex(plan) {
4562
5204
  if (this.contentIndex) return this.contentIndex;
4563
- if (!preparation && this.contentIndexPromise)
5205
+ if (!plan && this.contentIndexPromise)
4564
5206
  return (await this.contentIndexPromise).index;
4565
- if (preparation) {
4566
- this.contentIndex = preparation.currentContentIndex;
4567
- this.contentIndexAmbiguities = preparation.currentContentIndexAmbiguities;
5207
+ if (plan) {
5208
+ this.contentIndex = plan.currentContentIndex;
5209
+ this.contentIndexAmbiguities = plan.currentContentIndexAmbiguities;
4568
5210
  return this.contentIndex;
4569
5211
  }
4570
5212
  const promise = this.observability().tracer.span(
@@ -4575,14 +5217,14 @@ var ContentManager = class {
4575
5217
  (entry) => this.entryReader.read(entry)
4576
5218
  )
4577
5219
  );
4578
- if (!preparation) this.contentIndexPromise = promise;
5220
+ if (!plan) this.contentIndexPromise = promise;
4579
5221
  try {
4580
5222
  const built = await promise;
4581
5223
  this.contentIndex = built.index;
4582
5224
  this.contentIndexAmbiguities = built.ambiguities;
4583
5225
  return this.contentIndex;
4584
5226
  } finally {
4585
- if (!preparation) this.contentIndexPromise = null;
5227
+ if (!plan) this.contentIndexPromise = null;
4586
5228
  }
4587
5229
  }
4588
5230
  async getProcessedContent(slug) {
@@ -4607,14 +5249,27 @@ var ContentManager = class {
4607
5249
  this.getContentIndex(),
4608
5250
  this.getPost(slug)
4609
5251
  ]);
5252
+ this.getProcessedContentCacheFingerprint(slug, rawPost);
4610
5253
  await this.pluginRuntime.startBuild(contentIndex);
4611
5254
  await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4612
5255
  await this.ensureRoutableSlugs();
5256
+ const locations = await this.getContentLocations();
5257
+ if (this.pipelineOptions.config) {
5258
+ this.pipelineFingerprint = this.computePipelineFingerprint(locations);
5259
+ }
4613
5260
  this.pipeline ??= new Pipeline(
4614
5261
  contentIndex,
4615
- await this.locationResolver.getPermalinks(),
5262
+ new Map(
5263
+ [...locations.entries()].map(([locationSlug, location]) => [
5264
+ locationSlug,
5265
+ location.permalink
5266
+ ])
5267
+ ),
4616
5268
  (postSlug) => this.getPost(postSlug),
4617
- this.pipelineOptions,
5269
+ {
5270
+ ...this.pipelineOptions,
5271
+ contentScopeFingerprint: this.pipelineFingerprint ?? void 0
5272
+ },
4618
5273
  this.isBuildTime
4619
5274
  );
4620
5275
  const content = await this.pipeline.execute(rawPost, {
@@ -4644,6 +5299,7 @@ var ContentManager = class {
4644
5299
  return content;
4645
5300
  }
4646
5301
  async getManifest(options) {
5302
+ this.requestBuildStateCommit(options);
4647
5303
  if (this.manifest) return this.manifest;
4648
5304
  if (this.manifestPromise) return await this.manifestPromise;
4649
5305
  this.manifestPromise = this.buildManifest(options);
@@ -4657,19 +5313,19 @@ var ContentManager = class {
4657
5313
  async buildManifest(options) {
4658
5314
  if (this.manifest) return this.manifest;
4659
5315
  this.enableBuildTime();
4660
- const preparation = options ? await this.buildCoordinator.getPreparation(
4661
- options.incremental,
4662
- this.getPipelineFingerprint(),
4663
- this.requiresFullContentRegeneration()
4664
- ) : 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
+ }
4665
5322
  return await this.observability().tracer.span(
4666
5323
  "content.manifest",
4667
5324
  {},
4668
5325
  async () => {
4669
- const [posts, contentIndex, locations] = await Promise.all([
5326
+ const [posts, contentIndex] = await Promise.all([
4670
5327
  this.getAllPosts(),
4671
- this.getContentIndex(preparation),
4672
- this.getContentLocations()
5328
+ this.getContentIndex(plan)
4673
5329
  ]);
4674
5330
  const entries = await this.observability().tracer.span(
4675
5331
  "content.manifest.entries",
@@ -4678,12 +5334,14 @@ var ContentManager = class {
4678
5334
  posts,
4679
5335
  CONTENT_PROCESSING_CONCURRENCY,
4680
5336
  async (post) => {
4681
- 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
+ );
4682
5343
  if (reused) return reused;
4683
- const [rawPost, processed] = await Promise.all([
4684
- this.getPost(post.slug),
4685
- this.getProcessedContent(post.slug)
4686
- ]);
5344
+ const processed = await this.getProcessedContent(post.slug);
4687
5345
  const location = locations.get(post.slug);
4688
5346
  if (!location) {
4689
5347
  throw new Error(
@@ -4723,7 +5381,12 @@ var ContentManager = class {
4723
5381
  manifest.pageRoutes = [
4724
5382
  ...await this.pluginRuntime.getPageRoutes(manifest, contentIndex)
4725
5383
  ];
4726
- await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
5384
+ await this.pluginRuntime.runManifestCreated(
5385
+ manifest,
5386
+ contentIndex,
5387
+ plan.previousState?.manifestHookCache,
5388
+ this.getPipelineFingerprint()
5389
+ );
4727
5390
  this.synchronizeManifestHtml(manifest, cacheManifestEntries);
4728
5391
  manifest.assets = this.pluginRuntime.collectAssets();
4729
5392
  manifest.clientEntries = this.pluginRuntime.collectClientEntries();
@@ -4731,21 +5394,29 @@ var ContentManager = class {
4731
5394
  ...this.pluginRuntime.getDiagnostics(),
4732
5395
  ...await this.pluginRuntime.collectDiagnostics(contentIndex)
4733
5396
  ];
5397
+ await this.pluginRuntime.runGeneratedOutputPlans(manifest);
4734
5398
  await this.pluginRuntime.runBuildEnd(manifest, contentIndex);
4735
5399
  await this.emitPublicImageAssets(manifest);
4736
5400
  manifest.generatedOutputs = this.pluginRuntime.collectGeneratedOutputs();
4737
- const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4738
- manifest,
4739
- 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
+ })
4740
5419
  );
4741
- this.outputChangeSet = preparation ? determineOutputChanges({
4742
- manifest,
4743
- previousState: preparation.previousState,
4744
- changeSet: preparation.changeSet,
4745
- affectedContent: preparation.affectedContent,
4746
- pluginPageOutputs,
4747
- contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4748
- }) : null;
4749
5420
  if (this.outputChangeSet) {
4750
5421
  this.observability().tracer.event("build.incremental.outputs", {
4751
5422
  candidateOutputCount: this.outputChangeSet.candidateOutputCount,
@@ -4755,19 +5426,9 @@ var ContentManager = class {
4755
5426
  fullRegenerationRequired: this.outputChangeSet.fullRegenerationRequired
4756
5427
  });
4757
5428
  }
4758
- if (preparation)
4759
- await this.buildCoordinator.commit(
4760
- preparation,
4761
- manifest,
4762
- this.getPipelineFingerprint(),
4763
- this.outputChangeSet ? [
4764
- ...this.outputChangeSet.affected,
4765
- ...this.outputChangeSet.unchanged
4766
- ] : [],
4767
- cacheManifestEntries,
4768
- this.trackedContentDependencies
4769
- );
4770
5429
  this.manifest = manifest;
5430
+ this.cacheManifestEntries = cacheManifestEntries;
5431
+ await this.commitBuildState();
4771
5432
  return manifest;
4772
5433
  }
4773
5434
  );
@@ -4781,31 +5442,72 @@ var ContentManager = class {
4781
5442
  getReusedContentCount() {
4782
5443
  return this.reusedContentCount;
4783
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
+ }
4784
5461
  async getOutputChangeSet(options = {}) {
4785
- 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
+ );
4786
5481
  if (!this.outputChangeSet) {
4787
5482
  const manifest = await this.getManifest();
4788
5483
  const contentIndex = await this.getContentIndex();
4789
- const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4790
- manifest,
4791
- 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
+ })
4792
5510
  );
4793
- return determineOutputChanges({
4794
- manifest,
4795
- previousState: void 0,
4796
- changeSet: {
4797
- added: manifest.entries.map((entry) => `${entry.slug}.md`),
4798
- changed: [],
4799
- removed: [],
4800
- unchanged: []
4801
- },
4802
- affectedContent: {
4803
- direct: new Set(manifest.entries.map((entry) => entry.slug)),
4804
- dependent: /* @__PURE__ */ new Set()
4805
- },
4806
- pluginPageOutputs,
4807
- contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4808
- });
4809
5511
  }
4810
5512
  return this.outputChangeSet;
4811
5513
  }
@@ -4910,29 +5612,113 @@ var ContentManager = class {
4910
5612
  publishStrategy() {
4911
5613
  return this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4912
5614
  }
4913
- reuseManifestEntry(slug, preparation) {
4914
- if (!preparation?.previousState?.manifestEntries) return null;
4915
- 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) {
4916
5628
  return null;
4917
5629
  }
4918
- if (preparation.affectedContent.direct.has(slug)) return null;
4919
- if (preparation.affectedContent.dependent.has(slug)) return null;
4920
- const entry = preparation.previousManifestEntriesBySlug.get(slug);
5630
+ const entry = plan.previousManifestEntriesBySlug.get(slug);
4921
5631
  if (!entry) return null;
4922
5632
  this.reusedContentCount += 1;
5633
+ this.processedContentCacheFingerprints.set(slug, contentCacheFingerprint);
4923
5634
  this.observability().tracer.event("content.reuse", { slug });
4924
5635
  return cloneManifestEntry(entry);
4925
5636
  }
4926
5637
  getPipelineFingerprint() {
4927
5638
  if (!this.pipelineOptions.config) return void 0;
4928
- this.pipelineFingerprint ??= computePipelineFingerprint(
4929
- this.pipelineOptions.config
4930
- );
5639
+ this.pipelineFingerprint ??= this.computePipelineFingerprint();
4931
5640
  return this.pipelineFingerprint;
4932
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
+ }
4933
5709
  requiresFullContentRegeneration() {
4934
5710
  const config = this.pipelineOptions.config;
4935
- 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;
4936
5722
  }
4937
5723
  isRoutable(slug) {
4938
5724
  if (!this.routableSlugs) {
@@ -5009,8 +5795,7 @@ function cloneManifestEntry(entry) {
5009
5795
  links: entry.links.map((link) => ({ ...link })),
5010
5796
  backlinks: [...entry.backlinks],
5011
5797
  assets: entry.assets.map((asset) => ({ ...asset })),
5012
- ...entry.headTags ? { headTags: [...entry.headTags] } : {},
5013
- ...entry.bodySlots ? { bodySlots: { ...entry.bodySlots } } : {}
5798
+ ...entry.headTags ? { headTags: [...entry.headTags] } : {}
5014
5799
  };
5015
5800
  }
5016
5801
  async function mapConcurrent3(values, concurrency, map) {
@@ -5042,6 +5827,12 @@ async function getContentSourceEntry(source, logicalPath) {
5042
5827
  return (await source.scan()).find((candidate) => candidate.path === logicalPath) ?? null;
5043
5828
  }
5044
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
+
5045
5836
  // src/content/folder_entry.ts
5046
5837
  function resolveFolderEntry(manifest, folder, options = {}) {
5047
5838
  const base = normalizeFolder2(folder);
@@ -5307,6 +6098,105 @@ function clampFinite(value, min, max) {
5307
6098
  return clamp(value, min, max);
5308
6099
  }
5309
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
+
5310
6200
  // src/content/read_only_content_graph.ts
5311
6201
  async function readOnlyContentGraph(source, locations) {
5312
6202
  const entries = await source.scan();
@@ -5370,25 +6260,6 @@ function resolveBuildOutputDirectory(config) {
5370
6260
  return directory ? path5.resolve(directory) : void 0;
5371
6261
  }
5372
6262
 
5373
- // src/types/content_manifest.ts
5374
- var ARTICLE_SLOT = {
5375
- header: "article.header",
5376
- metadata: "article.metadata",
5377
- aside: "article.aside",
5378
- beforeContent: "article.before-content",
5379
- afterContent: "article.after-content",
5380
- footer: "article.footer"
5381
- };
5382
- function appendContentBodySlot(entry, slot, html) {
5383
- if (!html.trim()) return;
5384
- const previous = entry.bodySlots?.[slot];
5385
- entry.bodySlots = {
5386
- ...entry.bodySlots,
5387
- [slot]: previous ? `${previous}
5388
- ${html}` : html
5389
- };
5390
- }
5391
-
5392
6263
  // src/types/plugin_endpoint.ts
5393
6264
  function defineEndpoint(path6, handler, options = {}) {
5394
6265
  if (options.cacheControl === void 0) {
@@ -5456,8 +6327,8 @@ function calculateReadingTime(html) {
5456
6327
  return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
5457
6328
  }
5458
6329
  export {
5459
- ARTICLE_SLOT,
5460
6330
  ATTACHMENTS_BASE_PATH,
6331
+ CONTENT_BUILD_STATE_VERSION,
5461
6332
  CompositeTraceSink,
5462
6333
  ConfigValidationError,
5463
6334
  ConsoleLogger,
@@ -5471,7 +6342,6 @@ export {
5471
6342
  PluginDependencyError,
5472
6343
  PluginHookError,
5473
6344
  SinkTracer,
5474
- appendContentBodySlot,
5475
6345
  attachErrorPath,
5476
6346
  attachmentUrl,
5477
6347
  buildContentCollections,
@@ -5482,6 +6352,7 @@ export {
5482
6352
  createClientEntry,
5483
6353
  createContentGraph,
5484
6354
  createPluginMemo,
6355
+ createPublicContentView,
5485
6356
  createStyleAsset,
5486
6357
  createUnavailableGeneratedOutputSink,
5487
6358
  defineConfig,
@@ -5494,6 +6365,7 @@ export {
5494
6365
  escapeScriptJson,
5495
6366
  extractFrontmatterAliases,
5496
6367
  fingerprintContentEntries,
6368
+ fingerprintExternalValue,
5497
6369
  getContentSourceEntry,
5498
6370
  getEntryLanguage,
5499
6371
  getExtension,