@riebeckite/core 0.0.13 → 0.0.15

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.
Files changed (35) hide show
  1. package/README.md +6 -5
  2. package/README_ja.md +9 -8
  3. package/dist/client.d.ts +2 -2
  4. package/dist/client.js +142 -1
  5. package/dist/index.d.ts +15 -6
  6. package/dist/index.js +2086 -406
  7. package/dist/src/config_deprecations.d.ts +8 -0
  8. package/dist/src/content/affected_content.d.ts +1 -1
  9. package/dist/src/content/content_build_coordinator.d.ts +11 -3
  10. package/dist/src/content/content_build_state.d.ts +13 -2
  11. package/dist/src/content/content_dependency_tracker.d.ts +18 -0
  12. package/dist/src/content/content_entry_reader.d.ts +2 -0
  13. package/dist/src/content/content_index_builder.d.ts +2 -0
  14. package/dist/src/content/content_location_resolver.d.ts +7 -0
  15. package/dist/src/content/content_manager.d.ts +21 -0
  16. package/dist/src/content/content_persistent_cache.d.ts +43 -0
  17. package/dist/src/content/file_system_content_source.d.ts +2 -0
  18. package/dist/src/content/folder_entry.d.ts +35 -0
  19. package/dist/src/content/graph_layout.d.ts +20 -0
  20. package/dist/src/content/manifest_builder.d.ts +2 -1
  21. package/dist/src/content/output_dependency.d.ts +34 -0
  22. package/dist/src/content/publishing.d.ts +15 -0
  23. package/dist/src/deprecation.d.ts +14 -0
  24. package/dist/src/pipeline.d.ts +10 -0
  25. package/dist/src/plugin/plugin_runtime.d.ts +7 -1
  26. package/dist/src/types/content_manifest.d.ts +18 -1
  27. package/dist/src/types/generated_output.d.ts +9 -0
  28. package/dist/src/types/output_dependency.d.ts +14 -0
  29. package/dist/src/types/plugin.d.ts +19 -0
  30. package/dist/src/types/plugin_page.d.ts +6 -0
  31. package/dist/src/types/plugin_pipeline.d.ts +1 -0
  32. package/dist/src/types/post_content.d.ts +2 -0
  33. package/dist/src/types/resolved_riebeckite_config.d.ts +4 -0
  34. package/dist/src/types/riebeckite_config.d.ts +4 -0
  35. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -109,41 +109,41 @@ function validateTheme(value, issues) {
109
109
  }
110
110
  }
111
111
  }
112
- function validateThemeConfig(value, path4, issues) {
113
- validateOptionalString(value.name, `${path4}.name`, issues);
112
+ function validateThemeConfig(value, path5, issues) {
113
+ validateOptionalString(value.name, `${path5}.name`, issues);
114
114
  validateOneOf(
115
115
  value.colorMode,
116
- `${path4}.colorMode`,
116
+ `${path5}.colorMode`,
117
117
  ["light", "dark", "system"],
118
118
  issues
119
119
  );
120
120
  validateOneOf(
121
121
  value.typography,
122
- `${path4}.typography`,
122
+ `${path5}.typography`,
123
123
  ["system", "serif", "sans"],
124
124
  issues
125
125
  );
126
126
  validateOneOf(
127
127
  value.articleLayout,
128
- `${path4}.articleLayout`,
128
+ `${path5}.articleLayout`,
129
129
  ["article", "sidebar", "full-width"],
130
130
  issues
131
131
  );
132
- validateStringArray(value.userCss, `${path4}.userCss`, issues);
132
+ validateStringArray(value.userCss, `${path5}.userCss`, issues);
133
133
  if (value.attributes !== void 0 && !isRecord(value.attributes)) {
134
- issues.push({ path: `${path4}.attributes`, message: "Expected an object." });
134
+ issues.push({ path: `${path5}.attributes`, message: "Expected an object." });
135
135
  }
136
136
  }
137
- function validateThemeStyles(value, path4, issues) {
137
+ function validateThemeStyles(value, path5, issues) {
138
138
  if (value === void 0) return;
139
139
  if (!Array.isArray(value)) {
140
- issues.push({ path: path4, message: "Expected an array." });
140
+ issues.push({ path: path5, message: "Expected an array." });
141
141
  return;
142
142
  }
143
143
  for (const [index, style] of value.entries()) {
144
144
  if (!isRecord(style) || !isNonEmptyString(style.moduleSpecifier)) {
145
145
  issues.push({
146
- path: `${path4}[${index}].moduleSpecifier`,
146
+ path: `${path5}[${index}].moduleSpecifier`,
147
147
  message: "Expected a non-empty string."
148
148
  });
149
149
  }
@@ -163,69 +163,69 @@ function validatePlugins(value, issues) {
163
163
  continue;
164
164
  }
165
165
  const plugin = input;
166
- const path4 = `plugins[${index}]`;
166
+ const path5 = `plugins[${index}]`;
167
167
  if (!isNonEmptyString(plugin.name)) {
168
168
  issues.push({
169
- path: `${path4}.name`,
169
+ path: `${path5}.name`,
170
170
  message: "Expected a non-empty string."
171
171
  });
172
172
  } else if (plugin.enabled !== false) {
173
173
  const duplicateIndex = names.get(plugin.name);
174
174
  if (duplicateIndex !== void 0) {
175
175
  issues.push({
176
- path: `${path4}.name`,
176
+ path: `${path5}.name`,
177
177
  message: `Duplicate plugin name "${plugin.name}" (also plugins[${duplicateIndex}]).`
178
178
  });
179
179
  } else {
180
180
  names.set(plugin.name, index);
181
181
  }
182
182
  }
183
- validateOptionalBoolean(plugin.enabled, `${path4}.enabled`, issues);
184
- validateCapabilityMetadata(plugin, path4, issues);
185
- if (plugin.enabled !== false) validatePluginOptions(plugin, path4, issues);
183
+ validateOptionalBoolean(plugin.enabled, `${path5}.enabled`, issues);
184
+ validateCapabilityMetadata(plugin, path5, issues);
185
+ if (plugin.enabled !== false) validatePluginOptions(plugin, path5, issues);
186
186
  }
187
187
  }
188
- function validateCapabilityMetadata(plugin, path4, issues) {
189
- validateStringArray(plugin.provides, `${path4}.provides`, issues, true);
190
- validateStringArray(plugin.requires, `${path4}.requires`, issues, true);
191
- validateStringArray(plugin.optional, `${path4}.optional`, issues, true);
188
+ function validateCapabilityMetadata(plugin, path5, issues) {
189
+ validateStringArray(plugin.provides, `${path5}.provides`, issues, true);
190
+ validateStringArray(plugin.requires, `${path5}.requires`, issues, true);
191
+ validateStringArray(plugin.optional, `${path5}.optional`, issues, true);
192
192
  }
193
- function validatePluginOptions(plugin, path4, issues) {
193
+ function validatePluginOptions(plugin, path5, issues) {
194
194
  if (!plugin.validateOptions) return;
195
195
  try {
196
196
  const optionIssues = plugin.validateOptions(plugin.options) ?? [];
197
197
  for (const issue of optionIssues) {
198
198
  issues.push({
199
- path: issue.path ? `${path4}.options.${issue.path}` : `${path4}.options`,
199
+ path: issue.path ? `${path5}.options.${issue.path}` : `${path5}.options`,
200
200
  message: issue.message
201
201
  });
202
202
  }
203
203
  } catch (error) {
204
204
  issues.push({
205
- path: `${path4}.options`,
205
+ path: `${path5}.options`,
206
206
  message: `Validator failed: ${toErrorMessage(error)}`
207
207
  });
208
208
  }
209
209
  }
210
- function validateRequiredString(value, path4, issues) {
210
+ function validateRequiredString(value, path5, issues) {
211
211
  if (!isNonEmptyString(value)) {
212
- issues.push({ path: path4, message: "Expected a non-empty string." });
212
+ issues.push({ path: path5, message: "Expected a non-empty string." });
213
213
  }
214
214
  }
215
- function validateOptionalString(value, path4, issues) {
215
+ function validateOptionalString(value, path5, issues) {
216
216
  if (value !== void 0 && typeof value !== "string") {
217
- issues.push({ path: path4, message: "Expected a string." });
217
+ issues.push({ path: path5, message: "Expected a string." });
218
218
  }
219
219
  }
220
- function validateOptionalBoolean(value, path4, issues) {
220
+ function validateOptionalBoolean(value, path5, issues) {
221
221
  if (value !== void 0 && typeof value !== "boolean") {
222
- issues.push({ path: path4, message: "Expected a boolean." });
222
+ issues.push({ path: path5, message: "Expected a boolean." });
223
223
  }
224
224
  }
225
- function validateAbsoluteUrl(value, path4, issues) {
225
+ function validateAbsoluteUrl(value, path5, issues) {
226
226
  if (value === void 0 || value === "") return;
227
227
  if (typeof value !== "string") {
228
- issues.push({ path: path4, message: "Expected an absolute URL." });
228
+ issues.push({ path: path5, message: "Expected an absolute URL." });
229
229
  return;
230
230
  }
231
231
  try {
@@ -233,27 +233,27 @@ function validateAbsoluteUrl(value, path4, issues) {
233
233
  if (url.protocol !== "http:" && url.protocol !== "https:")
234
234
  throw new Error();
235
235
  } catch {
236
- issues.push({ path: path4, message: "Expected an absolute HTTP(S) URL." });
236
+ issues.push({ path: path5, message: "Expected an absolute HTTP(S) URL." });
237
237
  }
238
238
  }
239
- function validateStringArray(value, path4, issues, nonEmpty = false) {
239
+ function validateStringArray(value, path5, issues, nonEmpty = false) {
240
240
  if (value === void 0) return;
241
241
  if (!Array.isArray(value)) {
242
- issues.push({ path: path4, message: "Expected an array." });
242
+ issues.push({ path: path5, message: "Expected an array." });
243
243
  return;
244
244
  }
245
245
  for (const [index, item] of value.entries()) {
246
246
  if (typeof item !== "string" || nonEmpty && !item.trim()) {
247
247
  issues.push({
248
- path: `${path4}[${index}]`,
248
+ path: `${path5}[${index}]`,
249
249
  message: nonEmpty ? "Expected a non-empty string." : "Expected a string."
250
250
  });
251
251
  }
252
252
  }
253
253
  }
254
- function validateOneOf(value, path4, choices, issues) {
254
+ function validateOneOf(value, path5, choices, issues) {
255
255
  if (value !== void 0 && (typeof value !== "string" || !choices.includes(value))) {
256
- issues.push({ path: path4, message: `Expected one of: ${choices.join(", ")}.` });
256
+ issues.push({ path: path5, message: `Expected one of: ${choices.join(", ")}.` });
257
257
  }
258
258
  }
259
259
  function formatValidationIssues(issues) {
@@ -538,8 +538,21 @@ function definePlugin(plugin) {
538
538
  }
539
539
  function resolvePlugins(plugins = []) {
540
540
  const orderedPlugins = plugins.filter((plugin) => Boolean(plugin)).filter((plugin) => plugin.enabled !== false).toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
541
+ assertUniquePluginNames(orderedPlugins);
541
542
  return resolvePluginDependencies(orderedPlugins);
542
543
  }
544
+ function assertUniquePluginNames(plugins) {
545
+ const owners = /* @__PURE__ */ new Map();
546
+ plugins.forEach((plugin, index) => {
547
+ const previous = owners.get(plugin.name);
548
+ if (previous !== void 0) {
549
+ throw new Error(
550
+ `Plugin name "${plugin.name}" is used by both plugins[${previous}] and plugins[${index}]`
551
+ );
552
+ }
553
+ owners.set(plugin.name, index);
554
+ });
555
+ }
543
556
  function getResolvedPluginMetadata(plugins) {
544
557
  return plugins.map((plugin) => ({
545
558
  name: plugin.name,
@@ -626,7 +639,11 @@ function resolveConfig(config) {
626
639
  }
627
640
  },
628
641
  theme,
629
- plugins: resolvePlugins(config.plugins)
642
+ plugins: resolvePlugins(config.plugins),
643
+ cache: {
644
+ enabled: config.cache?.enabled ?? true,
645
+ directory: config.cache?.directory ?? ""
646
+ }
630
647
  };
631
648
  }
632
649
  function resolveThemeConfig(themeInput) {
@@ -659,6 +676,8 @@ function sanitizeThemeAttributes(attributes) {
659
676
  );
660
677
  }
661
678
  function isPublished(config, frontmatter) {
679
+ if (frontmatter?.visibility === "unlisted") return true;
680
+ if (frontmatter?.visibility === "draft") return false;
662
681
  return isPublishable(config.content.filters.publishStrategy, frontmatter);
663
682
  }
664
683
  function isExcluded(patterns, relativePath) {
@@ -693,6 +712,46 @@ function globToRegExp(glob) {
693
712
  return new RegExp(`^${re}$`);
694
713
  }
695
714
 
715
+ // src/deprecation.ts
716
+ function createDeprecationDiagnostic(notice) {
717
+ return {
718
+ code: `deprecated-${notice.kind}`,
719
+ severity: "warning",
720
+ message: formatDeprecationMessage(notice),
721
+ target: notice.target,
722
+ suggestion: notice.replacement ? `Use ${notice.replacement}. ${notice.action}` : notice.action,
723
+ meta: { deprecation: notice }
724
+ };
725
+ }
726
+ function formatDeprecationMessage(notice) {
727
+ const lines = [
728
+ `Deprecated: ${notice.target}`,
729
+ `Deprecated since: ${notice.deprecatedSince}`
730
+ ];
731
+ if (notice.replacement) lines.push(`Use: ${notice.replacement}`);
732
+ lines.push(`Migration: ${notice.action}`);
733
+ if (notice.documentationUrl) lines.push(`See: ${notice.documentationUrl}`);
734
+ if (notice.removedIn) lines.push(`Removal planned: ${notice.removedIn}`);
735
+ return lines.join("\n");
736
+ }
737
+
738
+ // src/config_deprecations.ts
739
+ var configDeprecations = [];
740
+ function collectConfigDeprecationDiagnostics(config, notices = configDeprecations) {
741
+ return notices.filter((notice) => hasConfigPath(config, notice.path)).map(createDeprecationDiagnostic);
742
+ }
743
+ function hasConfigPath(config, path5) {
744
+ let value = config;
745
+ for (const segment of path5.split(".")) {
746
+ if (!isRecord2(value) || !(segment in value)) return false;
747
+ value = value[segment];
748
+ }
749
+ return value !== void 0;
750
+ }
751
+ function isRecord2(value) {
752
+ return typeof value === "object" && value !== null && !Array.isArray(value);
753
+ }
754
+
696
755
  // src/content/image_extensions.ts
697
756
  var IMAGE_EXTENSIONS = [
698
757
  "png",
@@ -746,7 +805,7 @@ import fs from "node:fs/promises";
746
805
  import path from "node:path";
747
806
 
748
807
  // src/content/content_build_state.ts
749
- var CONTENT_BUILD_STATE_VERSION = 2;
808
+ var CONTENT_BUILD_STATE_VERSION = 6;
750
809
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
751
810
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
752
811
 
@@ -770,7 +829,7 @@ async function readContentBuildStateStatus(statePath) {
770
829
  }
771
830
  throw error;
772
831
  }
773
- if (!isRecord2(parsed)) {
832
+ if (!isRecord3(parsed)) {
774
833
  return { kind: "invalid", path: statePath, reason: "invalid-shape" };
775
834
  }
776
835
  if (parsed.version !== CONTENT_BUILD_STATE_VERSION) {
@@ -786,12 +845,12 @@ async function readContentBuildStateStatus(statePath) {
786
845
  entryCount: Object.keys(parsed.entries).length
787
846
  };
788
847
  }
789
- async function saveContentBuildState(statePath, state) {
848
+ async function saveContentBuildState(statePath, state2) {
790
849
  const directory = path.dirname(statePath);
791
850
  const temporaryPath = `${statePath}.${randomUUID()}.tmp`;
792
851
  await fs.mkdir(directory, { recursive: true });
793
852
  try {
794
- await fs.writeFile(temporaryPath, JSON.stringify(state), "utf8");
853
+ await fs.writeFile(temporaryPath, JSON.stringify(state2), "utf8");
795
854
  await fs.rename(temporaryPath, statePath);
796
855
  } catch (error) {
797
856
  await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -810,19 +869,37 @@ function resolveContentBuildStatePath(config, contentDirectory) {
810
869
  );
811
870
  }
812
871
  function isContentBuildState(value) {
813
- return isRecord2(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
872
+ return isRecord3(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
814
873
  }
815
874
  function isContentBuildStateShape(value) {
816
- if (!isRecord2(value.entries) || !isRecord2(value.contentIndex)) return false;
875
+ if (!isRecord3(value.entries) || !isRecord3(value.contentIndex) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs)) {
876
+ return false;
877
+ }
817
878
  return Object.values(value.entries).every(
818
- (entry) => isRecord2(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.dependencies) && entry.dependencies.every(
819
- (dependency) => typeof dependency === "string"
879
+ (entry) => isRecord3(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.aliases) && entry.aliases.every((alias) => typeof alias === "string") && Array.isArray(entry.dependencies) && entry.dependencies.every(
880
+ (dependency) => isRecord3(dependency) && (dependency.kind === "content" || dependency.kind === "file") && typeof dependency.id === "string"
820
881
  )
821
882
  ) && Object.values(value.contentIndex).every(
822
883
  (entry) => typeof entry === "string"
823
884
  );
824
885
  }
825
- function isRecord2(value) {
886
+ function isOutputDescriptors(value) {
887
+ return Array.isArray(value) && value.every(
888
+ (output) => isRecord3(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)
889
+ );
890
+ }
891
+ function isOutputPath(value) {
892
+ if (typeof value !== "string" || value.length === 0 || value.includes("\\") || value.includes("\0") || value.startsWith("/")) {
893
+ return false;
894
+ }
895
+ return value.split("/").every((segment) => segment !== "." && segment !== "..");
896
+ }
897
+ function isOutputDependency(value) {
898
+ if (!isRecord3(value) || typeof value.type !== "string") return false;
899
+ if (value.type === "global" || value.type === "unknown") return true;
900
+ return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string";
901
+ }
902
+ function isRecord3(value) {
826
903
  return typeof value === "object" && value !== null && !Array.isArray(value);
827
904
  }
828
905
  function isNotFoundError(error) {
@@ -1133,9 +1210,9 @@ function buildContentCollections(entries, definitions) {
1133
1210
  function normalizePageSize(value) {
1134
1211
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1135
1212
  }
1136
- function withPage(path4, index) {
1137
- if (index === 0) return path4;
1138
- const base = path4 === "/" ? "" : path4;
1213
+ function withPage(path5, index) {
1214
+ if (index === 0) return path5;
1215
+ const base = path5 === "/" ? "" : path5;
1139
1216
  return normalizePath(`${base}/page/${index + 1}`);
1140
1217
  }
1141
1218
  function defaultPath(context) {
@@ -1163,12 +1240,32 @@ function normalizePath(value) {
1163
1240
  // src/content/content_fingerprint.ts
1164
1241
  import { createHash } from "node:crypto";
1165
1242
  async function fingerprintContentEntries(entries, read) {
1166
- return await Promise.all(
1167
- [...entries].sort((left, right) => left.path.localeCompare(right.path)).map(async (entry) => ({
1168
- entry,
1169
- fingerprint: await fingerprintContentEntry(entry, read)
1170
- }))
1243
+ const sortedEntries = [...entries].sort(
1244
+ (left, right) => left.path.localeCompare(right.path)
1245
+ );
1246
+ return await mapConcurrent(sortedEntries, 64, async (entry) => ({
1247
+ entry,
1248
+ fingerprint: await fingerprintContentEntry(entry, read)
1249
+ }));
1250
+ }
1251
+ async function mapConcurrent(values, concurrency, map) {
1252
+ const results = new Array(values.length);
1253
+ let nextIndex = 0;
1254
+ async function worker() {
1255
+ while (nextIndex < values.length) {
1256
+ const index = nextIndex;
1257
+ nextIndex += 1;
1258
+ const value = values[index];
1259
+ if (value !== void 0) results[index] = await map(value);
1260
+ }
1261
+ }
1262
+ await Promise.all(
1263
+ Array.from(
1264
+ { length: Math.min(concurrency, values.length) },
1265
+ () => worker()
1266
+ )
1171
1267
  );
1268
+ return results;
1172
1269
  }
1173
1270
  async function fingerprintContentEntry(entry, read) {
1174
1271
  if (entry.metadata?.hash) return `hash:${entry.metadata.hash}`;
@@ -1233,6 +1330,10 @@ function resolveDefaultContentLocation(content) {
1233
1330
  };
1234
1331
  }
1235
1332
 
1333
+ // src/content/content_manager.ts
1334
+ import { VFile as VFile2 } from "vfile";
1335
+ import { matter as matter3 } from "vfile-matter";
1336
+
1236
1337
  // src/observability.ts
1237
1338
  var NoopLogger = class {
1238
1339
  debug() {
@@ -1374,12 +1475,334 @@ import remarkMath from "remark-math";
1374
1475
  import remarkParse from "remark-parse";
1375
1476
  import remarkRehype from "remark-rehype";
1376
1477
  import { unified } from "unified";
1377
- import { matter } from "vfile-matter";
1478
+ import { matter as matter2 } from "vfile-matter";
1378
1479
 
1379
- // src/plugin/plugin_cache.ts
1380
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1480
+ // src/content/content_dependency_tracker.ts
1481
+ import { createHash as createHash2 } from "node:crypto";
1482
+ function createContentDependencyTracker(source) {
1483
+ const dependencies = /* @__PURE__ */ new Map();
1484
+ const record = (identity, content) => {
1485
+ const normalized = normalizeDependencyIdentity(identity);
1486
+ dependencies.set(dependencyKey(normalized), {
1487
+ ...normalized,
1488
+ fingerprint: fingerprintContent(content)
1489
+ });
1490
+ };
1491
+ return {
1492
+ contentSource: {
1493
+ scan: () => source.scan(),
1494
+ async read(entry) {
1495
+ const content = await source.read(entry);
1496
+ record({ kind: "file", id: entry.path }, content);
1497
+ return content;
1498
+ }
1499
+ },
1500
+ async readContent(slug, read) {
1501
+ const content = await read();
1502
+ record({ kind: "content", id: slug }, content);
1503
+ return content;
1504
+ },
1505
+ recordLinkResolution(id, value) {
1506
+ record({ kind: "link", id }, value);
1507
+ },
1508
+ dependencies: () => [...dependencies.values()].toSorted(
1509
+ (left, right) => dependencyKey(left).localeCompare(dependencyKey(right))
1510
+ )
1511
+ };
1512
+ }
1513
+ function fingerprintContent(content) {
1514
+ return `sha256:${createHash2("sha256").update(typeof content === "string" ? content : content).digest("hex")}`;
1515
+ }
1516
+ function normalizeDependencyIdentity(dependency) {
1517
+ return {
1518
+ ...dependency,
1519
+ id: dependency.kind === "file" ? dependency.id.replace(/\\/g, "/") : dependency.id
1520
+ };
1521
+ }
1522
+ function dependencyKey(dependency) {
1523
+ const normalized = normalizeDependencyIdentity(dependency);
1524
+ return `${normalized.kind}:${normalized.id}`;
1525
+ }
1526
+
1527
+ // src/content/content_persistent_cache.ts
1528
+ import { createHash as createHash4 } from "node:crypto";
1381
1529
  import fs2 from "node:fs/promises";
1530
+ import os from "node:os";
1382
1531
  import path2 from "node:path";
1532
+ import { VFile } from "vfile";
1533
+ import { matter } from "vfile-matter";
1534
+
1535
+ // src/plugin/plugin_memo.ts
1536
+ import { createHash as createHash3 } from "node:crypto";
1537
+ function createPluginMemo(cache, options = {}) {
1538
+ return {
1539
+ async memo(key, dependencies, compute) {
1540
+ const memoKey = `${key}:${hash(stableStringify(dependencies))}`;
1541
+ try {
1542
+ const cached = await cache.get(memoKey);
1543
+ if (cached !== void 0) return cached;
1544
+ } catch (error) {
1545
+ warn(
1546
+ options.logger,
1547
+ "Plugin cache read failed; recomputing.",
1548
+ key,
1549
+ error
1550
+ );
1551
+ }
1552
+ const value = await compute();
1553
+ try {
1554
+ await cache.set(memoKey, value);
1555
+ } catch (error) {
1556
+ warn(
1557
+ options.logger,
1558
+ "Plugin cache write failed; result not cached.",
1559
+ key,
1560
+ error
1561
+ );
1562
+ }
1563
+ return value;
1564
+ }
1565
+ };
1566
+ }
1567
+ function stableStringify(value) {
1568
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1569
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
1570
+ const keys = Object.keys(value).sort();
1571
+ const body = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",");
1572
+ return `{${body}}`;
1573
+ }
1574
+ function hash(value) {
1575
+ return createHash3("sha256").update(value).digest("hex");
1576
+ }
1577
+ function warn(logger, message, key, error) {
1578
+ logger?.warn(message, {
1579
+ memoKey: key,
1580
+ error: error instanceof Error ? error.message : String(error)
1581
+ });
1582
+ }
1583
+
1584
+ // src/content/content_persistent_cache.ts
1585
+ var CONTENT_CACHE_NAMESPACE = "content";
1586
+ var CONTENT_CACHE_SCHEMA_VERSION = 3;
1587
+ var CONTENT_CACHE_COMPATIBILITY_VERSION = 1;
1588
+ function hash2(value) {
1589
+ return createHash4("sha256").update(value).digest("hex");
1590
+ }
1591
+ function cacheDirectory(config) {
1592
+ if (config.cache?.directory) {
1593
+ return path2.join(config.cache.directory, CONTENT_CACHE_NAMESPACE);
1594
+ }
1595
+ if (config.buildDirectory) {
1596
+ return path2.join(config.buildDirectory, "cache", CONTENT_CACHE_NAMESPACE);
1597
+ }
1598
+ return path2.join(
1599
+ os.tmpdir(),
1600
+ "riebeckite-content-cache",
1601
+ CONTENT_CACHE_NAMESPACE
1602
+ );
1603
+ }
1604
+ function versionDirectory(baseDir) {
1605
+ return path2.join(baseDir, `v${CONTENT_CACHE_SCHEMA_VERSION}`);
1606
+ }
1607
+ function cacheFilePath(baseDir, key) {
1608
+ return path2.join(versionDirectory(baseDir), `${hash2(key)}.json`);
1609
+ }
1610
+ async function writeAtomically(targetPath, dir, serialized) {
1611
+ await fs2.mkdir(dir, { recursive: true });
1612
+ const temporaryPath = `${targetPath}.${createHash4("sha256").update(`${Date.now()}-${Math.random()}`).digest("hex").slice(0, 12)}.tmp`;
1613
+ try {
1614
+ await fs2.writeFile(temporaryPath, serialized, "utf8");
1615
+ await fs2.rename(temporaryPath, targetPath);
1616
+ } catch (error) {
1617
+ await fs2.rm(temporaryPath, { force: true }).catch(() => void 0);
1618
+ throw error;
1619
+ }
1620
+ }
1621
+ function createPersistentContentCache(options) {
1622
+ const { config, logger, tracer } = options;
1623
+ const baseDir = cacheDirectory(config);
1624
+ const versionDir = versionDirectory(baseDir);
1625
+ return {
1626
+ async get(key) {
1627
+ if (!config.cache?.enabled) return void 0;
1628
+ const filePath = cacheFilePath(baseDir, key);
1629
+ try {
1630
+ const raw = await fs2.readFile(filePath, "utf8");
1631
+ const entry = JSON.parse(raw);
1632
+ if (entry.schemaVersion !== CONTENT_CACHE_SCHEMA_VERSION) {
1633
+ tracer?.event("persistentContentCache.schemaMismatch", { key });
1634
+ return void 0;
1635
+ }
1636
+ if (entry.key !== key) {
1637
+ tracer?.event("persistentContentCache.keyMismatch", { key });
1638
+ return void 0;
1639
+ }
1640
+ if (!isValidCacheEntry(entry)) {
1641
+ tracer?.event("persistentContentCache.invalidShape", { key });
1642
+ return void 0;
1643
+ }
1644
+ tracer?.event("persistentContentCache.found", { key });
1645
+ return entry;
1646
+ } catch (error) {
1647
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1648
+ tracer?.event("persistentContentCache.miss", { key });
1649
+ return void 0;
1650
+ }
1651
+ if (error instanceof SyntaxError) {
1652
+ logger?.warn(
1653
+ "Persistent content cache entry is corrupted and will be ignored.",
1654
+ {
1655
+ key
1656
+ }
1657
+ );
1658
+ return void 0;
1659
+ }
1660
+ throw error;
1661
+ }
1662
+ },
1663
+ async set(key, value) {
1664
+ if (!config.cache?.enabled) return;
1665
+ const filePath = cacheFilePath(baseDir, key);
1666
+ const serialized = JSON.stringify(value);
1667
+ if (serialized === void 0) {
1668
+ throw new TypeError(
1669
+ "Persistent content cache values must be JSON serializable."
1670
+ );
1671
+ }
1672
+ await writeAtomically(filePath, versionDir, serialized);
1673
+ tracer?.event("persistentContentCache.write", { key });
1674
+ },
1675
+ async clear() {
1676
+ try {
1677
+ await fs2.rm(baseDir, { recursive: true, force: true });
1678
+ } catch {
1679
+ }
1680
+ }
1681
+ };
1682
+ }
1683
+ function isValidCacheEntry(entry) {
1684
+ return typeof entry === "object" && entry !== null && typeof entry.key === "string" && typeof entry.schemaVersion === "number" && Array.isArray(entry.dependencies) && entry.dependencies.every(isValidDependency) && typeof entry.value === "object" && entry.value !== null && typeof entry.value.html === "string" && typeof entry.value.frontmatter === "object" && entry.value.frontmatter !== null && !Array.isArray(entry.value.frontmatter);
1685
+ }
1686
+ function isValidDependency(value) {
1687
+ if (typeof value !== "object" || value === null) return false;
1688
+ const candidate = value;
1689
+ return (candidate.kind === "content" || candidate.kind === "file" || candidate.kind === "link") && typeof candidate.id === "string" && typeof candidate.fingerprint === "string";
1690
+ }
1691
+ function computeContentCacheKey(inputs) {
1692
+ const { slug, source, frontmatter, pipelineFingerprint } = inputs;
1693
+ const normalizedFrontmatter = normalizeFrontmatterForCache(frontmatter);
1694
+ const keyData = {
1695
+ schemaVersion: CONTENT_CACHE_SCHEMA_VERSION,
1696
+ slug,
1697
+ source,
1698
+ frontmatter: normalizedFrontmatter,
1699
+ pipelineFingerprint
1700
+ };
1701
+ return hash2(stableStringify(keyData));
1702
+ }
1703
+ function normalizeFrontmatterForCache(frontmatter) {
1704
+ const normalized = {};
1705
+ for (const [key, value] of Object.entries(frontmatter)) {
1706
+ if (value instanceof Date) {
1707
+ normalized[key] = value.toISOString();
1708
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1709
+ normalized[key] = normalizeFrontmatterForCache(
1710
+ value
1711
+ );
1712
+ } else {
1713
+ normalized[key] = value;
1714
+ }
1715
+ }
1716
+ return normalized;
1717
+ }
1718
+ function computePipelineFingerprint(config) {
1719
+ const plugins = config.plugins;
1720
+ const fingerprintData = {
1721
+ compatibilityVersion: CONTENT_CACHE_COMPATIBILITY_VERSION,
1722
+ plugins: plugins.map((p) => pluginFingerprint(p)),
1723
+ markdown: config.markdown,
1724
+ content: {
1725
+ filters: config.content.filters
1726
+ }
1727
+ };
1728
+ return hash2(stableStringify(fingerprintData));
1729
+ }
1730
+ var pluginFingerprints = /* @__PURE__ */ new WeakMap();
1731
+ function pluginFingerprint(plugin) {
1732
+ const cached = pluginFingerprints.get(plugin);
1733
+ if (cached !== void 0) return cached;
1734
+ const fingerprint = sanitizeForFingerprint(plugin);
1735
+ pluginFingerprints.set(plugin, fingerprint);
1736
+ return fingerprint;
1737
+ }
1738
+ var functionSourceFingerprints = /* @__PURE__ */ new Map();
1739
+ function fingerprintFunctionSource(value) {
1740
+ const source = Function.prototype.toString.call(value);
1741
+ const cached = functionSourceFingerprints.get(source);
1742
+ if (cached !== void 0) return cached;
1743
+ const fingerprint = `fn:${hash2(source)}`;
1744
+ functionSourceFingerprints.set(source, fingerprint);
1745
+ return fingerprint;
1746
+ }
1747
+ function sanitizeForFingerprint(value) {
1748
+ if (value === null || value === void 0) return value;
1749
+ if (typeof value === "function") {
1750
+ return fingerprintFunctionSource(value);
1751
+ }
1752
+ if (typeof value === "symbol") return "[Symbol]";
1753
+ if (Array.isArray(value)) return value.map(sanitizeForFingerprint);
1754
+ if (value instanceof Map || value instanceof Set)
1755
+ return `[${value.constructor.name}]`;
1756
+ if (typeof value === "object") {
1757
+ const result = {};
1758
+ for (const [key, val] of Object.entries(value)) {
1759
+ result[key] = sanitizeForFingerprint(val);
1760
+ }
1761
+ return result;
1762
+ }
1763
+ return value;
1764
+ }
1765
+ function isPersistentlyCacheable(_slug, config) {
1766
+ const hasL10n = config.plugins.some(
1767
+ (p) => p.name === "l10n" && p.enabled !== false
1768
+ );
1769
+ if (hasL10n) {
1770
+ return { cacheable: false, reason: "l10n plugin is enabled" };
1771
+ }
1772
+ const unsafePlugin = config.plugins.find((plugin) => {
1773
+ if (plugin.enabled === false) return false;
1774
+ if (!canAffectProcessedContent(plugin)) return false;
1775
+ return plugin.processedContentCache?.dependencyMode !== "none" && plugin.processedContentCache?.dependencyMode !== "tracked";
1776
+ });
1777
+ if (unsafePlugin) {
1778
+ if (unsafePlugin.processedContentCache?.dependencyMode === "unsafe") {
1779
+ return {
1780
+ cacheable: false,
1781
+ reason: `plugin ${unsafePlugin.name} explicitly marks processed content cache as unsafe`
1782
+ };
1783
+ }
1784
+ return {
1785
+ cacheable: false,
1786
+ reason: `plugin ${unsafePlugin.name} can affect processed content without a processedContentCache contract`
1787
+ };
1788
+ }
1789
+ return { cacheable: true };
1790
+ }
1791
+ function canAffectProcessedContent(plugin) {
1792
+ return Boolean(
1793
+ plugin.remarkPlugins?.length || plugin.rehypePlugins?.length || plugin.extendMarkdownPipeline || plugin.extendHtmlPipeline || plugin.renderers?.length || plugin.onContentLoaded || plugin.onPostParsed || plugin.onPostProcessed
1794
+ );
1795
+ }
1796
+ function extractFrontmatter(markdown) {
1797
+ const vfile = new VFile({ value: markdown });
1798
+ matter(vfile);
1799
+ return vfile.data.matter ?? {};
1800
+ }
1801
+
1802
+ // src/plugin/plugin_cache.ts
1803
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
1804
+ import fs3 from "node:fs/promises";
1805
+ import path3 from "node:path";
1383
1806
  function createUnavailablePluginCache() {
1384
1807
  return {
1385
1808
  get: unavailableAtRuntime,
@@ -1389,20 +1812,20 @@ function createUnavailablePluginCache() {
1389
1812
  };
1390
1813
  }
1391
1814
  function createPluginCache(options) {
1392
- const pluginDirectory = path2.join(
1815
+ const pluginDirectory = path3.join(
1393
1816
  options.cacheDirectory,
1394
1817
  pluginNamespace(options.pluginName)
1395
1818
  );
1396
- const versionDirectory = path2.join(
1819
+ const versionDirectory2 = path3.join(
1397
1820
  pluginDirectory,
1398
- hash(options.cacheVersion ?? "1")
1821
+ hash3(options.cacheVersion ?? "1")
1399
1822
  );
1400
1823
  const writes = /* @__PURE__ */ new Map();
1401
1824
  return {
1402
1825
  async get(key) {
1403
1826
  let raw;
1404
1827
  try {
1405
- raw = await fs2.readFile(cachePath(versionDirectory, key), "utf8");
1828
+ raw = await fs3.readFile(cachePath(versionDirectory2, key), "utf8");
1406
1829
  } catch (error) {
1407
1830
  if (isNotFoundError2(error)) {
1408
1831
  options.tracer?.event("cache.miss", { plugin: options.pluginName });
@@ -1425,13 +1848,13 @@ function createPluginCache(options) {
1425
1848
  }
1426
1849
  },
1427
1850
  async set(key, value) {
1428
- const targetPath = cachePath(versionDirectory, key);
1851
+ const targetPath = cachePath(versionDirectory2, key);
1429
1852
  const serialized = JSON.stringify(value);
1430
1853
  if (serialized === void 0) {
1431
1854
  throw new TypeError("Plugin cache values must be JSON serializable.");
1432
1855
  }
1433
1856
  const previousWrite = writes.get(targetPath) ?? Promise.resolve();
1434
- const write = previousWrite.catch(() => void 0).then(() => writeAtomically(targetPath, versionDirectory, serialized));
1857
+ const write = previousWrite.catch(() => void 0).then(() => writeAtomically2(targetPath, versionDirectory2, serialized));
1435
1858
  writes.set(targetPath, write);
1436
1859
  try {
1437
1860
  await write;
@@ -1442,37 +1865,37 @@ function createPluginCache(options) {
1442
1865
  }
1443
1866
  },
1444
1867
  async delete(key) {
1445
- await fs2.rm(cachePath(versionDirectory, key), { force: true });
1868
+ await fs3.rm(cachePath(versionDirectory2, key), { force: true });
1446
1869
  },
1447
1870
  async clear() {
1448
- await fs2.rm(pluginDirectory, { recursive: true, force: true });
1871
+ await fs3.rm(pluginDirectory, { recursive: true, force: true });
1449
1872
  }
1450
1873
  };
1451
1874
  }
1452
- async function writeAtomically(targetPath, versionDirectory, serialized) {
1453
- await fs2.mkdir(versionDirectory, { recursive: true });
1875
+ async function writeAtomically2(targetPath, versionDirectory2, serialized) {
1876
+ await fs3.mkdir(versionDirectory2, { recursive: true });
1454
1877
  const temporaryPath = `${targetPath}.${randomUUID2()}.tmp`;
1455
1878
  try {
1456
- await fs2.writeFile(temporaryPath, serialized, "utf8");
1457
- await fs2.rename(temporaryPath, targetPath);
1879
+ await fs3.writeFile(temporaryPath, serialized, "utf8");
1880
+ await fs3.rename(temporaryPath, targetPath);
1458
1881
  } catch (error) {
1459
- await fs2.rm(temporaryPath, { force: true }).catch(() => void 0);
1882
+ await fs3.rm(temporaryPath, { force: true }).catch(() => void 0);
1460
1883
  throw error;
1461
1884
  }
1462
1885
  }
1463
1886
  function resolvePluginCacheDirectory(config) {
1464
- if (!config) return path2.resolve(process.cwd(), ".riebeckite", "cache");
1465
- if (config.buildDirectory) return path2.join(config.buildDirectory, "cache");
1466
- return path2.resolve(config.content.directory, "..", ".riebeckite", "cache");
1887
+ if (!config) return path3.resolve(process.cwd(), ".riebeckite", "cache");
1888
+ if (config.buildDirectory) return path3.join(config.buildDirectory, "cache");
1889
+ return path3.resolve(config.content.directory, "..", ".riebeckite", "cache");
1467
1890
  }
1468
- function cachePath(versionDirectory, key) {
1469
- return path2.join(versionDirectory, `${hash(key)}.json`);
1891
+ function cachePath(versionDirectory2, key) {
1892
+ return path3.join(versionDirectory2, `${hash3(key)}.json`);
1470
1893
  }
1471
1894
  function pluginNamespace(pluginName) {
1472
- return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash(pluginName);
1895
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash3(pluginName);
1473
1896
  }
1474
- function hash(value) {
1475
- return createHash2("sha256").update(value).digest("hex");
1897
+ function hash3(value) {
1898
+ return createHash5("sha256").update(value).digest("hex");
1476
1899
  }
1477
1900
  function isNotFoundError2(error) {
1478
1901
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -1484,16 +1907,20 @@ async function unavailableAtRuntime() {
1484
1907
  }
1485
1908
 
1486
1909
  // src/types/generated_output.ts
1910
+ var ATTACHMENTS_NAMESPACE = ATTACHMENTS_BASE_PATH.replace(/^\/+/, "");
1911
+ var STATIC_ASSETS_DIR = "assets";
1487
1912
  function createUnavailableGeneratedOutputSink() {
1913
+ const unavailable = () => {
1914
+ throw new Error(
1915
+ "Generated output is available only during a build lifecycle."
1916
+ );
1917
+ };
1488
1918
  return {
1489
- emit() {
1490
- throw new Error(
1491
- "Generated output is available only during a build lifecycle."
1492
- );
1493
- }
1919
+ emit: unavailable,
1920
+ emitAsset: unavailable
1494
1921
  };
1495
1922
  }
1496
- function normalizeGeneratedOutputPath(input) {
1923
+ function normalizeOutputPathSegments(input) {
1497
1924
  if (typeof input !== "string" || input.length === 0) {
1498
1925
  throw new Error("Generated output path must be a non-empty string.");
1499
1926
  }
@@ -1517,12 +1944,27 @@ function normalizeGeneratedOutputPath(input) {
1517
1944
  );
1518
1945
  }
1519
1946
  }
1520
- const normalized = segments.join("/");
1521
- if (normalized === "assets" || normalized.startsWith("assets/")) {
1947
+ return segments.join("/");
1948
+ }
1949
+ function normalizeGeneratedOutputPath(input) {
1950
+ const normalized = normalizeOutputPathSegments(input);
1951
+ if (normalized === STATIC_ASSETS_DIR || normalized.startsWith(`${STATIC_ASSETS_DIR}/`)) {
1522
1952
  throw new Error(
1523
1953
  `Generated output path must not conflict with the static assets namespace: "${input}".`
1524
1954
  );
1525
1955
  }
1956
+ return rejectInternalRiebeckitePath(normalized, input);
1957
+ }
1958
+ function normalizeContentAssetOutputPath(input) {
1959
+ const normalized = normalizeOutputPathSegments(input);
1960
+ if (normalized === ATTACHMENTS_NAMESPACE || normalized.startsWith(`${ATTACHMENTS_NAMESPACE}/`)) {
1961
+ throw new Error(
1962
+ `Generated output path must not conflict with the attachment namespace: "${input}".`
1963
+ );
1964
+ }
1965
+ return rejectInternalRiebeckitePath(normalized, input);
1966
+ }
1967
+ function rejectInternalRiebeckitePath(normalized, input) {
1526
1968
  if (normalized === ".riebeckite" || normalized.startsWith(".riebeckite/")) {
1527
1969
  throw new Error(
1528
1970
  `Generated output path must not target internal Riebeckite files: "${input}".`
@@ -1539,6 +1981,16 @@ var Pipeline = class {
1539
1981
  this.getMarkdownBySlug = getMarkdownBySlug;
1540
1982
  this.options = options;
1541
1983
  this.isBuildTime = isBuildTime;
1984
+ if (this.options.config && this.options.config.cache?.enabled !== false) {
1985
+ this.persistentCache = createPersistentContentCache({
1986
+ config: this.options.config,
1987
+ logger: this.options.observability?.logger,
1988
+ tracer: this.options.observability?.tracer
1989
+ });
1990
+ this.pipelineFingerprint = computePipelineFingerprint(
1991
+ this.options.config
1992
+ );
1993
+ }
1542
1994
  }
1543
1995
  contentIndex;
1544
1996
  permalinks;
@@ -1546,23 +1998,106 @@ var Pipeline = class {
1546
1998
  options;
1547
1999
  isBuildTime;
1548
2000
  pluginCaches = /* @__PURE__ */ new Map();
2001
+ persistentCache = null;
2002
+ pipelineFingerprint = null;
2003
+ resolvedPlugins = null;
1549
2004
  async execute(markDownContent, context = {}) {
1550
2005
  return await this.executeWithEmbedState(
1551
2006
  markDownContent,
1552
2007
  context,
1553
2008
  0,
1554
- new Set(context.sourceSlug ? [context.sourceSlug] : [])
2009
+ new Set(context.sourceSlug ? [context.sourceSlug] : []),
2010
+ this.options.contentSource ? createContentDependencyTracker(this.options.contentSource) : null
1555
2011
  );
1556
2012
  }
1557
- async executeWithEmbedState(markDownContent, context, embedDepth, embedTrail) {
2013
+ async executeWithEmbedState(markDownContent, context, embedDepth, embedTrail, rootDependencyTracker) {
2014
+ const dependencyTracker = rootDependencyTracker;
2015
+ const effectiveContentSource = dependencyTracker ? dependencyTracker.contentSource : this.options.contentSource;
2016
+ const sourceSlug = context.sourceSlug;
2017
+ if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && this.options.contentSource && this.options.config) {
2018
+ const cacheable = isPersistentlyCacheable(
2019
+ sourceSlug,
2020
+ this.options.config
2021
+ );
2022
+ if (cacheable.cacheable) {
2023
+ const frontmatter = extractFrontmatter(markDownContent);
2024
+ const cacheKey = computeContentCacheKey({
2025
+ slug: sourceSlug,
2026
+ source: markDownContent,
2027
+ frontmatter,
2028
+ pipelineFingerprint: this.pipelineFingerprint
2029
+ });
2030
+ const cachedEntry = await this.persistentCache.get(cacheKey);
2031
+ if (cachedEntry) {
2032
+ const validationTracker = createContentDependencyTracker(
2033
+ this.options.contentSource
2034
+ );
2035
+ let valid = true;
2036
+ for (const dep of cachedEntry.dependencies) {
2037
+ if (dep.kind === "content") {
2038
+ const content = await validationTracker.readContent(
2039
+ dep.id,
2040
+ async () => await this.getMarkdownBySlug?.(dep.id) ?? ""
2041
+ );
2042
+ if (fingerprintContent(content) !== dep.fingerprint) {
2043
+ valid = false;
2044
+ break;
2045
+ }
2046
+ } else if (dep.kind === "file") {
2047
+ const fileContent = await validationTracker.contentSource.read({
2048
+ path: dep.id
2049
+ });
2050
+ if (fingerprintContent(fileContent) !== dep.fingerprint) {
2051
+ valid = false;
2052
+ break;
2053
+ }
2054
+ } else if (dep.kind === "link") {
2055
+ if (fingerprintContent(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2056
+ valid = false;
2057
+ break;
2058
+ }
2059
+ }
2060
+ }
2061
+ if (valid) {
2062
+ this.reportContentDependencies(
2063
+ sourceSlug,
2064
+ cachedEntry.dependencies
2065
+ );
2066
+ this.options.observability?.tracer?.event(
2067
+ "persistentContentCache.hit",
2068
+ { key: cacheKey, slug: sourceSlug }
2069
+ );
2070
+ this.options.onPersistentContentCacheResult?.("hit");
2071
+ return {
2072
+ frontmatter: cachedEntry.value.frontmatter,
2073
+ html: cachedEntry.value.html
2074
+ };
2075
+ }
2076
+ }
2077
+ this.options.observability?.tracer?.event(
2078
+ "persistentContentCache.miss",
2079
+ { key: cacheKey, slug: sourceSlug }
2080
+ );
2081
+ this.options.onPersistentContentCacheResult?.("miss");
2082
+ } else {
2083
+ this.options.observability?.tracer?.event(
2084
+ "persistentContentCache.bypass",
2085
+ {
2086
+ slug: sourceSlug,
2087
+ ...cacheable.reason ? { reason: cacheable.reason } : {}
2088
+ }
2089
+ );
2090
+ this.options.onPersistentContentCacheResult?.("bypass");
2091
+ }
2092
+ }
1558
2093
  const processor = unified();
1559
- const plugins = resolvePlugins(this.options.plugins);
2094
+ const plugins = this.resolvePlugins();
1560
2095
  this.use(processor, remarkParse);
1561
2096
  this.use(processor, remarkDirective);
1562
2097
  this.use(processor, remarkFrontmatter, ["yaml", "toml"]);
1563
2098
  this.use(processor, function loadFrontmatter() {
1564
2099
  return (_tree, file2) => {
1565
- matter(file2);
2100
+ matter2(file2);
1566
2101
  };
1567
2102
  });
1568
2103
  this.use(processor, remarkMath);
@@ -1571,9 +2106,14 @@ var Pipeline = class {
1571
2106
  sourceSlug: context.sourceSlug,
1572
2107
  contentIndex: this.contentIndex,
1573
2108
  resolvePermalink: (slug) => this.getPermalink(slug),
1574
- renderNoteEmbed: this.createNoteEmbedRenderer(embedDepth, embedTrail),
1575
- renderContent: this.createContentRenderer(),
1576
- contentSource: this.options.contentSource
2109
+ isRoutable: this.options.isRoutable,
2110
+ renderNoteEmbed: this.createNoteEmbedRenderer(
2111
+ embedDepth,
2112
+ embedTrail,
2113
+ dependencyTracker
2114
+ ),
2115
+ renderContent: this.createContentRenderer(effectiveContentSource),
2116
+ contentSource: effectiveContentSource
1577
2117
  };
1578
2118
  for (const plugin of plugins) {
1579
2119
  for (const remarkPlugin of plugin.remarkPlugins ?? []) {
@@ -1588,7 +2128,10 @@ var Pipeline = class {
1588
2128
  markdownPipelineContext
1589
2129
  );
1590
2130
  }
1591
- this.use(processor, normalizeMarkdownLinks, markdownPipelineContext);
2131
+ this.use(processor, normalizeMarkdownLinks, {
2132
+ ...markdownPipelineContext,
2133
+ recordLinkResolution: dependencyTracker ? (id, value) => dependencyTracker.recordLinkResolution(id, value) : void 0
2134
+ });
1592
2135
  this.use(processor, remarkRehype, { allowDangerousHtml: true });
1593
2136
  this.use(processor, rehypeRaw);
1594
2137
  for (const plugin of plugins) {
@@ -1606,10 +2149,43 @@ var Pipeline = class {
1606
2149
  this.use(processor, rehypeFormat);
1607
2150
  this.use(processor, rehypeStringify, { allowDangerousHtml: true });
1608
2151
  const file = await processor.process(markDownContent.trim());
1609
- return {
2152
+ const result = {
1610
2153
  frontmatter: file.data.matter || {},
1611
2154
  html: String(file.value)
1612
2155
  };
2156
+ if (sourceSlug && dependencyTracker) {
2157
+ this.reportContentDependencies(
2158
+ sourceSlug,
2159
+ dependencyTracker.dependencies()
2160
+ );
2161
+ }
2162
+ if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && dependencyTracker && this.options.config) {
2163
+ const cacheable = isPersistentlyCacheable(
2164
+ sourceSlug,
2165
+ this.options.config
2166
+ );
2167
+ if (cacheable.cacheable) {
2168
+ const frontmatter = extractFrontmatter(markDownContent);
2169
+ const cacheKey = computeContentCacheKey({
2170
+ slug: sourceSlug,
2171
+ source: markDownContent,
2172
+ frontmatter,
2173
+ pipelineFingerprint: this.pipelineFingerprint
2174
+ });
2175
+ const dependencies = dependencyTracker.dependencies();
2176
+ const entry = {
2177
+ schemaVersion: CONTENT_CACHE_SCHEMA_VERSION,
2178
+ key: cacheKey,
2179
+ dependencies,
2180
+ value: result
2181
+ };
2182
+ await this.persistentCache.set(cacheKey, entry);
2183
+ }
2184
+ }
2185
+ return result;
2186
+ }
2187
+ reportContentDependencies(slug, dependencies) {
2188
+ this.options.onContentDependencies?.(slug, dependencies);
1613
2189
  }
1614
2190
  getPermalink(slug) {
1615
2191
  const permalink = this.permalinks.get(slug);
@@ -1618,6 +2194,14 @@ var Pipeline = class {
1618
2194
  }
1619
2195
  return permalink;
1620
2196
  }
2197
+ resolveLinkDependency(id) {
2198
+ return resolveLinkDependencyValue(
2199
+ id,
2200
+ this.contentIndex,
2201
+ (slug) => this.permalinks.get(slug),
2202
+ this.options.isRoutable
2203
+ );
2204
+ }
1621
2205
  use(processor, plugin, options) {
1622
2206
  const register = processor.use.bind(processor);
1623
2207
  if (options === void 0) {
@@ -1626,26 +2210,37 @@ var Pipeline = class {
1626
2210
  register(plugin, options);
1627
2211
  }
1628
2212
  }
1629
- createNoteEmbedRenderer(embedDepth, embedTrail) {
2213
+ createNoteEmbedRenderer(embedDepth, embedTrail, dependencyTracker) {
1630
2214
  if (!this.getMarkdownBySlug || embedDepth >= 3) return void 0;
1631
2215
  return async (slug, fragment) => {
2216
+ if (this.options.isRoutable && !this.options.isRoutable(slug)) {
2217
+ return null;
2218
+ }
1632
2219
  if (embedTrail.has(slug)) return null;
1633
2220
  const sourceMarkdown = await this.getMarkdownBySlug?.(slug);
1634
2221
  const markdown = sourceMarkdown ? selectEmbedMarkdownFragment(sourceMarkdown, fragment) : null;
1635
2222
  if (!markdown) return null;
1636
2223
  const nextEmbedTrail = new Set(embedTrail);
1637
2224
  nextEmbedTrail.add(slug);
2225
+ if (dependencyTracker) {
2226
+ await dependencyTracker.readContent(slug, async () => markdown);
2227
+ }
1638
2228
  const content = await this.executeWithEmbedState(
1639
2229
  markdown,
1640
2230
  { sourceSlug: slug },
1641
2231
  embedDepth + 1,
1642
- nextEmbedTrail
2232
+ nextEmbedTrail,
2233
+ dependencyTracker
1643
2234
  );
1644
2235
  return content.html;
1645
2236
  };
1646
2237
  }
1647
- createContentRenderer() {
1648
- const renderers = resolvePlugins(this.options.plugins).flatMap(
2238
+ resolvePlugins() {
2239
+ this.resolvedPlugins ??= resolvePlugins(this.options.plugins);
2240
+ return this.resolvedPlugins;
2241
+ }
2242
+ createContentRenderer(contentSource) {
2243
+ const renderers = this.resolvePlugins().flatMap(
1649
2244
  (plugin) => (plugin.renderers ?? []).map((renderer) => ({ plugin, renderer }))
1650
2245
  );
1651
2246
  if (renderers.length === 0) return void 0;
@@ -1667,7 +2262,7 @@ var Pipeline = class {
1667
2262
  output: createUnavailableGeneratedOutputSink(),
1668
2263
  logger: observability.logger.child({ plugin: plugin.name }),
1669
2264
  tracer: observability.tracer,
1670
- contentSource: this.options.contentSource,
2265
+ contentSource,
1671
2266
  ...input
1672
2267
  })
1673
2268
  );
@@ -1710,6 +2305,21 @@ function normalizeMarkdownLinks(context) {
1710
2305
  });
1711
2306
  };
1712
2307
  }
2308
+ var LINK_INDEX_PREFIX = "index:";
2309
+ var LINK_LOCATION_PREFIX = "location:";
2310
+ function resolveLinkDependencyValue(id, contentIndex, lookupPermalink, isRoutable) {
2311
+ if (id.startsWith(LINK_INDEX_PREFIX)) {
2312
+ return contentIndex.get(id.slice(LINK_INDEX_PREFIX.length)) ?? "";
2313
+ }
2314
+ if (id.startsWith(LINK_LOCATION_PREFIX)) {
2315
+ const slug = id.slice(LINK_LOCATION_PREFIX.length);
2316
+ const permalink = lookupPermalink(slug);
2317
+ if (permalink === void 0) return "";
2318
+ if (isRoutable && !isRoutable(slug)) return "";
2319
+ return permalink;
2320
+ }
2321
+ return "";
2322
+ }
1713
2323
  function visitMarkdownLinkNodes(node, visitor) {
1714
2324
  const linkNode = node;
1715
2325
  if ((node.type === "link" || node.type === "definition") && typeof linkNode.url === "string") {
@@ -1722,7 +2332,7 @@ function visitMarkdownLinkNodes(node, visitor) {
1722
2332
  function normalizeMarkdownLinkUrl(url, context) {
1723
2333
  if (!context.sourceSlug) return null;
1724
2334
  if (!isMarkdownContentLink(url)) return null;
1725
- const [pathAndQuery, hash3 = ""] = splitOnce(url, "#");
2335
+ const [pathAndQuery, hash4 = ""] = splitOnce(url, "#");
1726
2336
  const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
1727
2337
  const decodedPath = decodeUriPath(rawPath);
1728
2338
  if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
@@ -1733,15 +2343,35 @@ function normalizeMarkdownLinkUrl(url, context) {
1733
2343
  if (!resolvedSlug) return null;
1734
2344
  const indexedSlug = getExactContentSlug(context, resolvedSlug);
1735
2345
  if (!indexedSlug) return null;
1736
- const suffix = `${query ? `?${query}` : ""}${hash3 ? `#${hash3}` : ""}`;
2346
+ recordLinkResolution(context, LINK_LOCATION_PREFIX, indexedSlug);
2347
+ if (context.isRoutable && !context.isRoutable(indexedSlug)) return null;
2348
+ const suffix = `${query ? `?${query}` : ""}${hash4 ? `#${hash4}` : ""}`;
1737
2349
  return `${context.resolvePermalink(indexedSlug)}${suffix}`;
1738
2350
  }
2351
+ function recordLinkResolution(context, prefix, key) {
2352
+ if (!context.recordLinkResolution) return;
2353
+ const value = prefix === LINK_INDEX_PREFIX ? context.contentIndex.get(key) ?? "" : resolveLinkDependencyValue(
2354
+ `${prefix}${key}`,
2355
+ context.contentIndex,
2356
+ (slug) => {
2357
+ try {
2358
+ return context.resolvePermalink(slug);
2359
+ } catch {
2360
+ return void 0;
2361
+ }
2362
+ },
2363
+ context.isRoutable
2364
+ );
2365
+ context.recordLinkResolution(`${prefix}${key}`, value);
2366
+ }
1739
2367
  function getExactContentSlug(context, slug) {
1740
2368
  try {
1741
2369
  context.resolvePermalink(slug);
1742
2370
  return slug;
1743
2371
  } catch {
1744
- return context.contentIndex.get(slug.toLowerCase()) ?? null;
2372
+ const indexKey = slug.toLowerCase();
2373
+ recordLinkResolution(context, LINK_INDEX_PREFIX, indexKey);
2374
+ return context.contentIndex.get(indexKey) ?? null;
1745
2375
  }
1746
2376
  }
1747
2377
  function isMarkdownContentLink(url) {
@@ -1765,9 +2395,9 @@ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
1765
2395
  }
1766
2396
  return normalizedParts.join("/").replace(/\.md$/i, "");
1767
2397
  }
1768
- function decodeUriPath(path4) {
2398
+ function decodeUriPath(path5) {
1769
2399
  try {
1770
- return decodeURI(path4);
2400
+ return decodeURI(path5);
1771
2401
  } catch {
1772
2402
  return null;
1773
2403
  }
@@ -1855,17 +2485,27 @@ var GeneratedOutputRegistry = class {
1855
2485
  outputs = [];
1856
2486
  owners = /* @__PURE__ */ new Map();
1857
2487
  sinkFor(pluginName) {
1858
- return {
1859
- emit: (output) => {
1860
- const path4 = normalizeGeneratedOutputPath(output.path);
1861
- const owner = this.owners.get(path4);
1862
- if (owner) {
1863
- throw new Error(
1864
- `Duplicate generated output path "${path4}" declared by "${owner}" and "${pluginName}".`
1865
- );
1866
- }
1867
- this.owners.set(path4, pluginName);
1868
- this.outputs.push({ path: path4, content: output.content, owner: pluginName });
2488
+ const register = (path5, output) => {
2489
+ const owner = this.owners.get(path5);
2490
+ if (owner) {
2491
+ throw new Error(
2492
+ `Duplicate generated output path "${path5}" declared by "${owner}" and "${pluginName}".`
2493
+ );
2494
+ }
2495
+ this.owners.set(path5, pluginName);
2496
+ this.outputs.push({
2497
+ path: path5,
2498
+ content: output.content,
2499
+ dependencies: output.dependencies,
2500
+ owner: pluginName
2501
+ });
2502
+ };
2503
+ return {
2504
+ emit: (output) => {
2505
+ register(normalizeGeneratedOutputPath(output.path), output);
2506
+ },
2507
+ emitAsset: (output) => {
2508
+ register(normalizeContentAssetOutputPath(output.path), output);
1869
2509
  }
1870
2510
  };
1871
2511
  }
@@ -1948,6 +2588,7 @@ var PluginRuntime = class {
1948
2588
  pluginCaches = /* @__PURE__ */ new Map();
1949
2589
  generatedOutputs = new GeneratedOutputRegistry();
1950
2590
  isBuildTime = false;
2591
+ resolvedPlugins = null;
1951
2592
  getDiagnostics() {
1952
2593
  return this.diagnostics;
1953
2594
  }
@@ -2068,20 +2709,26 @@ var PluginRuntime = class {
2068
2709
  }
2069
2710
  async resolvePage(pathname, manifest, contentIndex) {
2070
2711
  const matches = [];
2071
- const normalizedPathname = normalizePagePath(pathname);
2072
- const context = {
2073
- ...this.createContext(contentIndex),
2074
- manifest,
2075
- pathname: normalizedPathname
2076
- };
2712
+ const requestedPathname = normalizePagePath(pathname);
2077
2713
  for (const plugin of this.plugins()) {
2078
2714
  for (const pageType of plugin.pageTypes ?? []) {
2715
+ const pagePathname = normalizePageTypePath(pathname, pageType);
2716
+ const context = {
2717
+ ...this.createContext(contentIndex),
2718
+ manifest,
2719
+ pathname: pagePathname
2720
+ };
2079
2721
  const page = await pageType.resolve(
2080
2722
  this.createPluginContext(plugin, context)
2081
2723
  );
2082
2724
  if (page) {
2083
2725
  matches.push({
2084
- page: { ...page, type: pageType.id, pluginName: plugin.name },
2726
+ page: {
2727
+ ...page,
2728
+ pathname: pageType.directoryIndex ? normalizeDirectoryPagePath(page.pathname) : page.pathname,
2729
+ type: pageType.id,
2730
+ pluginName: plugin.name
2731
+ },
2085
2732
  priority: pageType.priority ?? 0
2086
2733
  });
2087
2734
  }
@@ -2094,7 +2741,7 @@ var PluginRuntime = class {
2094
2741
  );
2095
2742
  if (highest.length !== 1) {
2096
2743
  throw new Error(
2097
- `Multiple plugin page types match ${normalizedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
2744
+ `Multiple plugin page types match ${requestedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
2098
2745
  );
2099
2746
  }
2100
2747
  return highest[0].page;
@@ -2106,12 +2753,60 @@ var PluginRuntime = class {
2106
2753
  for (const pageType of plugin.pageTypes ?? []) {
2107
2754
  const declared = pageType.paths;
2108
2755
  if (!declared) continue;
2109
- paths.push(
2110
- ...typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared
2111
- );
2756
+ const resolved = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2757
+ for (const rawPath of resolved) {
2758
+ paths.push(normalizePageTypePath(rawPath, pageType));
2759
+ }
2760
+ }
2761
+ }
2762
+ return [...new Set(paths)];
2763
+ }
2764
+ async getPageRoutes(manifest, contentIndex) {
2765
+ const routes = [];
2766
+ const context = { ...this.createContext(contentIndex), manifest };
2767
+ for (const plugin of this.plugins()) {
2768
+ for (const pageType of plugin.pageTypes ?? []) {
2769
+ const declared = pageType.paths;
2770
+ if (!declared) continue;
2771
+ const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2772
+ for (const pathname of paths) {
2773
+ routes.push({
2774
+ pathname: normalizePageTypePath(pathname, pageType),
2775
+ pluginName: plugin.name,
2776
+ pageType: pageType.id
2777
+ });
2778
+ }
2112
2779
  }
2113
2780
  }
2114
- return [...new Set(paths.map(normalizePagePath))];
2781
+ return routes;
2782
+ }
2783
+ async getPageOutputs(manifest, contentIndex) {
2784
+ const outputs = [];
2785
+ const context = { ...this.createContext(contentIndex), manifest };
2786
+ for (const plugin of this.plugins()) {
2787
+ for (const pageType of plugin.pageTypes ?? []) {
2788
+ const declared = pageType.paths;
2789
+ if (!declared) continue;
2790
+ const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2791
+ for (const rawPath of paths) {
2792
+ const pathname = normalizePageTypePath(rawPath, pageType);
2793
+ const dependencyContext = { ...context, pathname };
2794
+ const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
2795
+ this.createPluginContext(plugin, dependencyContext)
2796
+ ) : pageType.outputDependencies : [{ type: "unknown" }];
2797
+ outputs.push({
2798
+ kind: "plugin-page",
2799
+ path: pathname,
2800
+ producer: `plugin:${plugin.name}:page:${pageType.id}`,
2801
+ dependencies
2802
+ });
2803
+ }
2804
+ }
2805
+ }
2806
+ return uniqueOutputs(outputs);
2807
+ }
2808
+ getContentOutputDependencies() {
2809
+ return this.plugins().flatMap((plugin) => plugin.outputDependencies ?? []);
2115
2810
  }
2116
2811
  async collectDiagnostics(contentIndex) {
2117
2812
  const tracer = this.observability().tracer;
@@ -2169,6 +2864,7 @@ var PluginRuntime = class {
2169
2864
  return {
2170
2865
  config: this.pipelineOptions.config,
2171
2866
  contentIndex,
2867
+ contentSource: this.pipelineOptions.contentSource,
2172
2868
  diagnostics: this.diagnostics
2173
2869
  };
2174
2870
  }
@@ -2218,27 +2914,33 @@ var PluginRuntime = class {
2218
2914
  return this.pipelineOptions.observability ?? noopObservability;
2219
2915
  }
2220
2916
  plugins() {
2221
- const plugins = resolvePlugins(this.pipelineOptions.plugins);
2222
- const pageTypes = /* @__PURE__ */ new Map();
2223
- for (const plugin of plugins) {
2224
- const declaredPageTypes = plugin.pageTypes;
2225
- if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
2226
- throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
2227
- }
2228
- for (const pageType of declaredPageTypes ?? []) {
2229
- validatePageType(plugin.name, pageType);
2230
- const previous = pageTypes.get(pageType.id);
2231
- if (previous) {
2232
- throw new Error(
2233
- `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
2234
- );
2235
- }
2236
- pageTypes.set(pageType.id, plugin.name);
2237
- }
2917
+ if (this.resolvedPlugins === null) {
2918
+ const resolved = resolvePlugins(this.pipelineOptions.plugins);
2919
+ collectPageTypeOwners(resolved);
2920
+ this.resolvedPlugins = resolved;
2238
2921
  }
2239
- return plugins;
2922
+ return this.resolvedPlugins;
2240
2923
  }
2241
2924
  };
2925
+ function collectPageTypeOwners(plugins) {
2926
+ const pageTypes = /* @__PURE__ */ new Map();
2927
+ for (const plugin of plugins) {
2928
+ const declaredPageTypes = plugin.pageTypes;
2929
+ if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
2930
+ throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
2931
+ }
2932
+ for (const pageType of declaredPageTypes ?? []) {
2933
+ validatePageType(plugin.name, pageType);
2934
+ const previous = pageTypes.get(pageType.id);
2935
+ if (previous) {
2936
+ throw new Error(
2937
+ `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
2938
+ );
2939
+ }
2940
+ pageTypes.set(pageType.id, plugin.name);
2941
+ }
2942
+ }
2943
+ }
2242
2944
  function validatePageType(pluginName, pageType) {
2243
2945
  if (typeof pageType !== "object" || pageType === null) {
2244
2946
  throw new TypeError(`Plugin ${pluginName} pageTypes must contain objects`);
@@ -2264,24 +2966,48 @@ function validatePageType(pluginName, pageType) {
2264
2966
  `Plugin ${pluginName} page type "${candidate.id}" priority must be a finite number`
2265
2967
  );
2266
2968
  }
2969
+ if (candidate.outputDependencies !== void 0 && !Array.isArray(candidate.outputDependencies) && typeof candidate.outputDependencies !== "function") {
2970
+ throw new TypeError(
2971
+ `Plugin ${pluginName} page type "${candidate.id}" outputDependencies must be an array or function`
2972
+ );
2973
+ }
2974
+ if (candidate.directoryIndex !== void 0 && typeof candidate.directoryIndex !== "boolean") {
2975
+ throw new TypeError(
2976
+ `Plugin ${pluginName} page type "${candidate.id}" directoryIndex must be a boolean`
2977
+ );
2978
+ }
2979
+ }
2980
+ function normalizePageTypePath(pathname, pageType) {
2981
+ return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
2982
+ }
2983
+ function normalizeDirectoryPagePath(pathname) {
2984
+ const path5 = normalizePagePath(pathname);
2985
+ return path5 === "/" ? path5 : `${path5}/`;
2267
2986
  }
2268
2987
  function normalizePagePath(pathname) {
2269
- const path4 = `/${pathname.split("/").filter(Boolean).join("/")}`;
2270
- return path4 === "/" ? path4 : path4.replace(/\/$/, "");
2988
+ const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
2989
+ return path5 === "/" ? path5 : path5.replace(/\/$/, "");
2271
2990
  }
2272
2991
  function countSeverity(diagnostics, severity) {
2273
2992
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
2274
2993
  }
2994
+ function uniqueOutputs(outputs) {
2995
+ const byPath = /* @__PURE__ */ new Map();
2996
+ for (const output of outputs) byPath.set(output.path, output);
2997
+ return [...byPath.values()].sort(
2998
+ (left, right) => left.path.localeCompare(right.path)
2999
+ );
3000
+ }
2275
3001
 
2276
3002
  // src/content/affected_content.ts
2277
- function determineAffectedContent(changeSet, previousState, currentPaths) {
3003
+ function determineAffectedContent(changeSet, previousState, currentPaths, currentContentIndex) {
2278
3004
  const changedPaths = [
2279
3005
  ...changeSet.added,
2280
3006
  ...changeSet.changed,
2281
3007
  ...changeSet.removed
2282
3008
  ];
2283
3009
  const direct = new Set(
2284
- changedPaths.filter(isMarkdownPath2).map(toDependencyKey)
3010
+ changedPaths.filter(isMarkdownPath2).map(toDependencyId)
2285
3011
  );
2286
3012
  const dependent = /* @__PURE__ */ new Set();
2287
3013
  if (!previousState) {
@@ -2289,40 +3015,81 @@ function determineAffectedContent(changeSet, previousState, currentPaths) {
2289
3015
  return { direct, dependent };
2290
3016
  }
2291
3017
  if (changeSet.added.length > 0 || changeSet.removed.length > 0) {
2292
- addAllNotes(dependent, currentPaths, direct);
3018
+ const changedIndexKeys = findChangedIndexKeys(
3019
+ previousState.contentIndex,
3020
+ currentContentIndex
3021
+ );
3022
+ for (const [path5, entry] of Object.entries(previousState.entries)) {
3023
+ if (!isMarkdownPath2(path5)) continue;
3024
+ const slug = toDependencyId(path5);
3025
+ if (direct.has(slug) || dependent.has(slug)) continue;
3026
+ if (!entry.linkTargets?.some((target) => changedIndexKeys.has(target))) {
3027
+ continue;
3028
+ }
3029
+ dependent.add(slug);
3030
+ }
2293
3031
  }
2294
- const pending = changedPaths.map(toDependencyKey);
2295
- const seen = new Set(pending);
3032
+ const dependentsByDependency = buildDependentsByDependency(previousState);
3033
+ const pending = changedPaths.map(toDependency);
3034
+ const seen = new Set(pending.map(dependencyKey2));
2296
3035
  while (pending.length > 0) {
2297
3036
  const changed = pending.pop();
2298
3037
  if (changed === void 0) continue;
2299
- for (const [path4, entry] of Object.entries(previousState.entries)) {
2300
- if (!isMarkdownPath2(path4)) continue;
2301
- const slug = toDependencyKey(path4);
3038
+ for (const slug of dependentsByDependency.get(dependencyKey2(changed)) ?? []) {
2302
3039
  if (direct.has(slug) || dependent.has(slug)) continue;
2303
- if (!entry.dependencies.includes(changed)) continue;
2304
3040
  dependent.add(slug);
2305
- if (!seen.has(slug)) {
2306
- seen.add(slug);
2307
- pending.push(slug);
3041
+ const dependency = { kind: "content", id: slug };
3042
+ if (!seen.has(dependencyKey2(dependency))) {
3043
+ seen.add(dependencyKey2(dependency));
3044
+ pending.push(dependency);
2308
3045
  }
2309
3046
  }
2310
3047
  }
2311
3048
  return { direct, dependent };
2312
3049
  }
3050
+ function findChangedIndexKeys(previousIndex, currentIndex) {
3051
+ const changed = /* @__PURE__ */ new Set();
3052
+ for (const [key, previousValue] of Object.entries(previousIndex)) {
3053
+ if (currentIndex.get(key) !== previousValue) changed.add(key);
3054
+ }
3055
+ for (const key of currentIndex.keys()) {
3056
+ if (!(key in previousIndex)) changed.add(key);
3057
+ }
3058
+ return changed;
3059
+ }
2313
3060
  function addAllNotes(target, currentPaths, exclude) {
2314
- for (const path4 of currentPaths) {
2315
- if (!isMarkdownPath2(path4)) continue;
2316
- const slug = toDependencyKey(path4);
3061
+ for (const path5 of currentPaths) {
3062
+ if (!isMarkdownPath2(path5)) continue;
3063
+ const slug = toDependencyId(path5);
2317
3064
  if (exclude?.has(slug)) continue;
2318
3065
  target.add(slug);
2319
3066
  }
2320
3067
  }
2321
- function isMarkdownPath2(path4) {
2322
- return path4.endsWith(".md");
3068
+ function isMarkdownPath2(path5) {
3069
+ return path5.endsWith(".md");
3070
+ }
3071
+ function toDependencyId(path5) {
3072
+ return path5.replace(/\.md$/, "");
3073
+ }
3074
+ function toDependency(path5) {
3075
+ return isMarkdownPath2(path5) ? { kind: "content", id: toDependencyId(path5) } : { kind: "file", id: path5.replace(/\\/g, "/") };
3076
+ }
3077
+ function buildDependentsByDependency(state2) {
3078
+ const dependents = /* @__PURE__ */ new Map();
3079
+ for (const [path5, entry] of Object.entries(state2.entries)) {
3080
+ if (!isMarkdownPath2(path5)) continue;
3081
+ const slug = toDependencyId(path5);
3082
+ for (const dependency of entry.dependencies) {
3083
+ const key = dependencyKey2(dependency);
3084
+ const values = dependents.get(key) ?? /* @__PURE__ */ new Set();
3085
+ values.add(slug);
3086
+ dependents.set(key, values);
3087
+ }
3088
+ }
3089
+ return dependents;
2323
3090
  }
2324
- function toDependencyKey(path4) {
2325
- return path4.replace(/\.md$/, "");
3091
+ function dependencyKey2(dependency) {
3092
+ return `${dependency.kind}:${dependency.id}`;
2326
3093
  }
2327
3094
 
2328
3095
  // src/content/content_change_set.ts
@@ -2333,18 +3100,18 @@ function diffContentEntries(previous, current) {
2333
3100
  const added = [];
2334
3101
  const changed = [];
2335
3102
  const unchanged = [];
2336
- for (const [path4, fingerprint] of currentFingerprints) {
2337
- const previousEntry = previous.entries[path4];
3103
+ for (const [path5, fingerprint] of currentFingerprints) {
3104
+ const previousEntry = previous.entries[path5];
2338
3105
  if (!previousEntry) {
2339
- added.push(path4);
3106
+ added.push(path5);
2340
3107
  } else if (previousEntry.fingerprint !== fingerprint) {
2341
- changed.push(path4);
3108
+ changed.push(path5);
2342
3109
  } else {
2343
- unchanged.push(path4);
3110
+ unchanged.push(path5);
2344
3111
  }
2345
3112
  }
2346
3113
  const removed = Object.keys(previous.entries).filter(
2347
- (path4) => !currentFingerprints.has(path4)
3114
+ (path5) => !currentFingerprints.has(path5)
2348
3115
  );
2349
3116
  return sortChangeSet({ added, changed, removed, unchanged });
2350
3117
  }
@@ -2368,119 +3135,6 @@ function sortChangeSet(changeSet) {
2368
3135
  };
2369
3136
  }
2370
3137
 
2371
- // src/content/content_build_coordinator.ts
2372
- var ContentBuildCoordinator = class {
2373
- constructor(dependencies) {
2374
- this.dependencies = dependencies;
2375
- }
2376
- dependencies;
2377
- preparation = null;
2378
- getPreparation(incremental) {
2379
- this.preparation ??= this.prepare(incremental);
2380
- return this.preparation;
2381
- }
2382
- async commit(preparation, manifest) {
2383
- const entriesBySlug = new Map(
2384
- manifest.entries.map((entry) => [entry.slug, entry])
2385
- );
2386
- const state = {
2387
- version: CONTENT_BUILD_STATE_VERSION,
2388
- entries: Object.fromEntries(
2389
- preparation.currentEntries.map(({ entry, fingerprint }) => {
2390
- const manifestEntry = entriesBySlug.get(toSlug(entry.path));
2391
- return [
2392
- entry.path,
2393
- {
2394
- fingerprint,
2395
- dependencies: collectDependencies2(manifestEntry)
2396
- }
2397
- ];
2398
- })
2399
- ),
2400
- contentIndex: Object.fromEntries(
2401
- [...manifest.contentIndex.entries()].sort(
2402
- ([left], [right]) => left.localeCompare(right)
2403
- )
2404
- )
2405
- };
2406
- try {
2407
- await saveContentBuildState(this.dependencies.buildStatePath, state);
2408
- } catch {
2409
- }
2410
- }
2411
- async prepare(incremental) {
2412
- const currentEntries = await fingerprintContentEntries(
2413
- await this.dependencies.getEntries(),
2414
- (entry) => this.dependencies.readEntry(entry)
2415
- );
2416
- const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
2417
- const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
2418
- const affected = determineAffectedContent(
2419
- changeSet,
2420
- previousState,
2421
- currentEntries.map(({ entry }) => entry.path)
2422
- );
2423
- this.dependencies.observability.tracer.event("build.incremental", {
2424
- incremental: incremental !== false,
2425
- added: changeSet.added.length,
2426
- changed: changeSet.changed.length,
2427
- removed: changeSet.removed.length,
2428
- unchanged: changeSet.unchanged.length,
2429
- affected: affected.direct.size + affected.dependent.size
2430
- });
2431
- return { previousState, currentEntries, changeSet };
2432
- }
2433
- };
2434
- function toSlug(path4) {
2435
- return path4.replace(/\.md$/, "");
2436
- }
2437
- function collectDependencies2(manifestEntry) {
2438
- if (!manifestEntry) return [];
2439
- return [
2440
- ...new Set(
2441
- manifestEntry.links.filter(
2442
- (link) => link.slug !== null
2443
- ).map((link) => link.slug)
2444
- )
2445
- ].sort();
2446
- }
2447
-
2448
- // src/content/content_entry_reader.ts
2449
- var ContentEntryReader = class {
2450
- constructor(source, observability) {
2451
- this.source = source;
2452
- this.observability = observability;
2453
- }
2454
- source;
2455
- observability;
2456
- entries = null;
2457
- texts = /* @__PURE__ */ new Map();
2458
- async getEntries() {
2459
- this.entries ??= await this.observability.tracer.span(
2460
- "content.scan",
2461
- {},
2462
- () => this.source.scan()
2463
- );
2464
- return this.entries;
2465
- }
2466
- async readText(logicalPath) {
2467
- const entry = (await this.getEntries()).find(
2468
- (current) => current.path === logicalPath
2469
- );
2470
- if (!entry) throw new Error(`Content entry was not found: ${logicalPath}`);
2471
- return await this.read(entry);
2472
- }
2473
- read(entry) {
2474
- const cached = this.texts.get(entry.path);
2475
- if (cached) return cached;
2476
- const content = this.source.read(entry).then(
2477
- (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
2478
- );
2479
- this.texts.set(entry.path, content);
2480
- return content;
2481
- }
2482
- };
2483
-
2484
3138
  // src/utils/tags.ts
2485
3139
  function normalizeTag(raw) {
2486
3140
  const cleaned = raw.replace(/[/-]+$/, "");
@@ -2541,7 +3195,7 @@ function stripYamlQuotes(value) {
2541
3195
  }
2542
3196
 
2543
3197
  // src/content/content_index_builder.ts
2544
- var ContentIndexBuilder = class {
3198
+ var ContentIndexBuilder = class _ContentIndexBuilder {
2545
3199
  constructor(source) {
2546
3200
  this.source = source;
2547
3201
  }
@@ -2553,7 +3207,26 @@ var ContentIndexBuilder = class {
2553
3207
  }
2554
3208
  return index;
2555
3209
  }
3210
+ static buildFromAliases(contentEntries, aliasesByPath) {
3211
+ const index = /* @__PURE__ */ new Map();
3212
+ for (const contentEntry of contentEntries) {
3213
+ _ContentIndexBuilder.indexContentEntryFromAliases(
3214
+ index,
3215
+ contentEntry,
3216
+ aliasesByPath.get(contentEntry.path) ?? []
3217
+ );
3218
+ }
3219
+ return index;
3220
+ }
2556
3221
  async indexContentEntry(index, contentEntry, read) {
3222
+ const aliases = contentEntry.path.toLowerCase().endsWith(".md") ? extractFrontmatterAliases(readText(await read(contentEntry))) : [];
3223
+ _ContentIndexBuilder.indexContentEntryFromAliases(
3224
+ index,
3225
+ contentEntry,
3226
+ aliases
3227
+ );
3228
+ }
3229
+ static indexContentEntryFromAliases(index, contentEntry, aliases) {
2557
3230
  const contentPath = contentEntry.path;
2558
3231
  const ext = contentPath.split(".").pop()?.toLowerCase() ?? "";
2559
3232
  const value = ext === "md" ? contentPath.replace(/\.md$/, "") : contentPath;
@@ -2566,20 +3239,246 @@ var ContentIndexBuilder = class {
2566
3239
  }
2567
3240
  }
2568
3241
  if (ext === "md") {
2569
- const markdown = readText(await read(contentEntry));
2570
- for (const alias of extractFrontmatterAliases(markdown)) {
2571
- addIndexEntry(index, alias, value);
3242
+ for (const alias of aliases) addIndexEntry(index, alias, value);
3243
+ }
3244
+ }
3245
+ };
3246
+ function readText(content) {
3247
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
3248
+ }
3249
+ function addIndexEntry(index, key, value) {
3250
+ const normalizedKey = key.toLowerCase();
3251
+ if (!index.has(normalizedKey)) index.set(normalizedKey, value);
3252
+ }
3253
+
3254
+ // src/content/content_build_coordinator.ts
3255
+ var ContentBuildCoordinator = class {
3256
+ constructor(dependencies) {
3257
+ this.dependencies = dependencies;
3258
+ }
3259
+ dependencies;
3260
+ preparation = null;
3261
+ getPreparation(incremental, pipelineFingerprint, fullContentRegenerationRequired = false) {
3262
+ this.preparation ??= this.prepare(
3263
+ incremental,
3264
+ pipelineFingerprint,
3265
+ fullContentRegenerationRequired
3266
+ );
3267
+ return this.preparation;
3268
+ }
3269
+ async commit(preparation, manifest, pipelineFingerprint, outputs = [], manifestEntries = manifest.entries, trackedDependencies = /* @__PURE__ */ new Map()) {
3270
+ const entriesBySlug = new Map(
3271
+ manifest.entries.map((entry) => [entry.slug, entry])
3272
+ );
3273
+ const state2 = {
3274
+ version: CONTENT_BUILD_STATE_VERSION,
3275
+ entries: Object.fromEntries(
3276
+ preparation.currentEntries.map(({ entry, fingerprint }) => {
3277
+ const manifestEntry = entriesBySlug.get(toSlug(entry.path));
3278
+ return [
3279
+ entry.path,
3280
+ {
3281
+ fingerprint,
3282
+ aliases: preparation.currentContentAliases.get(entry.path) ?? [],
3283
+ dependencies: collectDependencies2(
3284
+ manifestEntry,
3285
+ trackedDependencies.get(toSlug(entry.path)) ?? preparation.previousState?.entries[entry.path]?.dependencies ?? []
3286
+ ),
3287
+ linkTargets: collectLinkTargets(manifestEntry)
3288
+ }
3289
+ ];
3290
+ })
3291
+ ),
3292
+ contentIndex: Object.fromEntries(
3293
+ [...manifest.contentIndex.entries()].sort(
3294
+ ([left], [right]) => left.localeCompare(right)
3295
+ )
3296
+ ),
3297
+ pipelineFingerprint,
3298
+ manifestEntries,
3299
+ outputs
3300
+ };
3301
+ try {
3302
+ await saveContentBuildState(this.dependencies.buildStatePath, state2);
3303
+ } catch (error) {
3304
+ this.dependencies.observability.logger.warn(
3305
+ "Incremental build state could not be persisted; the next build regenerates every note instead of reusing stale output.",
3306
+ {
3307
+ path: this.dependencies.buildStatePath,
3308
+ reason: error instanceof Error ? error.message : String(error)
3309
+ }
3310
+ );
3311
+ }
3312
+ }
3313
+ async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired) {
3314
+ const entries = await this.dependencies.observability.tracer.span(
3315
+ "content.discovery",
3316
+ {},
3317
+ () => this.dependencies.getEntries()
3318
+ );
3319
+ const currentEntries = await this.dependencies.observability.tracer.span(
3320
+ "content.fingerprint",
3321
+ {},
3322
+ () => fingerprintContentEntries(
3323
+ entries,
3324
+ (entry) => this.dependencies.readEntry(entry)
3325
+ )
3326
+ );
3327
+ const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
3328
+ const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
3329
+ const currentContentAliases = await this.getContentAliases(
3330
+ currentEntries,
3331
+ previousState,
3332
+ changeSet
3333
+ );
3334
+ const currentContentIndex = await this.dependencies.observability.tracer.span(
3335
+ "content.index",
3336
+ {},
3337
+ () => ContentIndexBuilder.buildFromAliases(
3338
+ currentEntries.map(({ entry }) => entry),
3339
+ currentContentAliases
3340
+ )
3341
+ );
3342
+ const affected = !fullContentRegenerationRequired && previousState?.pipelineFingerprint === pipelineFingerprint ? determineAffectedContent(
3343
+ changeSet,
3344
+ previousState,
3345
+ currentEntries.map(({ entry }) => entry.path),
3346
+ currentContentIndex
3347
+ ) : determineAffectedContent(
3348
+ allContentChanged(currentEntries),
3349
+ void 0,
3350
+ currentEntries.map(({ entry }) => entry.path),
3351
+ currentContentIndex
3352
+ );
3353
+ this.dependencies.observability.tracer.event("build.incremental", {
3354
+ incremental: incremental !== false,
3355
+ added: changeSet.added.length,
3356
+ changed: changeSet.changed.length,
3357
+ removed: changeSet.removed.length,
3358
+ unchanged: changeSet.unchanged.length,
3359
+ affected: affected.direct.size + affected.dependent.size
3360
+ });
3361
+ return {
3362
+ previousState,
3363
+ previousManifestEntriesBySlug: new Map(
3364
+ (previousState?.manifestEntries ?? []).map((entry) => [
3365
+ entry.slug,
3366
+ entry
3367
+ ])
3368
+ ),
3369
+ currentEntries,
3370
+ currentContentAliases,
3371
+ currentContentIndex,
3372
+ changeSet,
3373
+ affectedContent: affected
3374
+ };
3375
+ }
3376
+ async getContentAliases(currentEntries, previousState, changeSet) {
3377
+ const changedPaths = /* @__PURE__ */ new Set([...changeSet.added, ...changeSet.changed]);
3378
+ const aliases = /* @__PURE__ */ new Map();
3379
+ for (const { entry } of currentEntries) {
3380
+ if (!entry.path.toLowerCase().endsWith(".md")) continue;
3381
+ const previousAliases = previousState?.entries[entry.path]?.aliases;
3382
+ if (!changedPaths.has(entry.path) && previousAliases) {
3383
+ aliases.set(entry.path, previousAliases);
3384
+ continue;
2572
3385
  }
3386
+ aliases.set(
3387
+ entry.path,
3388
+ extractFrontmatterAliases(
3389
+ readText2(await this.dependencies.readEntry(entry))
3390
+ )
3391
+ );
3392
+ }
3393
+ return aliases;
3394
+ }
3395
+ };
3396
+ function readText2(content) {
3397
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
3398
+ }
3399
+ function toSlug(path5) {
3400
+ return path5.replace(/\.md$/, "");
3401
+ }
3402
+ function collectDependencies2(manifestEntry, trackedDependencies) {
3403
+ if (!manifestEntry) return [];
3404
+ return [
3405
+ ...new Map(
3406
+ [
3407
+ ...manifestEntry.links.filter(
3408
+ (link) => link.slug !== null
3409
+ ).map((link) => ({ kind: "content", id: link.slug })),
3410
+ ...manifestEntry.assets.map((asset) => ({
3411
+ kind: "file",
3412
+ id: asset.path.replace(/\\/g, "/")
3413
+ })),
3414
+ ...trackedDependencies.filter(
3415
+ (dependency) => dependency.kind === "content" || dependency.kind === "file"
3416
+ ).map((dependency) => ({
3417
+ kind: dependency.kind,
3418
+ id: dependency.id.replace(/\\/g, "/")
3419
+ }))
3420
+ ].map((dependency) => [
3421
+ `${dependency.kind}:${dependency.id}`,
3422
+ dependency
3423
+ ])
3424
+ ).values()
3425
+ ].toSorted(
3426
+ (left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)
3427
+ );
3428
+ }
3429
+ function collectLinkTargets(manifestEntry) {
3430
+ if (!manifestEntry) return [];
3431
+ return [
3432
+ ...new Set(manifestEntry.links.map((link) => link.raw.toLowerCase()))
3433
+ ].sort();
3434
+ }
3435
+
3436
+ // src/content/content_entry_reader.ts
3437
+ var ContentEntryReader = class {
3438
+ constructor(source, observability) {
3439
+ this.source = source;
3440
+ this.observability = observability;
3441
+ }
3442
+ source;
3443
+ observability;
3444
+ entries = null;
3445
+ entriesPromise = null;
3446
+ entriesByPath = null;
3447
+ texts = /* @__PURE__ */ new Map();
3448
+ async getEntries() {
3449
+ if (this.entries) return this.entries;
3450
+ if (this.entriesPromise) return await this.entriesPromise;
3451
+ this.entriesPromise = this.observability.tracer.span(
3452
+ "content.scan",
3453
+ {},
3454
+ () => this.source.scan()
3455
+ );
3456
+ try {
3457
+ this.entries = await this.entriesPromise;
3458
+ this.entriesByPath = new Map(
3459
+ this.entries.map((entry) => [entry.path, entry])
3460
+ );
3461
+ return this.entries;
3462
+ } finally {
3463
+ this.entriesPromise = null;
2573
3464
  }
2574
3465
  }
3466
+ async readText(logicalPath) {
3467
+ await this.getEntries();
3468
+ const entry = this.entriesByPath?.get(logicalPath);
3469
+ if (!entry) throw new Error(`Content entry was not found: ${logicalPath}`);
3470
+ return await this.read(entry);
3471
+ }
3472
+ read(entry) {
3473
+ const cached = this.texts.get(entry.path);
3474
+ if (cached) return cached;
3475
+ const content = this.source.read(entry).then(
3476
+ (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
3477
+ ).finally(() => this.texts.delete(entry.path));
3478
+ this.texts.set(entry.path, content);
3479
+ return content;
3480
+ }
2575
3481
  };
2576
- function readText(content) {
2577
- return typeof content === "string" ? content : new TextDecoder().decode(content);
2578
- }
2579
- function addIndexEntry(index, key, value) {
2580
- const normalizedKey = key.toLowerCase();
2581
- if (!index.has(normalizedKey)) index.set(normalizedKey, value);
2582
- }
2583
3482
 
2584
3483
  // src/content/content_location_resolver.ts
2585
3484
  var ContentLocationResolver = class {
@@ -2588,19 +3487,37 @@ var ContentLocationResolver = class {
2588
3487
  }
2589
3488
  dependencies;
2590
3489
  locations = null;
3490
+ locationsPromise = null;
3491
+ permalinks = null;
3492
+ permalinksPromise = null;
2591
3493
  async getLocations() {
2592
3494
  if (this.locations) return this.locations;
3495
+ if (this.locationsPromise) return await this.locationsPromise;
3496
+ this.locationsPromise = this.dependencies.observability.tracer.span(
3497
+ "content.locations",
3498
+ {},
3499
+ () => this.resolveLocations()
3500
+ );
3501
+ try {
3502
+ return await this.locationsPromise;
3503
+ } finally {
3504
+ this.locationsPromise = null;
3505
+ }
3506
+ }
3507
+ async resolveLocations() {
2593
3508
  const [entries, contentIndex] = await Promise.all([
2594
3509
  this.dependencies.getEntries(),
2595
3510
  this.dependencies.getContentIndex()
2596
3511
  ]);
2597
3512
  await this.dependencies.pluginRuntime.startBuild(contentIndex);
2598
- const inputs = await Promise.all(
2599
- entries.filter((entry) => entry.path.endsWith(".md")).map(async (entry) => ({
3513
+ const inputs = await mapConcurrent2(
3514
+ entries.filter((entry) => entry.path.endsWith(".md")),
3515
+ 64,
3516
+ async (entry) => ({
2600
3517
  slug: toSlug2(entry.path),
2601
3518
  path: entry.path,
2602
3519
  markdown: await this.dependencies.readEntry(entry)
2603
- }))
3520
+ })
2604
3521
  );
2605
3522
  const locations = new Map(
2606
3523
  inputs.map((input) => [input.slug, resolveDefaultContentLocation(input)])
@@ -2625,6 +3542,17 @@ var ContentLocationResolver = class {
2625
3542
  return locations;
2626
3543
  }
2627
3544
  async getPermalinks() {
3545
+ if (this.permalinks) return this.permalinks;
3546
+ if (this.permalinksPromise) return await this.permalinksPromise;
3547
+ this.permalinksPromise = this.createPermalinks();
3548
+ try {
3549
+ this.permalinks = await this.permalinksPromise;
3550
+ return this.permalinks;
3551
+ } finally {
3552
+ this.permalinksPromise = null;
3553
+ }
3554
+ }
3555
+ async createPermalinks() {
2628
3556
  return new Map(
2629
3557
  Array.from(
2630
3558
  (await this.getLocations()).entries(),
@@ -2640,13 +3568,45 @@ var ContentLocationResolver = class {
2640
3568
  }
2641
3569
  }
2642
3570
  };
2643
- function toSlug2(path4) {
2644
- return path4.replace(/\.md$/, "");
3571
+ function toSlug2(path5) {
3572
+ return path5.replace(/\.md$/, "");
3573
+ }
3574
+ async function mapConcurrent2(values, concurrency, map) {
3575
+ const results = new Array(values.length);
3576
+ let nextIndex = 0;
3577
+ async function worker() {
3578
+ while (nextIndex < values.length) {
3579
+ const index = nextIndex;
3580
+ nextIndex += 1;
3581
+ const value = values[index];
3582
+ if (value !== void 0) results[index] = await map(value);
3583
+ }
3584
+ }
3585
+ await Promise.all(
3586
+ Array.from(
3587
+ { length: Math.min(concurrency, values.length) },
3588
+ () => worker()
3589
+ )
3590
+ );
3591
+ return results;
2645
3592
  }
2646
3593
 
2647
3594
  // src/content/file_system_content_source.ts
2648
- import fs3 from "node:fs/promises";
2649
- import path3 from "node:path";
3595
+ import fs4 from "node:fs/promises";
3596
+ import path4 from "node:path";
3597
+ var INTERNAL_CONTENT_IGNORE_PATTERNS = [
3598
+ ".git",
3599
+ ".git/**",
3600
+ ".github",
3601
+ ".github/**",
3602
+ ".obsidian",
3603
+ ".obsidian/**",
3604
+ "node_modules",
3605
+ "node_modules/**",
3606
+ "**/.DS_Store",
3607
+ "**/Thumbs.db",
3608
+ "**/desktop.ini"
3609
+ ];
2650
3610
  var FileSystemContentSource = class {
2651
3611
  constructor(contentDirectory, exclude = []) {
2652
3612
  this.contentDirectory = contentDirectory;
@@ -2655,33 +3615,41 @@ var FileSystemContentSource = class {
2655
3615
  contentDirectory;
2656
3616
  exclude;
2657
3617
  async scan() {
2658
- return await this.scanDirectory(this.contentDirectory, "");
3618
+ try {
3619
+ return await this.scanDirectory(this.contentDirectory, "");
3620
+ } catch (error) {
3621
+ if (isMissingDirectoryError(error, this.contentDirectory)) {
3622
+ throw missingContentDirectoryError(this.contentDirectory);
3623
+ }
3624
+ throw error;
3625
+ }
2659
3626
  }
2660
3627
  async read(entry) {
2661
3628
  const filePath = this.resolveFilePath(entry.path);
2662
- const stats = await fs3.lstat(filePath);
3629
+ const stats = await fs4.lstat(filePath);
2663
3630
  if (!stats.isFile() || stats.isSymbolicLink()) {
2664
3631
  throw new Error(`Content entry is not a regular file: ${entry.path}`);
2665
3632
  }
2666
- return await fs3.readFile(filePath);
3633
+ return await fs4.readFile(filePath);
2667
3634
  }
2668
- async scanDirectory(directory, parentPath) {
2669
- const directoryEntries = await fs3.readdir(directory, {
3635
+ async scanDirectory(directory, parentPath2) {
3636
+ const directoryEntries = await fs4.readdir(directory, {
2670
3637
  withFileTypes: true
2671
3638
  });
2672
3639
  const entries = [];
2673
3640
  for (const directoryEntry of directoryEntries) {
2674
3641
  if (directoryEntry.isSymbolicLink()) continue;
2675
3642
  const logicalPath = normalizeContentPath(
2676
- parentPath ? `${parentPath}/${directoryEntry.name}` : directoryEntry.name
3643
+ parentPath2 ? `${parentPath2}/${directoryEntry.name}` : directoryEntry.name
2677
3644
  );
2678
- const filePath = path3.join(directory, directoryEntry.name);
3645
+ const filePath = path4.join(directory, directoryEntry.name);
3646
+ if (this.isExcluded(logicalPath)) continue;
2679
3647
  if (directoryEntry.isDirectory()) {
2680
3648
  entries.push(...await this.scanDirectory(filePath, logicalPath));
2681
3649
  continue;
2682
3650
  }
2683
- if (!directoryEntry.isFile() || this.isExcluded(logicalPath)) continue;
2684
- const stats = await fs3.stat(filePath);
3651
+ if (!directoryEntry.isFile()) continue;
3652
+ const stats = await fs4.stat(filePath);
2685
3653
  entries.push({
2686
3654
  path: logicalPath,
2687
3655
  metadata: {
@@ -2693,26 +3661,44 @@ var FileSystemContentSource = class {
2693
3661
  return entries;
2694
3662
  }
2695
3663
  isExcluded(logicalPath) {
2696
- if (isExcluded(this.exclude, logicalPath)) return true;
2697
- return logicalPath.endsWith(".md") && isExcluded(this.exclude, logicalPath.slice(0, -3));
3664
+ return isIgnoredContentPath(logicalPath, this.exclude);
2698
3665
  }
2699
3666
  resolveFilePath(logicalPath) {
2700
3667
  const normalizedPath = normalizeLogicalPath(logicalPath);
2701
- const filePath = path3.resolve(this.contentDirectory, normalizedPath);
2702
- const relativePath = path3.relative(this.contentDirectory, filePath);
2703
- if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
3668
+ const filePath = path4.resolve(this.contentDirectory, normalizedPath);
3669
+ const relativePath = path4.relative(this.contentDirectory, filePath);
3670
+ if (relativePath.startsWith("..") || path4.isAbsolute(relativePath)) {
2704
3671
  throw new Error(`Invalid content path: ${logicalPath}`);
2705
3672
  }
2706
3673
  return filePath;
2707
3674
  }
2708
3675
  };
3676
+ function isIgnoredContentPath(logicalPath, exclude = []) {
3677
+ if (isExcluded(INTERNAL_CONTENT_IGNORE_PATTERNS, logicalPath)) return true;
3678
+ if (isExcluded(exclude, logicalPath)) return true;
3679
+ return logicalPath.endsWith(".md") && isExcluded(exclude, logicalPath.slice(0, -3));
3680
+ }
2709
3681
  function normalizeLogicalPath(logicalPath) {
2710
3682
  const normalizedPath = normalizeContentPath(logicalPath);
2711
- if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path3.isAbsolute(normalizedPath) || path3.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
3683
+ if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path4.isAbsolute(normalizedPath) || path4.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
2712
3684
  throw new Error(`Invalid content path: ${logicalPath}`);
2713
3685
  }
2714
3686
  return normalizedPath;
2715
3687
  }
3688
+ function isMissingDirectoryError(error, directory) {
3689
+ return error instanceof Error && "code" in error && error.code === "ENOENT" && "path" in error && error.path === directory;
3690
+ }
3691
+ function missingContentDirectoryError(directory) {
3692
+ const relative = path4.relative(process.cwd(), directory);
3693
+ const display = relative && !relative.startsWith("..") ? relative : directory;
3694
+ const error = new Error(
3695
+ `Could not find the content directory:
3696
+
3697
+ ${display}`
3698
+ );
3699
+ error.hint = "Check `content.directory` in your Riebeckite configuration.";
3700
+ return error;
3701
+ }
2716
3702
 
2717
3703
  // src/content/content_links.ts
2718
3704
  var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
@@ -2768,7 +3754,7 @@ function readOptionalStableId(value, field) {
2768
3754
 
2769
3755
  // src/content/manifest_builder.ts
2770
3756
  var ManifestBuilder = class {
2771
- createEntry(slug, markdown, processed, contentIndex, location) {
3757
+ createEntry(slug, markdown, processed, contentIndex, location, publishing) {
2772
3758
  const links = extractContentLinks(markdown, contentIndex);
2773
3759
  const assets = links.filter(
2774
3760
  (link) => (link.kind === "image" || link.kind === "attachment") && link.slug !== null
@@ -2781,6 +3767,7 @@ var ManifestBuilder = class {
2781
3767
  publicLocation: location,
2782
3768
  title: getManifestTitle(slug, processed.frontmatter.title),
2783
3769
  frontmatter: processed.frontmatter,
3770
+ publishing,
2784
3771
  html: processed.html,
2785
3772
  tags: uniqueStrings([
2786
3773
  ...normalizeFrontmatterTags(processed.frontmatter.tags),
@@ -2818,9 +3805,11 @@ var ManifestBuilder = class {
2818
3805
  const manifest = {
2819
3806
  entries,
2820
3807
  publicEntries: [...entries],
3808
+ discoverableEntries: [...entries],
2821
3809
  bySlug,
2822
3810
  byContentId,
2823
3811
  byPermalink,
3812
+ byRoutablePermalink: new Map(byPermalink),
2824
3813
  redirects: /* @__PURE__ */ new Map(),
2825
3814
  publicRedirects: /* @__PURE__ */ new Map(),
2826
3815
  byTag,
@@ -2831,7 +3820,10 @@ var ManifestBuilder = class {
2831
3820
  assets: [],
2832
3821
  clientEntries: [],
2833
3822
  diagnostics: [],
2834
- generatedOutputs: []
3823
+ generatedOutputs: [],
3824
+ folderLocations: /* @__PURE__ */ new Map(),
3825
+ pageRoutes: [],
3826
+ pagePaths: []
2835
3827
  };
2836
3828
  return { ...manifest, graph: createContentGraph(manifest) };
2837
3829
  }
@@ -2872,21 +3864,283 @@ function appendUniqueToMap(map, key, value) {
2872
3864
  map.set(key, values);
2873
3865
  }
2874
3866
 
3867
+ // src/content/output_dependency.ts
3868
+ function determineOutputChanges(input) {
3869
+ const current = buildOutputInventory(
3870
+ input.manifest,
3871
+ input.pluginPageOutputs,
3872
+ input.contentOutputDependencies
3873
+ );
3874
+ const previous = new Map(
3875
+ (input.previousState?.outputs ?? []).map((output) => [output.path, output])
3876
+ );
3877
+ const changedContent = collectChangedContent(input);
3878
+ const changedTags = collectChangedTags(
3879
+ input.previousState,
3880
+ input.manifest,
3881
+ changedContent
3882
+ );
3883
+ const changedFolders = collectChangedFolders(
3884
+ input.previousState,
3885
+ input.manifest,
3886
+ changedContent
3887
+ );
3888
+ const affected = /* @__PURE__ */ new Map();
3889
+ const unchanged = [];
3890
+ for (const output of current) {
3891
+ if (isOutputAffected(output, changedContent, changedTags, changedFolders)) {
3892
+ affected.set(output.path, output);
3893
+ } else {
3894
+ unchanged.push(output);
3895
+ }
3896
+ }
3897
+ for (const output of current) {
3898
+ const previousOutput = previous.get(output.path);
3899
+ if (!previousOutput || output.kind !== previousOutput.kind) {
3900
+ affected.set(output.path, output);
3901
+ }
3902
+ }
3903
+ const currentPaths = new Set(current.map((output) => output.path));
3904
+ const removed = [...previous.values()].filter(
3905
+ (output) => !currentPaths.has(output.path)
3906
+ );
3907
+ const fullRegenerationRequired = current.some(
3908
+ (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
3909
+ );
3910
+ const affectedValues = [...affected.values()].sort(compareOutput);
3911
+ const unchangedValues = unchanged.filter((output) => !affected.has(output.path)).sort(compareOutput);
3912
+ return {
3913
+ affected: affectedValues,
3914
+ removed: removed.sort(compareOutput),
3915
+ unchanged: unchangedValues,
3916
+ fullRegenerationRequired,
3917
+ candidateOutputCount: current.length,
3918
+ affectedOutputCount: affectedValues.length,
3919
+ removedOutputCount: removed.length,
3920
+ unchangedOutputCount: unchangedValues.length
3921
+ };
3922
+ }
3923
+ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDependencies = []) {
3924
+ const outputs = /* @__PURE__ */ new Map();
3925
+ for (const entry of manifest.publicEntries) {
3926
+ addOutput(outputs, contentOutput(entry, contentOutputDependencies));
3927
+ }
3928
+ for (const [path5, redirect] of manifest.publicRedirects) {
3929
+ addOutput(outputs, {
3930
+ kind: "redirect",
3931
+ path: htmlOutputPath(path5),
3932
+ producer: `content:${redirect.slug}:redirect`,
3933
+ dependencies: [{ type: "content", slug: redirect.slug }]
3934
+ });
3935
+ }
3936
+ for (const output of pluginPageOutputs) {
3937
+ addOutput(outputs, { ...output, path: htmlOutputPath(output.path) });
3938
+ }
3939
+ for (const output of manifest.generatedOutputs) {
3940
+ addOutput(outputs, {
3941
+ kind: "generated",
3942
+ path: output.path,
3943
+ producer: `plugin:${output.owner}`,
3944
+ dependencies: output.dependencies ?? [{ type: "unknown" }]
3945
+ });
3946
+ }
3947
+ return [...outputs.values()].sort(compareOutput);
3948
+ }
3949
+ function htmlOutputPath(pathname) {
3950
+ const normalized = normalizePublicPath(pathname);
3951
+ if (normalized === "/") return "index.html";
3952
+ const withoutSlash = normalized.replace(/^\//, "");
3953
+ if (pathname.endsWith("/")) return `${withoutSlash}/index.html`;
3954
+ return `${withoutSlash}.html`;
3955
+ }
3956
+ function contentOutput(entry, dependencies) {
3957
+ return {
3958
+ kind: "content",
3959
+ path: htmlOutputPath(entry.permalink),
3960
+ producer: `content:${entry.slug}`,
3961
+ dependencies: [{ type: "content", slug: entry.slug }, ...dependencies]
3962
+ };
3963
+ }
3964
+ function isOutputAffected(output, changedContent, changedTags, changedFolders) {
3965
+ for (const dependency of output.dependencies) {
3966
+ if (dependency.type === "unknown") return true;
3967
+ if (dependency.type === "global") return changedContent.size > 0;
3968
+ if (dependency.type === "content" && changedContent.has(dependency.slug)) {
3969
+ return true;
3970
+ }
3971
+ if (dependency.type === "tag" && changedTags.has(dependency.tag))
3972
+ return true;
3973
+ if (dependency.type === "folder" && changedFolders.has(dependency.folder)) {
3974
+ return true;
3975
+ }
3976
+ }
3977
+ return false;
3978
+ }
3979
+ function collectChangedContent(input) {
3980
+ const changed = /* @__PURE__ */ new Set([
3981
+ ...input.affectedContent.direct,
3982
+ ...input.affectedContent.dependent,
3983
+ ...input.changeSet.removed.filter(isMarkdownPath3).map(toSlug3)
3984
+ ]);
3985
+ const previousEntries = input.previousState?.manifestEntries ?? [];
3986
+ const currentEntries = input.manifest.entries;
3987
+ const previousBySlug = new Map(
3988
+ previousEntries.map((entry) => [entry.slug, entry])
3989
+ );
3990
+ const currentBySlug = new Map(
3991
+ currentEntries.map((entry) => [entry.slug, entry])
3992
+ );
3993
+ for (const slug of changed) {
3994
+ for (const target of linkTargets(previousBySlug.get(slug)))
3995
+ changed.add(target);
3996
+ for (const target of linkTargets(currentBySlug.get(slug)))
3997
+ changed.add(target);
3998
+ }
3999
+ return changed;
4000
+ }
4001
+ function collectChangedTags(previousState, manifest, changedContent) {
4002
+ const previousBySlug = new Map(
4003
+ (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
4004
+ );
4005
+ const currentBySlug = new Map(
4006
+ manifest.entries.map((entry) => [entry.slug, entry])
4007
+ );
4008
+ const tags = /* @__PURE__ */ new Set();
4009
+ for (const slug of changedContent) {
4010
+ for (const tag of previousBySlug.get(slug)?.tags ?? []) tags.add(tag);
4011
+ for (const tag of currentBySlug.get(slug)?.tags ?? []) tags.add(tag);
4012
+ }
4013
+ return tags;
4014
+ }
4015
+ function collectChangedFolders(previousState, manifest, changedContent) {
4016
+ const previousBySlug = new Map(
4017
+ (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
4018
+ );
4019
+ const currentBySlug = new Map(
4020
+ manifest.entries.map((entry) => [entry.slug, entry])
4021
+ );
4022
+ const folders = /* @__PURE__ */ new Set();
4023
+ for (const slug of changedContent) {
4024
+ addFolderAncestors(folders, previousBySlug.get(slug)?.slug ?? slug);
4025
+ addFolderAncestors(folders, currentBySlug.get(slug)?.slug ?? slug);
4026
+ }
4027
+ return folders;
4028
+ }
4029
+ function addFolderAncestors(folders, slug) {
4030
+ const segments = slug.split("/");
4031
+ segments.pop();
4032
+ while (true) {
4033
+ folders.add(segments.join("/"));
4034
+ if (segments.length === 0) return;
4035
+ segments.pop();
4036
+ }
4037
+ }
4038
+ function linkTargets(entry) {
4039
+ if (!entry) return [];
4040
+ return entry.links.filter(
4041
+ (link) => link.slug !== null
4042
+ ).map((link) => link.slug);
4043
+ }
4044
+ function addOutput(outputs, output) {
4045
+ outputs.set(output.path, output);
4046
+ }
4047
+ function normalizePublicPath(pathname) {
4048
+ const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4049
+ return path5 === "/" ? path5 : path5.replace(/\/$/, "");
4050
+ }
4051
+ function isMarkdownPath3(path5) {
4052
+ return path5.endsWith(".md");
4053
+ }
4054
+ function toSlug3(path5) {
4055
+ return path5.replace(/\.md$/, "");
4056
+ }
4057
+ function compareOutput(left, right) {
4058
+ return left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind);
4059
+ }
4060
+
4061
+ // src/content/publishing.ts
4062
+ function resolvePublishingState(frontmatter, options) {
4063
+ const explicitVisibility = resolveExplicitVisibility(frontmatter?.visibility);
4064
+ const publishAt = resolvePublishAt(frontmatter?.publishAt);
4065
+ if (explicitVisibility === "draft")
4066
+ return state("draft", false, false, publishAt);
4067
+ if (publishAt && publishAt.getTime() > options.buildTime.getTime()) {
4068
+ return state("scheduled", false, false, publishAt);
4069
+ }
4070
+ if (explicitVisibility === "unlisted") {
4071
+ return state("unlisted", true, false, publishAt);
4072
+ }
4073
+ if (explicitVisibility === "public" || publishAt) {
4074
+ return state("public", true, true, publishAt);
4075
+ }
4076
+ const routable = isPublishable(options.strategy, frontmatter);
4077
+ return state(routable ? "public" : "draft", routable, routable, publishAt);
4078
+ }
4079
+ function resolvePublishingBuildTime(input) {
4080
+ if (input === void 0) return /* @__PURE__ */ new Date();
4081
+ if (input instanceof Date) {
4082
+ if (Number.isNaN(input.getTime()))
4083
+ throw new Error("Invalid publishing build time.");
4084
+ return new Date(input.getTime());
4085
+ }
4086
+ const parsed = new Date(input);
4087
+ if (Number.isNaN(parsed.getTime()))
4088
+ throw new Error("Invalid publishing build time.");
4089
+ return parsed;
4090
+ }
4091
+ function resolveExplicitVisibility(value) {
4092
+ if (value === void 0) return void 0;
4093
+ if (value === "public" || value === "unlisted" || value === "draft")
4094
+ return value;
4095
+ throw new Error(
4096
+ `Invalid content visibility "${String(value)}". Expected public, unlisted, or draft.`
4097
+ );
4098
+ }
4099
+ function resolvePublishAt(value) {
4100
+ if (value === void 0) return void 0;
4101
+ if (!(typeof value === "string" || value instanceof Date)) {
4102
+ throw new Error("Invalid publishAt. Expected an ISO 8601 date string.");
4103
+ }
4104
+ const parsed = value instanceof Date ? value : new Date(value);
4105
+ if (Number.isNaN(parsed.getTime())) {
4106
+ throw new Error(
4107
+ `Invalid publishAt "${String(value)}". Expected an ISO 8601 date string.`
4108
+ );
4109
+ }
4110
+ return new Date(parsed.getTime());
4111
+ }
4112
+ function state(visibility, routable, discoverable, publishAt) {
4113
+ return {
4114
+ visibility,
4115
+ routable,
4116
+ discoverable,
4117
+ ...publishAt ? { publishAt } : {}
4118
+ };
4119
+ }
4120
+
2875
4121
  // src/content/content_manager.ts
4122
+ var CONTENT_PROCESSING_CONCURRENCY = 16;
2876
4123
  var ContentManager = class {
2877
4124
  constructor(content, exclude = [], pipelineOptions = {}) {
2878
4125
  this.pipelineOptions = pipelineOptions;
2879
4126
  this.pipelineOptions = {
2880
4127
  ...pipelineOptions,
2881
- plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins
4128
+ plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins,
4129
+ isRoutable: (slug) => this.isRoutable(slug)
2882
4130
  };
4131
+ this.publishingBuildTime = resolvePublishingBuildTime(
4132
+ pipelineOptions.publishingBuildTime
4133
+ );
2883
4134
  this.source = typeof content === "string" ? pipelineOptions.config?.content.source ?? new FileSystemContentSource(content, [
2884
4135
  ...exclude,
2885
4136
  CONTENT_BUILD_STATE_EXCLUDE
2886
4137
  ]) : content;
2887
4138
  this.pipelineOptions = {
2888
4139
  ...this.pipelineOptions,
2889
- contentSource: this.source
4140
+ contentSource: this.source,
4141
+ onContentDependencies: (slug, dependencies) => {
4142
+ this.trackedContentDependencies.set(slug, dependencies);
4143
+ }
2890
4144
  };
2891
4145
  this.contentIndexBuilder = new ContentIndexBuilder(this.source);
2892
4146
  this.pluginRuntime = new PluginRuntime(this.pipelineOptions);
@@ -2900,7 +4154,8 @@ var ContentManager = class {
2900
4154
  getEntries: () => this.entryReader.getEntries(),
2901
4155
  readEntry: (entry) => this.entryReader.read(entry),
2902
4156
  getContentIndex: () => this.getContentIndex(),
2903
- pluginRuntime: this.pluginRuntime
4157
+ pluginRuntime: this.pluginRuntime,
4158
+ observability
2904
4159
  });
2905
4160
  this.buildCoordinator = new ContentBuildCoordinator({
2906
4161
  buildStatePath,
@@ -2915,13 +4170,22 @@ var ContentManager = class {
2915
4170
  manifestBuilder = new ManifestBuilder();
2916
4171
  pluginRuntime;
2917
4172
  contentIndex = null;
4173
+ contentIndexPromise = null;
2918
4174
  contentCache = /* @__PURE__ */ new Map();
2919
4175
  manifest = null;
4176
+ manifestPromise = null;
4177
+ pipelineFingerprint = null;
2920
4178
  pipeline = null;
2921
4179
  entryReader;
2922
4180
  locationResolver;
2923
4181
  buildCoordinator;
2924
4182
  isBuildTime = false;
4183
+ publishingBuildTime;
4184
+ routableSlugs = null;
4185
+ routableSlugsPromise = null;
4186
+ processedContentCount = 0;
4187
+ outputChangeSet = null;
4188
+ trackedContentDependencies = /* @__PURE__ */ new Map();
2925
4189
  async getAllPosts() {
2926
4190
  return (await this.scan()).filter((entry) => entry.path.endsWith(".md")).map((entry) => ({ slug: entry.path.replace(/\.md$/, "") }));
2927
4191
  }
@@ -2933,17 +4197,33 @@ var ContentManager = class {
2933
4197
  }
2934
4198
  async getContentIndex(preparation) {
2935
4199
  if (this.contentIndex) return this.contentIndex;
4200
+ if (!preparation && this.contentIndexPromise)
4201
+ return await this.contentIndexPromise;
2936
4202
  if (preparation?.previousState && !hasContentChanges(preparation.changeSet)) {
2937
4203
  this.contentIndex = new Map(
2938
4204
  Object.entries(preparation.previousState.contentIndex)
2939
4205
  );
2940
4206
  return this.contentIndex;
2941
4207
  }
2942
- this.contentIndex = await this.contentIndexBuilder.build(
2943
- await this.entryReader.getEntries(),
2944
- (entry) => this.entryReader.read(entry)
4208
+ if (preparation) {
4209
+ this.contentIndex = preparation.currentContentIndex;
4210
+ return this.contentIndex;
4211
+ }
4212
+ const promise = this.observability().tracer.span(
4213
+ "content.index",
4214
+ {},
4215
+ async () => await this.contentIndexBuilder.build(
4216
+ await this.entryReader.getEntries(),
4217
+ (entry) => this.entryReader.read(entry)
4218
+ )
2945
4219
  );
2946
- return this.contentIndex;
4220
+ if (!preparation) this.contentIndexPromise = promise;
4221
+ try {
4222
+ this.contentIndex = await promise;
4223
+ return this.contentIndex;
4224
+ } finally {
4225
+ if (!preparation) this.contentIndexPromise = null;
4226
+ }
2947
4227
  }
2948
4228
  async getProcessedContent(slug) {
2949
4229
  const cached = this.contentCache.get(slug);
@@ -2960,6 +4240,7 @@ var ContentManager = class {
2960
4240
  ]);
2961
4241
  await this.pluginRuntime.startBuild(contentIndex);
2962
4242
  await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4243
+ await this.ensureRoutableSlugs();
2963
4244
  this.pipeline ??= new Pipeline(
2964
4245
  contentIndex,
2965
4246
  await this.locationResolver.getPermalinks(),
@@ -2970,6 +4251,12 @@ var ContentManager = class {
2970
4251
  const content = await this.pipeline.execute(rawPost, {
2971
4252
  sourceSlug: slug
2972
4253
  });
4254
+ this.processedContentCount += 1;
4255
+ if (this.processedContentCount % 100 === 0) {
4256
+ this.observability().tracer.event("content.process.sample", {
4257
+ processed: this.processedContentCount
4258
+ });
4259
+ }
2973
4260
  await this.pluginRuntime.runPostHook(
2974
4261
  "onPostParsed",
2975
4262
  slug,
@@ -2990,9 +4277,24 @@ var ContentManager = class {
2990
4277
  );
2991
4278
  }
2992
4279
  async getManifest(options) {
4280
+ if (this.manifest) return this.manifest;
4281
+ if (this.manifestPromise) return await this.manifestPromise;
4282
+ this.manifestPromise = this.buildManifest(options);
4283
+ try {
4284
+ return await this.manifestPromise;
4285
+ } catch (error) {
4286
+ this.manifestPromise = null;
4287
+ throw error;
4288
+ }
4289
+ }
4290
+ async buildManifest(options) {
2993
4291
  if (this.manifest) return this.manifest;
2994
4292
  this.enableBuildTime();
2995
- const preparation = options ? await this.buildCoordinator.getPreparation(options.incremental) : void 0;
4293
+ const preparation = options ? await this.buildCoordinator.getPreparation(
4294
+ options.incremental,
4295
+ this.getPipelineFingerprint(),
4296
+ this.requiresFullContentRegeneration()
4297
+ ) : void 0;
2996
4298
  return await this.observability().tracer.span(
2997
4299
  "content.manifest",
2998
4300
  {},
@@ -3002,26 +4304,38 @@ var ContentManager = class {
3002
4304
  this.getContentIndex(preparation),
3003
4305
  this.getContentLocations()
3004
4306
  ]);
3005
- const entries = await Promise.all(
3006
- posts.map(async (post) => {
3007
- const [rawPost, processed] = await Promise.all([
3008
- this.getPost(post.slug),
3009
- this.getProcessedContent(post.slug)
3010
- ]);
3011
- const location = locations.get(post.slug);
3012
- if (!location) {
3013
- throw new Error(
3014
- `Content public location was not resolved: ${post.slug}`
4307
+ const entries = await this.observability().tracer.span(
4308
+ "content.manifest.entries",
4309
+ {},
4310
+ () => mapConcurrent3(
4311
+ posts,
4312
+ CONTENT_PROCESSING_CONCURRENCY,
4313
+ async (post) => {
4314
+ const reused = this.reuseManifestEntry(post.slug, preparation);
4315
+ if (reused) return reused;
4316
+ const [rawPost, processed] = await Promise.all([
4317
+ this.getPost(post.slug),
4318
+ this.getProcessedContent(post.slug)
4319
+ ]);
4320
+ const location = locations.get(post.slug);
4321
+ if (!location) {
4322
+ throw new Error(
4323
+ `Content public location was not resolved: ${post.slug}`
4324
+ );
4325
+ }
4326
+ return this.manifestBuilder.createEntry(
4327
+ post.slug,
4328
+ rawPost,
4329
+ processed,
4330
+ contentIndex,
4331
+ location,
4332
+ resolvePublishingState(processed.frontmatter, {
4333
+ strategy: this.publishStrategy(),
4334
+ buildTime: this.publishingBuildTime
4335
+ })
3015
4336
  );
3016
4337
  }
3017
- return this.manifestBuilder.createEntry(
3018
- post.slug,
3019
- rawPost,
3020
- processed,
3021
- contentIndex,
3022
- location
3023
- );
3024
- })
4338
+ )
3025
4339
  );
3026
4340
  await this.pluginRuntime.runGraphHook(entries, contentIndex);
3027
4341
  const manifest = await this.observability().tracer.span(
@@ -3029,9 +4343,17 @@ var ContentManager = class {
3029
4343
  {},
3030
4344
  () => this.manifestBuilder.build(entries, contentIndex)
3031
4345
  );
4346
+ const cacheManifestEntries = structuredClone(manifest.entries);
3032
4347
  this.locationResolver.populateRedirects(manifest, locations);
3033
- await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
3034
4348
  this.applyPublicView(manifest);
4349
+ manifest.pagePaths = [
4350
+ ...await this.pluginRuntime.getPagePaths(manifest, contentIndex)
4351
+ ];
4352
+ manifest.pageRoutes = [
4353
+ ...await this.pluginRuntime.getPageRoutes(manifest, contentIndex)
4354
+ ];
4355
+ await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
4356
+ this.synchronizeManifestHtml(manifest, cacheManifestEntries);
3035
4357
  manifest.assets = this.pluginRuntime.collectAssets();
3036
4358
  manifest.clientEntries = this.pluginRuntime.collectClientEntries();
3037
4359
  manifest.diagnostics = [
@@ -3040,8 +4362,39 @@ var ContentManager = class {
3040
4362
  ];
3041
4363
  await this.pluginRuntime.runBuildEnd(manifest, contentIndex);
3042
4364
  manifest.generatedOutputs = this.pluginRuntime.collectGeneratedOutputs();
4365
+ const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4366
+ manifest,
4367
+ contentIndex
4368
+ );
4369
+ this.outputChangeSet = preparation ? determineOutputChanges({
4370
+ manifest,
4371
+ previousState: preparation.previousState,
4372
+ changeSet: preparation.changeSet,
4373
+ affectedContent: preparation.affectedContent,
4374
+ pluginPageOutputs,
4375
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4376
+ }) : null;
4377
+ if (this.outputChangeSet) {
4378
+ this.observability().tracer.event("build.incremental.outputs", {
4379
+ candidateOutputCount: this.outputChangeSet.candidateOutputCount,
4380
+ affectedOutputCount: this.outputChangeSet.affectedOutputCount,
4381
+ removedOutputCount: this.outputChangeSet.removedOutputCount,
4382
+ unchangedOutputCount: this.outputChangeSet.unchangedOutputCount,
4383
+ fullRegenerationRequired: this.outputChangeSet.fullRegenerationRequired
4384
+ });
4385
+ }
3043
4386
  if (preparation)
3044
- await this.buildCoordinator.commit(preparation, manifest);
4387
+ await this.buildCoordinator.commit(
4388
+ preparation,
4389
+ manifest,
4390
+ this.getPipelineFingerprint(),
4391
+ this.outputChangeSet ? [
4392
+ ...this.outputChangeSet.affected,
4393
+ ...this.outputChangeSet.unchanged
4394
+ ] : [],
4395
+ cacheManifestEntries,
4396
+ this.trackedContentDependencies
4397
+ );
3045
4398
  this.manifest = manifest;
3046
4399
  return manifest;
3047
4400
  }
@@ -3050,6 +4403,34 @@ var ContentManager = class {
3050
4403
  async build(options = {}) {
3051
4404
  return await this.getManifest(options);
3052
4405
  }
4406
+ async getOutputChangeSet(options = {}) {
4407
+ await this.getManifest(options);
4408
+ if (!this.outputChangeSet) {
4409
+ const manifest = await this.getManifest();
4410
+ const contentIndex = await this.getContentIndex();
4411
+ const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4412
+ manifest,
4413
+ contentIndex
4414
+ );
4415
+ return determineOutputChanges({
4416
+ manifest,
4417
+ previousState: void 0,
4418
+ changeSet: {
4419
+ added: manifest.entries.map((entry) => `${entry.slug}.md`),
4420
+ changed: [],
4421
+ removed: [],
4422
+ unchanged: []
4423
+ },
4424
+ affectedContent: {
4425
+ direct: new Set(manifest.entries.map((entry) => entry.slug)),
4426
+ dependent: /* @__PURE__ */ new Set()
4427
+ },
4428
+ pluginPageOutputs,
4429
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4430
+ });
4431
+ }
4432
+ return this.outputChangeSet;
4433
+ }
3053
4434
  async getBacklinks(targetSlug) {
3054
4435
  const graph = await this.getContentGraph();
3055
4436
  return graph.incomingSlugs(targetSlug).map((slug) => ({
@@ -3104,19 +4485,106 @@ var ContentManager = class {
3104
4485
  return await this.locationResolver.getLocations();
3105
4486
  }
3106
4487
  applyPublicView(manifest) {
3107
- const strategy = this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4488
+ const strategy = this.publishStrategy();
4489
+ for (const entry of manifest.entries) {
4490
+ entry.publishing = resolvePublishingState(entry.frontmatter, {
4491
+ strategy,
4492
+ buildTime: this.publishingBuildTime
4493
+ });
4494
+ }
3108
4495
  manifest.publicEntries = manifest.entries.filter(
3109
- (entry) => isPublishable(strategy, entry.frontmatter)
4496
+ (entry) => entry.publishing.routable
4497
+ );
4498
+ manifest.discoverableEntries = manifest.entries.filter(
4499
+ (entry) => entry.publishing.discoverable
3110
4500
  );
3111
4501
  const publicSlugs = new Set(
3112
4502
  manifest.publicEntries.map((entry) => entry.slug)
3113
4503
  );
4504
+ const publicPermalinks = new Map(
4505
+ manifest.publicEntries.map((entry) => [entry.permalink, entry])
4506
+ );
4507
+ manifest.byRoutablePermalink = publicPermalinks;
3114
4508
  manifest.publicRedirects = new Map(
3115
4509
  [...manifest.redirects].filter(
3116
4510
  ([, redirect]) => publicSlugs.has(redirect.slug)
3117
4511
  )
3118
4512
  );
3119
4513
  }
4514
+ synchronizeManifestHtml(manifest, preHookEntries) {
4515
+ const preHookHtml = new Map(
4516
+ preHookEntries.map((entry) => [entry.slug, entry.html])
4517
+ );
4518
+ for (const entry of manifest.entries) {
4519
+ const changed = preHookHtml.get(entry.slug) !== entry.html;
4520
+ const cached = this.contentCache.get(entry.slug);
4521
+ if (cached) {
4522
+ if (changed) cached.html = entry.html;
4523
+ continue;
4524
+ }
4525
+ this.contentCache.set(entry.slug, {
4526
+ frontmatter: entry.frontmatter,
4527
+ html: entry.html
4528
+ });
4529
+ }
4530
+ }
4531
+ publishStrategy() {
4532
+ return this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4533
+ }
4534
+ reuseManifestEntry(slug, preparation) {
4535
+ if (!preparation?.previousState?.manifestEntries) return null;
4536
+ if (preparation.previousState.pipelineFingerprint !== this.getPipelineFingerprint()) {
4537
+ return null;
4538
+ }
4539
+ if (preparation.affectedContent.direct.has(slug)) return null;
4540
+ if (preparation.affectedContent.dependent.has(slug)) return null;
4541
+ const entry = preparation.previousManifestEntriesBySlug.get(slug);
4542
+ if (!entry) return null;
4543
+ this.observability().tracer.event("content.reuse", { slug });
4544
+ return cloneManifestEntry(entry);
4545
+ }
4546
+ getPipelineFingerprint() {
4547
+ if (!this.pipelineOptions.config) return void 0;
4548
+ this.pipelineFingerprint ??= computePipelineFingerprint(
4549
+ this.pipelineOptions.config
4550
+ );
4551
+ return this.pipelineFingerprint;
4552
+ }
4553
+ requiresFullContentRegeneration() {
4554
+ const config = this.pipelineOptions.config;
4555
+ return config ? !isPersistentlyCacheable("", config).cacheable : false;
4556
+ }
4557
+ isRoutable(slug) {
4558
+ if (!this.routableSlugs) {
4559
+ throw new Error(`Publishing state was not resolved: ${slug}`);
4560
+ }
4561
+ return this.routableSlugs.has(slug);
4562
+ }
4563
+ async ensureRoutableSlugs() {
4564
+ if (this.routableSlugs) return this.routableSlugs;
4565
+ if (this.routableSlugsPromise) return await this.routableSlugsPromise;
4566
+ this.routableSlugsPromise = this.resolveRoutableSlugs();
4567
+ try {
4568
+ this.routableSlugs = await this.routableSlugsPromise;
4569
+ return this.routableSlugs;
4570
+ } finally {
4571
+ this.routableSlugsPromise = null;
4572
+ }
4573
+ }
4574
+ async resolveRoutableSlugs() {
4575
+ const strategy = this.publishStrategy();
4576
+ const posts = await this.getAllPosts();
4577
+ const routableSlugs = /* @__PURE__ */ new Set();
4578
+ await mapConcurrent3(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
4579
+ const markdown = await this.getPost(post.slug);
4580
+ const publishing = resolvePublishingState(readFrontmatter(markdown), {
4581
+ strategy,
4582
+ buildTime: this.publishingBuildTime
4583
+ });
4584
+ if (publishing.routable) routableSlugs.add(post.slug);
4585
+ });
4586
+ return routableSlugs;
4587
+ }
3120
4588
  enableBuildTime() {
3121
4589
  this.isBuildTime = true;
3122
4590
  this.pluginRuntime.enableBuildTime();
@@ -3125,6 +4593,43 @@ var ContentManager = class {
3125
4593
  return this.pipelineOptions.observability ?? noopObservability;
3126
4594
  }
3127
4595
  };
4596
+ function readFrontmatter(markdown) {
4597
+ const file = new VFile2({ value: markdown });
4598
+ matter3(file);
4599
+ return file.data.matter ?? {};
4600
+ }
4601
+ function cloneManifestEntry(entry) {
4602
+ return {
4603
+ ...entry,
4604
+ publicLocation: { ...entry.publicLocation },
4605
+ frontmatter: { ...entry.frontmatter },
4606
+ tags: [...entry.tags],
4607
+ links: entry.links.map((link) => ({ ...link })),
4608
+ backlinks: [...entry.backlinks],
4609
+ assets: entry.assets.map((asset) => ({ ...asset })),
4610
+ ...entry.headTags ? { headTags: [...entry.headTags] } : {},
4611
+ ...entry.bodySlots ? { bodySlots: { ...entry.bodySlots } } : {}
4612
+ };
4613
+ }
4614
+ async function mapConcurrent3(values, concurrency, map) {
4615
+ const results = new Array(values.length);
4616
+ let nextIndex = 0;
4617
+ async function worker() {
4618
+ while (nextIndex < values.length) {
4619
+ const index = nextIndex;
4620
+ nextIndex += 1;
4621
+ const value = values[index];
4622
+ if (value !== void 0) results[index] = await map(value);
4623
+ }
4624
+ }
4625
+ await Promise.all(
4626
+ Array.from(
4627
+ { length: Math.min(concurrency, values.length) },
4628
+ () => worker()
4629
+ )
4630
+ );
4631
+ return results;
4632
+ }
3128
4633
 
3129
4634
  // src/content/content_source.ts
3130
4635
  async function readContentSourceEntry(source, logicalPath) {
@@ -3135,6 +4640,74 @@ async function getContentSourceEntry(source, logicalPath) {
3135
4640
  return (await source.scan()).find((candidate) => candidate.path === logicalPath) ?? null;
3136
4641
  }
3137
4642
 
4643
+ // src/content/folder_entry.ts
4644
+ function resolveFolderEntry(manifest, folder, options = {}) {
4645
+ const base = normalizeFolder2(folder);
4646
+ const scope = options.scope ?? "routable";
4647
+ const candidates = [];
4648
+ for (const [kind, slug] of folderEntrySlugs(base)) {
4649
+ if (slug === null) continue;
4650
+ const entry = manifest.bySlug.get(slug);
4651
+ if (!entry || !isInScope(entry, scope)) continue;
4652
+ candidates.push({ kind, entry });
4653
+ }
4654
+ if (candidates.length === 0) return { type: "none" };
4655
+ const candidate = candidates[0];
4656
+ if (candidates.length === 1 && candidate) {
4657
+ return { type: "resolved", kind: candidate.kind, entry: candidate.entry };
4658
+ }
4659
+ return { type: "ambiguous", candidates };
4660
+ }
4661
+ function resolveFolderLocation(manifest, folder) {
4662
+ const resolution = resolveFolderEntry(manifest, folder);
4663
+ const candidates = resolution.type === "ambiguous" ? resolution.candidates.filter((candidate) => candidate.kind !== "folder") : resolution.type === "resolved" && resolution.kind !== "folder" ? [{ kind: resolution.kind, entry: resolution.entry }] : [];
4664
+ if (candidates.length === 1) {
4665
+ const candidate = candidates[0];
4666
+ if (candidate) return { type: "content", entry: candidate.entry };
4667
+ }
4668
+ if (candidates.length > 1) return { type: "ambiguous", candidates };
4669
+ const location = manifest.folderLocations.get(normalizeFolder2(folder));
4670
+ return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4671
+ }
4672
+ function resolveGeneratedFolderLocation(manifest, folder) {
4673
+ const normalizedFolder = normalizeFolder2(folder);
4674
+ if (!normalizedFolder) return null;
4675
+ const candidates = /* @__PURE__ */ new Set();
4676
+ const prefix = `${normalizedFolder}/`;
4677
+ for (const entry of manifest.publicEntries) {
4678
+ if (!entry.slug.startsWith(prefix)) continue;
4679
+ const remainder = entry.slug.slice(prefix.length).split("/").filter(Boolean);
4680
+ const pathname = parentPath(
4681
+ entry.publicLocation.permalink,
4682
+ remainder.length
4683
+ );
4684
+ if (pathname) candidates.add(pathname);
4685
+ }
4686
+ return candidates.size === 1 ? [...candidates][0] ?? null : null;
4687
+ }
4688
+ function folderEntrySlugs(base) {
4689
+ const prefix = base === "" ? "" : `${base}/`;
4690
+ return [
4691
+ ["readme", `${prefix}README`],
4692
+ ["index", `${prefix}index`],
4693
+ ["folder", base === "" ? null : base]
4694
+ ];
4695
+ }
4696
+ function isInScope(entry, scope) {
4697
+ if (scope === "all") return true;
4698
+ if (scope === "discoverable") return entry.publishing.discoverable;
4699
+ return entry.publishing.routable;
4700
+ }
4701
+ function normalizeFolder2(folder) {
4702
+ return folder.split("/").filter(Boolean).join("/");
4703
+ }
4704
+ function parentPath(pathname, levels) {
4705
+ const segments = pathname.split("/").filter(Boolean);
4706
+ if (segments.length < levels) return null;
4707
+ const parent = segments.slice(0, -levels);
4708
+ return parent.length === 0 ? "/" : `/${parent.join("/")}/`;
4709
+ }
4710
+
3138
4711
  // src/content/graph_layout.ts
3139
4712
  function buildGraphEdges(nodes, visibleSlugs = new Set(nodes.map((node) => node.slug))) {
3140
4713
  const edges = /* @__PURE__ */ new Map();
@@ -3171,9 +4744,143 @@ function layoutRadialGraph(nodes, options) {
3171
4744
  });
3172
4745
  return layout;
3173
4746
  }
4747
+ function layoutForceGraph(nodes, options) {
4748
+ const safeOptions = resolveForceGraphLayoutOptions(options);
4749
+ const layoutNodes = [...nodes].sort((a, b) => a.slug.localeCompare(b.slug));
4750
+ const layout = createInitialLayout(layoutNodes, safeOptions);
4751
+ const visibleSlugs = new Set(layoutNodes.map((node) => node.slug));
4752
+ const edges = buildGraphEdges(layoutNodes, visibleSlugs).filter(
4753
+ (edge) => edge.source !== edge.target
4754
+ );
4755
+ const velocities = new Map(
4756
+ layoutNodes.map((node) => [node.slug, { x: 0, y: 0 }])
4757
+ );
4758
+ const centerX = safeOptions.width / 2;
4759
+ const centerY = safeOptions.height / 2;
4760
+ for (let iteration = 0; iteration < safeOptions.iterations; iteration++) {
4761
+ const alpha = 1 - iteration / safeOptions.iterations;
4762
+ applyRepulsion(
4763
+ layoutNodes,
4764
+ layout,
4765
+ velocities,
4766
+ safeOptions.repulsion * alpha
4767
+ );
4768
+ applyLinks(edges, layout, velocities, safeOptions.linkDistance, alpha);
4769
+ applyCentering(layoutNodes, layout, velocities, centerX, centerY, alpha);
4770
+ integrate(
4771
+ layoutNodes,
4772
+ layout,
4773
+ velocities,
4774
+ safeOptions,
4775
+ safeOptions.damping
4776
+ );
4777
+ }
4778
+ return layout;
4779
+ }
4780
+ function resolveForceGraphLayoutOptions(options) {
4781
+ const minNodeRadius = clampFinite(options.minNodeRadius ?? 8, 1, 64);
4782
+ const maxNodeRadius = Math.max(
4783
+ minNodeRadius,
4784
+ clampFinite(options.maxNodeRadius ?? 14, minNodeRadius, 96)
4785
+ );
4786
+ return {
4787
+ width: clampFinite(options.width, maxNodeRadius * 2, 2e4),
4788
+ height: clampFinite(options.height, maxNodeRadius * 2, 2e4),
4789
+ centerSlug: options.centerSlug,
4790
+ linkDistance: clampFinite(options.linkDistance ?? 84, 1, 2e3),
4791
+ repulsion: clampFinite(options.repulsion ?? 1800, 0, 1e5),
4792
+ iterations: Math.round(clampFinite(options.iterations ?? 160, 0, 1e3)),
4793
+ damping: clampFinite(options.damping ?? 0.82, 0, 1),
4794
+ minNodeRadius,
4795
+ maxNodeRadius
4796
+ };
4797
+ }
4798
+ function createInitialLayout(nodes, options) {
4799
+ return layoutRadialGraph(nodes, {
4800
+ width: options.width,
4801
+ height: options.height,
4802
+ centerSlug: options.centerSlug ?? nodes[0]?.slug ?? "",
4803
+ radiusRatio: 0.28,
4804
+ minNodeRadius: options.minNodeRadius,
4805
+ maxNodeRadius: options.maxNodeRadius
4806
+ });
4807
+ }
4808
+ function applyRepulsion(nodes, layout, velocities, strength) {
4809
+ for (let sourceIndex = 0; sourceIndex < nodes.length; sourceIndex++) {
4810
+ for (let targetIndex = sourceIndex + 1; targetIndex < nodes.length; targetIndex++) {
4811
+ const source = layout.get(nodes[sourceIndex]?.slug ?? "");
4812
+ const target = layout.get(nodes[targetIndex]?.slug ?? "");
4813
+ const sourceVelocity = velocities.get(nodes[sourceIndex]?.slug ?? "");
4814
+ const targetVelocity = velocities.get(nodes[targetIndex]?.slug ?? "");
4815
+ if (!source || !target || !sourceVelocity || !targetVelocity) continue;
4816
+ const dx = target.x - source.x || 0.01;
4817
+ const dy = target.y - source.y || 0.01;
4818
+ const distanceSquared = Math.max(dx * dx + dy * dy, 64);
4819
+ const force = strength / distanceSquared;
4820
+ const distance = Math.sqrt(distanceSquared);
4821
+ const fx = dx / distance * force;
4822
+ const fy = dy / distance * force;
4823
+ sourceVelocity.x -= fx;
4824
+ sourceVelocity.y -= fy;
4825
+ targetVelocity.x += fx;
4826
+ targetVelocity.y += fy;
4827
+ }
4828
+ }
4829
+ }
4830
+ function applyLinks(edges, layout, velocities, linkDistance, alpha) {
4831
+ for (const edge of edges) {
4832
+ const source = layout.get(edge.source);
4833
+ const target = layout.get(edge.target);
4834
+ const sourceVelocity = velocities.get(edge.source);
4835
+ const targetVelocity = velocities.get(edge.target);
4836
+ if (!source || !target || !sourceVelocity || !targetVelocity) continue;
4837
+ const dx = target.x - source.x || 0.01;
4838
+ const dy = target.y - source.y || 0.01;
4839
+ const distance = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
4840
+ const force = (distance - linkDistance) * 0.018 * alpha;
4841
+ const fx = dx / distance * force;
4842
+ const fy = dy / distance * force;
4843
+ sourceVelocity.x += fx;
4844
+ sourceVelocity.y += fy;
4845
+ targetVelocity.x -= fx;
4846
+ targetVelocity.y -= fy;
4847
+ }
4848
+ }
4849
+ function applyCentering(nodes, layout, velocities, centerX, centerY, alpha) {
4850
+ for (const node of nodes) {
4851
+ const point = layout.get(node.slug);
4852
+ const velocity = velocities.get(node.slug);
4853
+ if (!point || !velocity) continue;
4854
+ velocity.x += (centerX - point.x) * 4e-3 * alpha;
4855
+ velocity.y += (centerY - point.y) * 4e-3 * alpha;
4856
+ }
4857
+ }
4858
+ function integrate(nodes, layout, velocities, options, damping) {
4859
+ for (const node of nodes) {
4860
+ const point = layout.get(node.slug);
4861
+ const velocity = velocities.get(node.slug);
4862
+ if (!point || !velocity) continue;
4863
+ velocity.x *= damping;
4864
+ velocity.y *= damping;
4865
+ point.x = clamp(
4866
+ point.x + velocity.x,
4867
+ point.radius,
4868
+ options.width - point.radius
4869
+ );
4870
+ point.y = clamp(
4871
+ point.y + velocity.y,
4872
+ point.radius,
4873
+ options.height - point.radius
4874
+ );
4875
+ }
4876
+ }
3174
4877
  function clamp(value, min, max) {
3175
4878
  return Math.min(Math.max(value, min), max);
3176
4879
  }
4880
+ function clampFinite(value, min, max) {
4881
+ if (!Number.isFinite(value)) return min;
4882
+ return clamp(value, min, max);
4883
+ }
3177
4884
 
3178
4885
  // src/content/read_only_content_graph.ts
3179
4886
  async function readOnlyContentGraph(source, locations) {
@@ -3183,7 +4890,7 @@ async function readOnlyContentGraph(source, locations) {
3183
4890
  const markdownContents = await Promise.all(
3184
4891
  markdownEntries.map(async (entry) => ({
3185
4892
  entry,
3186
- markdown: await readText2(source, entry)
4893
+ markdown: await readText3(source, entry)
3187
4894
  }))
3188
4895
  );
3189
4896
  const graphEntries = markdownContents.map(
@@ -3203,6 +4910,13 @@ function createGraphEntry(entry, markdown, contentIndex, locations) {
3203
4910
  publicLocation: location,
3204
4911
  title: slug,
3205
4912
  frontmatter: {},
4913
+ publishing: resolvePublishingState(
4914
+ {},
4915
+ {
4916
+ strategy: "selective",
4917
+ buildTime: /* @__PURE__ */ new Date(0)
4918
+ }
4919
+ ),
3206
4920
  html: "",
3207
4921
  tags: [],
3208
4922
  links: extractContentLinks(markdown, contentIndex),
@@ -3210,60 +4924,11 @@ function createGraphEntry(entry, markdown, contentIndex, locations) {
3210
4924
  assets: []
3211
4925
  };
3212
4926
  }
3213
- async function readText2(source, entry) {
4927
+ async function readText3(source, entry) {
3214
4928
  const content = await source.read(entry);
3215
4929
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3216
4930
  }
3217
4931
 
3218
- // src/plugin/plugin_memo.ts
3219
- import { createHash as createHash3 } from "node:crypto";
3220
- function createPluginMemo(cache, options = {}) {
3221
- return {
3222
- async memo(key, dependencies, compute) {
3223
- const memoKey = `${key}:${hash2(stableStringify(dependencies))}`;
3224
- try {
3225
- const cached = await cache.get(memoKey);
3226
- if (cached !== void 0) return cached;
3227
- } catch (error) {
3228
- warn(
3229
- options.logger,
3230
- "Plugin cache read failed; recomputing.",
3231
- key,
3232
- error
3233
- );
3234
- }
3235
- const value = await compute();
3236
- try {
3237
- await cache.set(memoKey, value);
3238
- } catch (error) {
3239
- warn(
3240
- options.logger,
3241
- "Plugin cache write failed; result not cached.",
3242
- key,
3243
- error
3244
- );
3245
- }
3246
- return value;
3247
- }
3248
- };
3249
- }
3250
- function stableStringify(value) {
3251
- if (value === null || typeof value !== "object") return JSON.stringify(value);
3252
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
3253
- const keys = Object.keys(value).sort();
3254
- const body = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",");
3255
- return `{${body}}`;
3256
- }
3257
- function hash2(value) {
3258
- return createHash3("sha256").update(value).digest("hex");
3259
- }
3260
- function warn(logger, message, key, error) {
3261
- logger?.warn(message, {
3262
- memoKey: key,
3263
- error: error instanceof Error ? error.message : String(error)
3264
- });
3265
- }
3266
-
3267
4932
  // src/types/content_manifest.ts
3268
4933
  function appendContentBodySlot(entry, slot, html) {
3269
4934
  if (!html.trim()) return;
@@ -3276,12 +4941,12 @@ ${html}` : html
3276
4941
  }
3277
4942
 
3278
4943
  // src/types/plugin_endpoint.ts
3279
- function defineEndpoint(path4, handler, options = {}) {
4944
+ function defineEndpoint(path5, handler, options = {}) {
3280
4945
  if (options.cacheControl === void 0) {
3281
- return { path: path4, handler };
4946
+ return { path: path5, handler };
3282
4947
  }
3283
4948
  return {
3284
- path: path4,
4949
+ path: path5,
3285
4950
  handler: async (context) => {
3286
4951
  const response = await handler(context);
3287
4952
  const headers = { ...response.headers };
@@ -3323,6 +4988,7 @@ export {
3323
4988
  ContentManager,
3324
4989
  FileSystemContentSource,
3325
4990
  IMAGE_EXTENSIONS,
4991
+ INTERNAL_CONTENT_IGNORE_PATTERNS,
3326
4992
  NoopLogger,
3327
4993
  NoopTracer,
3328
4994
  Pipeline,
@@ -3332,9 +4998,12 @@ export {
3332
4998
  attachmentUrl,
3333
4999
  buildContentCollections,
3334
5000
  buildGraphEdges,
5001
+ buildOutputInventory,
3335
5002
  calculateReadingTime,
5003
+ collectConfigDeprecationDiagnostics,
3336
5004
  createClientEntry,
3337
5005
  createContentGraph,
5006
+ createDeprecationDiagnostic,
3338
5007
  createPluginMemo,
3339
5008
  createStyleAsset,
3340
5009
  createUnavailableGeneratedOutputSink,
@@ -3342,23 +5011,29 @@ export {
3342
5011
  defineEndpoint,
3343
5012
  definePlugin,
3344
5013
  defineTheme,
5014
+ determineOutputChanges,
3345
5015
  escapeHtml,
3346
5016
  escapeHtmlAttribute,
3347
5017
  escapeScriptJson,
3348
5018
  extractFrontmatterAliases,
3349
5019
  fingerprintContentEntries,
5020
+ formatDeprecationMessage,
3350
5021
  getContentSourceEntry,
3351
5022
  getExtension,
3352
5023
  getResolvedPluginMetadata,
3353
5024
  groupContentEntries,
5025
+ htmlOutputPath,
3354
5026
  isAttachmentPath,
3355
5027
  isExcluded,
5028
+ isIgnoredContentPath,
3356
5029
  isImagePath,
3357
5030
  isMarkdownPath,
3358
5031
  isPublishable,
3359
5032
  isPublished,
5033
+ layoutForceGraph,
3360
5034
  layoutRadialGraph,
3361
5035
  loadContentBuildState,
5036
+ normalizeContentAssetOutputPath,
3362
5037
  normalizeContentPath,
3363
5038
  normalizeGeneratedOutputPath,
3364
5039
  normalizeTag,
@@ -3373,7 +5048,12 @@ export {
3373
5048
  resolveContentQueryPagination,
3374
5049
  resolveContentStableId,
3375
5050
  resolveDefaultContentLocation,
5051
+ resolveFolderEntry,
5052
+ resolveFolderLocation,
5053
+ resolveGeneratedFolderLocation,
3376
5054
  resolvePlugins,
5055
+ resolvePublishingBuildTime,
5056
+ resolvePublishingState,
3377
5057
  serializePublicClientConfig,
3378
5058
  stableStringify,
3379
5059
  stripHtml,