@riebeckite/core 0.0.14 → 0.0.16

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 (34) 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 +14 -6
  6. package/dist/index.js +2149 -439
  7. package/dist/src/content/affected_content.d.ts +1 -1
  8. package/dist/src/content/content_build_coordinator.d.ts +11 -3
  9. package/dist/src/content/content_build_state.d.ts +13 -2
  10. package/dist/src/content/content_dependency_tracker.d.ts +18 -0
  11. package/dist/src/content/content_entry_reader.d.ts +2 -0
  12. package/dist/src/content/content_index_builder.d.ts +2 -0
  13. package/dist/src/content/content_location_resolver.d.ts +7 -0
  14. package/dist/src/content/content_manager.d.ts +21 -0
  15. package/dist/src/content/content_persistent_cache.d.ts +43 -0
  16. package/dist/src/content/file_system_content_source.d.ts +2 -0
  17. package/dist/src/content/folder_entry.d.ts +36 -0
  18. package/dist/src/content/graph_layout.d.ts +20 -0
  19. package/dist/src/content/manifest_builder.d.ts +2 -1
  20. package/dist/src/content/output_dependency.d.ts +34 -0
  21. package/dist/src/content/publishing.d.ts +15 -0
  22. package/dist/src/pipeline.d.ts +10 -0
  23. package/dist/src/plugin/plugin_runtime.d.ts +7 -1
  24. package/dist/src/types/content_manifest.d.ts +19 -1
  25. package/dist/src/types/generated_output.d.ts +9 -0
  26. package/dist/src/types/navigation_config.d.ts +10 -0
  27. package/dist/src/types/output_dependency.d.ts +14 -0
  28. package/dist/src/types/plugin.d.ts +19 -0
  29. package/dist/src/types/plugin_page.d.ts +6 -0
  30. package/dist/src/types/plugin_pipeline.d.ts +1 -0
  31. package/dist/src/types/post_content.d.ts +2 -0
  32. package/dist/src/types/resolved_riebeckite_config.d.ts +6 -0
  33. package/dist/src/types/riebeckite_config.d.ts +6 -0
  34. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -18,11 +18,55 @@ function validateConfigIssues(config) {
18
18
  return [{ path: "config", message: "Expected an object." }];
19
19
  }
20
20
  validateSite(value.site, issues);
21
+ validateNavigation(value.navigation, issues);
21
22
  validateContent(value.content, issues);
22
23
  validateTheme(value.theme, issues);
23
24
  validatePlugins(value.plugins, issues);
24
25
  return issues;
25
26
  }
27
+ function validateNavigation(value, issues) {
28
+ if (value === void 0) return;
29
+ if (!isRecord(value)) {
30
+ issues.push({ path: "navigation", message: "Expected an object." });
31
+ return;
32
+ }
33
+ validateNavigationItems(value.header, "navigation.header", issues, /* @__PURE__ */ new Set());
34
+ validateNavigationItems(value.footer, "navigation.footer", issues, /* @__PURE__ */ new Set());
35
+ }
36
+ function validateNavigationItems(value, path5, issues, ancestors) {
37
+ if (value === void 0) return;
38
+ if (!Array.isArray(value)) {
39
+ issues.push({ path: path5, message: "Expected an array." });
40
+ return;
41
+ }
42
+ for (const [index, item] of value.entries()) {
43
+ const itemPath = `${path5}[${index}]`;
44
+ if (!isRecord(item)) {
45
+ issues.push({ path: itemPath, message: "Expected an object." });
46
+ continue;
47
+ }
48
+ if (ancestors.has(item)) {
49
+ issues.push({
50
+ path: itemPath,
51
+ message: "Navigation children must not be recursive."
52
+ });
53
+ continue;
54
+ }
55
+ validateRequiredString(item.label, `${itemPath}.label`, issues);
56
+ validateRequiredString(item.href, `${itemPath}.href`, issues);
57
+ validateOptionalBoolean(item.external, `${itemPath}.external`, issues);
58
+ if (item.children !== void 0) {
59
+ const nextAncestors = new Set(ancestors);
60
+ nextAncestors.add(item);
61
+ validateNavigationItems(
62
+ item.children,
63
+ `${itemPath}.children`,
64
+ issues,
65
+ nextAncestors
66
+ );
67
+ }
68
+ }
69
+ }
26
70
  function validateSite(value, issues) {
27
71
  if (!isRecord(value)) {
28
72
  issues.push({ path: "site", message: "Expected an object." });
@@ -109,41 +153,41 @@ function validateTheme(value, issues) {
109
153
  }
110
154
  }
111
155
  }
112
- function validateThemeConfig(value, path4, issues) {
113
- validateOptionalString(value.name, `${path4}.name`, issues);
156
+ function validateThemeConfig(value, path5, issues) {
157
+ validateOptionalString(value.name, `${path5}.name`, issues);
114
158
  validateOneOf(
115
159
  value.colorMode,
116
- `${path4}.colorMode`,
160
+ `${path5}.colorMode`,
117
161
  ["light", "dark", "system"],
118
162
  issues
119
163
  );
120
164
  validateOneOf(
121
165
  value.typography,
122
- `${path4}.typography`,
166
+ `${path5}.typography`,
123
167
  ["system", "serif", "sans"],
124
168
  issues
125
169
  );
126
170
  validateOneOf(
127
171
  value.articleLayout,
128
- `${path4}.articleLayout`,
172
+ `${path5}.articleLayout`,
129
173
  ["article", "sidebar", "full-width"],
130
174
  issues
131
175
  );
132
- validateStringArray(value.userCss, `${path4}.userCss`, issues);
176
+ validateStringArray(value.userCss, `${path5}.userCss`, issues);
133
177
  if (value.attributes !== void 0 && !isRecord(value.attributes)) {
134
- issues.push({ path: `${path4}.attributes`, message: "Expected an object." });
178
+ issues.push({ path: `${path5}.attributes`, message: "Expected an object." });
135
179
  }
136
180
  }
137
- function validateThemeStyles(value, path4, issues) {
181
+ function validateThemeStyles(value, path5, issues) {
138
182
  if (value === void 0) return;
139
183
  if (!Array.isArray(value)) {
140
- issues.push({ path: path4, message: "Expected an array." });
184
+ issues.push({ path: path5, message: "Expected an array." });
141
185
  return;
142
186
  }
143
187
  for (const [index, style] of value.entries()) {
144
188
  if (!isRecord(style) || !isNonEmptyString(style.moduleSpecifier)) {
145
189
  issues.push({
146
- path: `${path4}[${index}].moduleSpecifier`,
190
+ path: `${path5}[${index}].moduleSpecifier`,
147
191
  message: "Expected a non-empty string."
148
192
  });
149
193
  }
@@ -163,69 +207,69 @@ function validatePlugins(value, issues) {
163
207
  continue;
164
208
  }
165
209
  const plugin = input;
166
- const path4 = `plugins[${index}]`;
210
+ const path5 = `plugins[${index}]`;
167
211
  if (!isNonEmptyString(plugin.name)) {
168
212
  issues.push({
169
- path: `${path4}.name`,
213
+ path: `${path5}.name`,
170
214
  message: "Expected a non-empty string."
171
215
  });
172
216
  } else if (plugin.enabled !== false) {
173
217
  const duplicateIndex = names.get(plugin.name);
174
218
  if (duplicateIndex !== void 0) {
175
219
  issues.push({
176
- path: `${path4}.name`,
220
+ path: `${path5}.name`,
177
221
  message: `Duplicate plugin name "${plugin.name}" (also plugins[${duplicateIndex}]).`
178
222
  });
179
223
  } else {
180
224
  names.set(plugin.name, index);
181
225
  }
182
226
  }
183
- validateOptionalBoolean(plugin.enabled, `${path4}.enabled`, issues);
184
- validateCapabilityMetadata(plugin, path4, issues);
185
- if (plugin.enabled !== false) validatePluginOptions(plugin, path4, issues);
227
+ validateOptionalBoolean(plugin.enabled, `${path5}.enabled`, issues);
228
+ validateCapabilityMetadata(plugin, path5, issues);
229
+ if (plugin.enabled !== false) validatePluginOptions(plugin, path5, issues);
186
230
  }
187
231
  }
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);
232
+ function validateCapabilityMetadata(plugin, path5, issues) {
233
+ validateStringArray(plugin.provides, `${path5}.provides`, issues, true);
234
+ validateStringArray(plugin.requires, `${path5}.requires`, issues, true);
235
+ validateStringArray(plugin.optional, `${path5}.optional`, issues, true);
192
236
  }
193
- function validatePluginOptions(plugin, path4, issues) {
237
+ function validatePluginOptions(plugin, path5, issues) {
194
238
  if (!plugin.validateOptions) return;
195
239
  try {
196
240
  const optionIssues = plugin.validateOptions(plugin.options) ?? [];
197
241
  for (const issue of optionIssues) {
198
242
  issues.push({
199
- path: issue.path ? `${path4}.options.${issue.path}` : `${path4}.options`,
243
+ path: issue.path ? `${path5}.options.${issue.path}` : `${path5}.options`,
200
244
  message: issue.message
201
245
  });
202
246
  }
203
247
  } catch (error) {
204
248
  issues.push({
205
- path: `${path4}.options`,
249
+ path: `${path5}.options`,
206
250
  message: `Validator failed: ${toErrorMessage(error)}`
207
251
  });
208
252
  }
209
253
  }
210
- function validateRequiredString(value, path4, issues) {
254
+ function validateRequiredString(value, path5, issues) {
211
255
  if (!isNonEmptyString(value)) {
212
- issues.push({ path: path4, message: "Expected a non-empty string." });
256
+ issues.push({ path: path5, message: "Expected a non-empty string." });
213
257
  }
214
258
  }
215
- function validateOptionalString(value, path4, issues) {
259
+ function validateOptionalString(value, path5, issues) {
216
260
  if (value !== void 0 && typeof value !== "string") {
217
- issues.push({ path: path4, message: "Expected a string." });
261
+ issues.push({ path: path5, message: "Expected a string." });
218
262
  }
219
263
  }
220
- function validateOptionalBoolean(value, path4, issues) {
264
+ function validateOptionalBoolean(value, path5, issues) {
221
265
  if (value !== void 0 && typeof value !== "boolean") {
222
- issues.push({ path: path4, message: "Expected a boolean." });
266
+ issues.push({ path: path5, message: "Expected a boolean." });
223
267
  }
224
268
  }
225
- function validateAbsoluteUrl(value, path4, issues) {
269
+ function validateAbsoluteUrl(value, path5, issues) {
226
270
  if (value === void 0 || value === "") return;
227
271
  if (typeof value !== "string") {
228
- issues.push({ path: path4, message: "Expected an absolute URL." });
272
+ issues.push({ path: path5, message: "Expected an absolute URL." });
229
273
  return;
230
274
  }
231
275
  try {
@@ -233,27 +277,27 @@ function validateAbsoluteUrl(value, path4, issues) {
233
277
  if (url.protocol !== "http:" && url.protocol !== "https:")
234
278
  throw new Error();
235
279
  } catch {
236
- issues.push({ path: path4, message: "Expected an absolute HTTP(S) URL." });
280
+ issues.push({ path: path5, message: "Expected an absolute HTTP(S) URL." });
237
281
  }
238
282
  }
239
- function validateStringArray(value, path4, issues, nonEmpty = false) {
283
+ function validateStringArray(value, path5, issues, nonEmpty = false) {
240
284
  if (value === void 0) return;
241
285
  if (!Array.isArray(value)) {
242
- issues.push({ path: path4, message: "Expected an array." });
286
+ issues.push({ path: path5, message: "Expected an array." });
243
287
  return;
244
288
  }
245
289
  for (const [index, item] of value.entries()) {
246
290
  if (typeof item !== "string" || nonEmpty && !item.trim()) {
247
291
  issues.push({
248
- path: `${path4}[${index}]`,
292
+ path: `${path5}[${index}]`,
249
293
  message: nonEmpty ? "Expected a non-empty string." : "Expected a string."
250
294
  });
251
295
  }
252
296
  }
253
297
  }
254
- function validateOneOf(value, path4, choices, issues) {
298
+ function validateOneOf(value, path5, choices, issues) {
255
299
  if (value !== void 0 && (typeof value !== "string" || !choices.includes(value))) {
256
- issues.push({ path: path4, message: `Expected one of: ${choices.join(", ")}.` });
300
+ issues.push({ path: path5, message: `Expected one of: ${choices.join(", ")}.` });
257
301
  }
258
302
  }
259
303
  function formatValidationIssues(issues) {
@@ -538,8 +582,21 @@ function definePlugin(plugin) {
538
582
  }
539
583
  function resolvePlugins(plugins = []) {
540
584
  const orderedPlugins = plugins.filter((plugin) => Boolean(plugin)).filter((plugin) => plugin.enabled !== false).toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
585
+ assertUniquePluginNames(orderedPlugins);
541
586
  return resolvePluginDependencies(orderedPlugins);
542
587
  }
588
+ function assertUniquePluginNames(plugins) {
589
+ const owners = /* @__PURE__ */ new Map();
590
+ plugins.forEach((plugin, index) => {
591
+ const previous = owners.get(plugin.name);
592
+ if (previous !== void 0) {
593
+ throw new Error(
594
+ `Plugin name "${plugin.name}" is used by both plugins[${previous}] and plugins[${index}]`
595
+ );
596
+ }
597
+ owners.set(plugin.name, index);
598
+ });
599
+ }
543
600
  function getResolvedPluginMetadata(plugins) {
544
601
  return plugins.map((plugin) => ({
545
602
  name: plugin.name,
@@ -612,6 +669,10 @@ function resolveConfig(config) {
612
669
  language: config.site?.feed?.language ?? config.site?.locale ?? "en"
613
670
  }
614
671
  },
672
+ navigation: {
673
+ header: config.navigation?.header ?? [],
674
+ footer: config.navigation?.footer ?? []
675
+ },
615
676
  content: {
616
677
  directory: config.content?.directory ?? "../../content",
617
678
  source: config.content?.source,
@@ -626,7 +687,11 @@ function resolveConfig(config) {
626
687
  }
627
688
  },
628
689
  theme,
629
- plugins: resolvePlugins(config.plugins)
690
+ plugins: resolvePlugins(config.plugins),
691
+ cache: {
692
+ enabled: config.cache?.enabled ?? true,
693
+ directory: config.cache?.directory ?? ""
694
+ }
630
695
  };
631
696
  }
632
697
  function resolveThemeConfig(themeInput) {
@@ -659,6 +724,8 @@ function sanitizeThemeAttributes(attributes) {
659
724
  );
660
725
  }
661
726
  function isPublished(config, frontmatter) {
727
+ if (frontmatter?.visibility === "unlisted") return true;
728
+ if (frontmatter?.visibility === "draft") return false;
662
729
  return isPublishable(config.content.filters.publishStrategy, frontmatter);
663
730
  }
664
731
  function isExcluded(patterns, relativePath) {
@@ -721,9 +788,9 @@ var configDeprecations = [];
721
788
  function collectConfigDeprecationDiagnostics(config, notices = configDeprecations) {
722
789
  return notices.filter((notice) => hasConfigPath(config, notice.path)).map(createDeprecationDiagnostic);
723
790
  }
724
- function hasConfigPath(config, path4) {
791
+ function hasConfigPath(config, path5) {
725
792
  let value = config;
726
- for (const segment of path4.split(".")) {
793
+ for (const segment of path5.split(".")) {
727
794
  if (!isRecord2(value) || !(segment in value)) return false;
728
795
  value = value[segment];
729
796
  }
@@ -786,7 +853,7 @@ import fs from "node:fs/promises";
786
853
  import path from "node:path";
787
854
 
788
855
  // src/content/content_build_state.ts
789
- var CONTENT_BUILD_STATE_VERSION = 2;
856
+ var CONTENT_BUILD_STATE_VERSION = 6;
790
857
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
791
858
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
792
859
 
@@ -826,12 +893,12 @@ async function readContentBuildStateStatus(statePath) {
826
893
  entryCount: Object.keys(parsed.entries).length
827
894
  };
828
895
  }
829
- async function saveContentBuildState(statePath, state) {
896
+ async function saveContentBuildState(statePath, state2) {
830
897
  const directory = path.dirname(statePath);
831
898
  const temporaryPath = `${statePath}.${randomUUID()}.tmp`;
832
899
  await fs.mkdir(directory, { recursive: true });
833
900
  try {
834
- await fs.writeFile(temporaryPath, JSON.stringify(state), "utf8");
901
+ await fs.writeFile(temporaryPath, JSON.stringify(state2), "utf8");
835
902
  await fs.rename(temporaryPath, statePath);
836
903
  } catch (error) {
837
904
  await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -853,15 +920,33 @@ function isContentBuildState(value) {
853
920
  return isRecord3(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
854
921
  }
855
922
  function isContentBuildStateShape(value) {
856
- if (!isRecord3(value.entries) || !isRecord3(value.contentIndex)) return false;
923
+ if (!isRecord3(value.entries) || !isRecord3(value.contentIndex) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs)) {
924
+ return false;
925
+ }
857
926
  return Object.values(value.entries).every(
858
- (entry) => isRecord3(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.dependencies) && entry.dependencies.every(
859
- (dependency) => typeof dependency === "string"
927
+ (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(
928
+ (dependency) => isRecord3(dependency) && (dependency.kind === "content" || dependency.kind === "file") && typeof dependency.id === "string"
860
929
  )
861
930
  ) && Object.values(value.contentIndex).every(
862
931
  (entry) => typeof entry === "string"
863
932
  );
864
933
  }
934
+ function isOutputDescriptors(value) {
935
+ return Array.isArray(value) && value.every(
936
+ (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)
937
+ );
938
+ }
939
+ function isOutputPath(value) {
940
+ if (typeof value !== "string" || value.length === 0 || value.includes("\\") || value.includes("\0") || value.startsWith("/")) {
941
+ return false;
942
+ }
943
+ return value.split("/").every((segment) => segment !== "." && segment !== "..");
944
+ }
945
+ function isOutputDependency(value) {
946
+ if (!isRecord3(value) || typeof value.type !== "string") return false;
947
+ if (value.type === "global" || value.type === "unknown") return true;
948
+ return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string";
949
+ }
865
950
  function isRecord3(value) {
866
951
  return typeof value === "object" && value !== null && !Array.isArray(value);
867
952
  }
@@ -1173,9 +1258,9 @@ function buildContentCollections(entries, definitions) {
1173
1258
  function normalizePageSize(value) {
1174
1259
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1175
1260
  }
1176
- function withPage(path4, index) {
1177
- if (index === 0) return path4;
1178
- const base = path4 === "/" ? "" : path4;
1261
+ function withPage(path5, index) {
1262
+ if (index === 0) return path5;
1263
+ const base = path5 === "/" ? "" : path5;
1179
1264
  return normalizePath(`${base}/page/${index + 1}`);
1180
1265
  }
1181
1266
  function defaultPath(context) {
@@ -1203,12 +1288,32 @@ function normalizePath(value) {
1203
1288
  // src/content/content_fingerprint.ts
1204
1289
  import { createHash } from "node:crypto";
1205
1290
  async function fingerprintContentEntries(entries, read) {
1206
- return await Promise.all(
1207
- [...entries].sort((left, right) => left.path.localeCompare(right.path)).map(async (entry) => ({
1208
- entry,
1209
- fingerprint: await fingerprintContentEntry(entry, read)
1210
- }))
1291
+ const sortedEntries = [...entries].sort(
1292
+ (left, right) => left.path.localeCompare(right.path)
1293
+ );
1294
+ return await mapConcurrent(sortedEntries, 64, async (entry) => ({
1295
+ entry,
1296
+ fingerprint: await fingerprintContentEntry(entry, read)
1297
+ }));
1298
+ }
1299
+ async function mapConcurrent(values, concurrency, map) {
1300
+ const results = new Array(values.length);
1301
+ let nextIndex = 0;
1302
+ async function worker() {
1303
+ while (nextIndex < values.length) {
1304
+ const index = nextIndex;
1305
+ nextIndex += 1;
1306
+ const value = values[index];
1307
+ if (value !== void 0) results[index] = await map(value);
1308
+ }
1309
+ }
1310
+ await Promise.all(
1311
+ Array.from(
1312
+ { length: Math.min(concurrency, values.length) },
1313
+ () => worker()
1314
+ )
1211
1315
  );
1316
+ return results;
1212
1317
  }
1213
1318
  async function fingerprintContentEntry(entry, read) {
1214
1319
  if (entry.metadata?.hash) return `hash:${entry.metadata.hash}`;
@@ -1273,6 +1378,10 @@ function resolveDefaultContentLocation(content) {
1273
1378
  };
1274
1379
  }
1275
1380
 
1381
+ // src/content/content_manager.ts
1382
+ import { VFile as VFile2 } from "vfile";
1383
+ import { matter as matter3 } from "vfile-matter";
1384
+
1276
1385
  // src/observability.ts
1277
1386
  var NoopLogger = class {
1278
1387
  debug() {
@@ -1414,12 +1523,334 @@ import remarkMath from "remark-math";
1414
1523
  import remarkParse from "remark-parse";
1415
1524
  import remarkRehype from "remark-rehype";
1416
1525
  import { unified } from "unified";
1417
- import { matter } from "vfile-matter";
1526
+ import { matter as matter2 } from "vfile-matter";
1418
1527
 
1419
- // src/plugin/plugin_cache.ts
1420
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1528
+ // src/content/content_dependency_tracker.ts
1529
+ import { createHash as createHash2 } from "node:crypto";
1530
+ function createContentDependencyTracker(source) {
1531
+ const dependencies = /* @__PURE__ */ new Map();
1532
+ const record = (identity, content) => {
1533
+ const normalized = normalizeDependencyIdentity(identity);
1534
+ dependencies.set(dependencyKey(normalized), {
1535
+ ...normalized,
1536
+ fingerprint: fingerprintContent(content)
1537
+ });
1538
+ };
1539
+ return {
1540
+ contentSource: {
1541
+ scan: () => source.scan(),
1542
+ async read(entry) {
1543
+ const content = await source.read(entry);
1544
+ record({ kind: "file", id: entry.path }, content);
1545
+ return content;
1546
+ }
1547
+ },
1548
+ async readContent(slug, read) {
1549
+ const content = await read();
1550
+ record({ kind: "content", id: slug }, content);
1551
+ return content;
1552
+ },
1553
+ recordLinkResolution(id, value) {
1554
+ record({ kind: "link", id }, value);
1555
+ },
1556
+ dependencies: () => [...dependencies.values()].toSorted(
1557
+ (left, right) => dependencyKey(left).localeCompare(dependencyKey(right))
1558
+ )
1559
+ };
1560
+ }
1561
+ function fingerprintContent(content) {
1562
+ return `sha256:${createHash2("sha256").update(typeof content === "string" ? content : content).digest("hex")}`;
1563
+ }
1564
+ function normalizeDependencyIdentity(dependency) {
1565
+ return {
1566
+ ...dependency,
1567
+ id: dependency.kind === "file" ? dependency.id.replace(/\\/g, "/") : dependency.id
1568
+ };
1569
+ }
1570
+ function dependencyKey(dependency) {
1571
+ const normalized = normalizeDependencyIdentity(dependency);
1572
+ return `${normalized.kind}:${normalized.id}`;
1573
+ }
1574
+
1575
+ // src/content/content_persistent_cache.ts
1576
+ import { createHash as createHash4 } from "node:crypto";
1421
1577
  import fs2 from "node:fs/promises";
1578
+ import os from "node:os";
1422
1579
  import path2 from "node:path";
1580
+ import { VFile } from "vfile";
1581
+ import { matter } from "vfile-matter";
1582
+
1583
+ // src/plugin/plugin_memo.ts
1584
+ import { createHash as createHash3 } from "node:crypto";
1585
+ function createPluginMemo(cache, options = {}) {
1586
+ return {
1587
+ async memo(key, dependencies, compute) {
1588
+ const memoKey = `${key}:${hash(stableStringify(dependencies))}`;
1589
+ try {
1590
+ const cached = await cache.get(memoKey);
1591
+ if (cached !== void 0) return cached;
1592
+ } catch (error) {
1593
+ warn(
1594
+ options.logger,
1595
+ "Plugin cache read failed; recomputing.",
1596
+ key,
1597
+ error
1598
+ );
1599
+ }
1600
+ const value = await compute();
1601
+ try {
1602
+ await cache.set(memoKey, value);
1603
+ } catch (error) {
1604
+ warn(
1605
+ options.logger,
1606
+ "Plugin cache write failed; result not cached.",
1607
+ key,
1608
+ error
1609
+ );
1610
+ }
1611
+ return value;
1612
+ }
1613
+ };
1614
+ }
1615
+ function stableStringify(value) {
1616
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1617
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
1618
+ const keys = Object.keys(value).sort();
1619
+ const body = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",");
1620
+ return `{${body}}`;
1621
+ }
1622
+ function hash(value) {
1623
+ return createHash3("sha256").update(value).digest("hex");
1624
+ }
1625
+ function warn(logger, message, key, error) {
1626
+ logger?.warn(message, {
1627
+ memoKey: key,
1628
+ error: error instanceof Error ? error.message : String(error)
1629
+ });
1630
+ }
1631
+
1632
+ // src/content/content_persistent_cache.ts
1633
+ var CONTENT_CACHE_NAMESPACE = "content";
1634
+ var CONTENT_CACHE_SCHEMA_VERSION = 3;
1635
+ var CONTENT_CACHE_COMPATIBILITY_VERSION = 1;
1636
+ function hash2(value) {
1637
+ return createHash4("sha256").update(value).digest("hex");
1638
+ }
1639
+ function cacheDirectory(config) {
1640
+ if (config.cache?.directory) {
1641
+ return path2.join(config.cache.directory, CONTENT_CACHE_NAMESPACE);
1642
+ }
1643
+ if (config.buildDirectory) {
1644
+ return path2.join(config.buildDirectory, "cache", CONTENT_CACHE_NAMESPACE);
1645
+ }
1646
+ return path2.join(
1647
+ os.tmpdir(),
1648
+ "riebeckite-content-cache",
1649
+ CONTENT_CACHE_NAMESPACE
1650
+ );
1651
+ }
1652
+ function versionDirectory(baseDir) {
1653
+ return path2.join(baseDir, `v${CONTENT_CACHE_SCHEMA_VERSION}`);
1654
+ }
1655
+ function cacheFilePath(baseDir, key) {
1656
+ return path2.join(versionDirectory(baseDir), `${hash2(key)}.json`);
1657
+ }
1658
+ async function writeAtomically(targetPath, dir, serialized) {
1659
+ await fs2.mkdir(dir, { recursive: true });
1660
+ const temporaryPath = `${targetPath}.${createHash4("sha256").update(`${Date.now()}-${Math.random()}`).digest("hex").slice(0, 12)}.tmp`;
1661
+ try {
1662
+ await fs2.writeFile(temporaryPath, serialized, "utf8");
1663
+ await fs2.rename(temporaryPath, targetPath);
1664
+ } catch (error) {
1665
+ await fs2.rm(temporaryPath, { force: true }).catch(() => void 0);
1666
+ throw error;
1667
+ }
1668
+ }
1669
+ function createPersistentContentCache(options) {
1670
+ const { config, logger, tracer } = options;
1671
+ const baseDir = cacheDirectory(config);
1672
+ const versionDir = versionDirectory(baseDir);
1673
+ return {
1674
+ async get(key) {
1675
+ if (!config.cache?.enabled) return void 0;
1676
+ const filePath = cacheFilePath(baseDir, key);
1677
+ try {
1678
+ const raw = await fs2.readFile(filePath, "utf8");
1679
+ const entry = JSON.parse(raw);
1680
+ if (entry.schemaVersion !== CONTENT_CACHE_SCHEMA_VERSION) {
1681
+ tracer?.event("persistentContentCache.schemaMismatch", { key });
1682
+ return void 0;
1683
+ }
1684
+ if (entry.key !== key) {
1685
+ tracer?.event("persistentContentCache.keyMismatch", { key });
1686
+ return void 0;
1687
+ }
1688
+ if (!isValidCacheEntry(entry)) {
1689
+ tracer?.event("persistentContentCache.invalidShape", { key });
1690
+ return void 0;
1691
+ }
1692
+ tracer?.event("persistentContentCache.found", { key });
1693
+ return entry;
1694
+ } catch (error) {
1695
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1696
+ tracer?.event("persistentContentCache.miss", { key });
1697
+ return void 0;
1698
+ }
1699
+ if (error instanceof SyntaxError) {
1700
+ logger?.warn(
1701
+ "Persistent content cache entry is corrupted and will be ignored.",
1702
+ {
1703
+ key
1704
+ }
1705
+ );
1706
+ return void 0;
1707
+ }
1708
+ throw error;
1709
+ }
1710
+ },
1711
+ async set(key, value) {
1712
+ if (!config.cache?.enabled) return;
1713
+ const filePath = cacheFilePath(baseDir, key);
1714
+ const serialized = JSON.stringify(value);
1715
+ if (serialized === void 0) {
1716
+ throw new TypeError(
1717
+ "Persistent content cache values must be JSON serializable."
1718
+ );
1719
+ }
1720
+ await writeAtomically(filePath, versionDir, serialized);
1721
+ tracer?.event("persistentContentCache.write", { key });
1722
+ },
1723
+ async clear() {
1724
+ try {
1725
+ await fs2.rm(baseDir, { recursive: true, force: true });
1726
+ } catch {
1727
+ }
1728
+ }
1729
+ };
1730
+ }
1731
+ function isValidCacheEntry(entry) {
1732
+ 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);
1733
+ }
1734
+ function isValidDependency(value) {
1735
+ if (typeof value !== "object" || value === null) return false;
1736
+ const candidate = value;
1737
+ return (candidate.kind === "content" || candidate.kind === "file" || candidate.kind === "link") && typeof candidate.id === "string" && typeof candidate.fingerprint === "string";
1738
+ }
1739
+ function computeContentCacheKey(inputs) {
1740
+ const { slug, source, frontmatter, pipelineFingerprint } = inputs;
1741
+ const normalizedFrontmatter = normalizeFrontmatterForCache(frontmatter);
1742
+ const keyData = {
1743
+ schemaVersion: CONTENT_CACHE_SCHEMA_VERSION,
1744
+ slug,
1745
+ source,
1746
+ frontmatter: normalizedFrontmatter,
1747
+ pipelineFingerprint
1748
+ };
1749
+ return hash2(stableStringify(keyData));
1750
+ }
1751
+ function normalizeFrontmatterForCache(frontmatter) {
1752
+ const normalized = {};
1753
+ for (const [key, value] of Object.entries(frontmatter)) {
1754
+ if (value instanceof Date) {
1755
+ normalized[key] = value.toISOString();
1756
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1757
+ normalized[key] = normalizeFrontmatterForCache(
1758
+ value
1759
+ );
1760
+ } else {
1761
+ normalized[key] = value;
1762
+ }
1763
+ }
1764
+ return normalized;
1765
+ }
1766
+ function computePipelineFingerprint(config) {
1767
+ const plugins = config.plugins;
1768
+ const fingerprintData = {
1769
+ compatibilityVersion: CONTENT_CACHE_COMPATIBILITY_VERSION,
1770
+ plugins: plugins.map((p) => pluginFingerprint(p)),
1771
+ markdown: config.markdown,
1772
+ content: {
1773
+ filters: config.content.filters
1774
+ }
1775
+ };
1776
+ return hash2(stableStringify(fingerprintData));
1777
+ }
1778
+ var pluginFingerprints = /* @__PURE__ */ new WeakMap();
1779
+ function pluginFingerprint(plugin) {
1780
+ const cached = pluginFingerprints.get(plugin);
1781
+ if (cached !== void 0) return cached;
1782
+ const fingerprint = sanitizeForFingerprint(plugin);
1783
+ pluginFingerprints.set(plugin, fingerprint);
1784
+ return fingerprint;
1785
+ }
1786
+ var functionSourceFingerprints = /* @__PURE__ */ new Map();
1787
+ function fingerprintFunctionSource(value) {
1788
+ const source = Function.prototype.toString.call(value);
1789
+ const cached = functionSourceFingerprints.get(source);
1790
+ if (cached !== void 0) return cached;
1791
+ const fingerprint = `fn:${hash2(source)}`;
1792
+ functionSourceFingerprints.set(source, fingerprint);
1793
+ return fingerprint;
1794
+ }
1795
+ function sanitizeForFingerprint(value) {
1796
+ if (value === null || value === void 0) return value;
1797
+ if (typeof value === "function") {
1798
+ return fingerprintFunctionSource(value);
1799
+ }
1800
+ if (typeof value === "symbol") return "[Symbol]";
1801
+ if (Array.isArray(value)) return value.map(sanitizeForFingerprint);
1802
+ if (value instanceof Map || value instanceof Set)
1803
+ return `[${value.constructor.name}]`;
1804
+ if (typeof value === "object") {
1805
+ const result = {};
1806
+ for (const [key, val] of Object.entries(value)) {
1807
+ result[key] = sanitizeForFingerprint(val);
1808
+ }
1809
+ return result;
1810
+ }
1811
+ return value;
1812
+ }
1813
+ function isPersistentlyCacheable(_slug, config) {
1814
+ const hasL10n = config.plugins.some(
1815
+ (p) => p.name === "l10n" && p.enabled !== false
1816
+ );
1817
+ if (hasL10n) {
1818
+ return { cacheable: false, reason: "l10n plugin is enabled" };
1819
+ }
1820
+ const unsafePlugin = config.plugins.find((plugin) => {
1821
+ if (plugin.enabled === false) return false;
1822
+ if (!canAffectProcessedContent(plugin)) return false;
1823
+ return plugin.processedContentCache?.dependencyMode !== "none" && plugin.processedContentCache?.dependencyMode !== "tracked";
1824
+ });
1825
+ if (unsafePlugin) {
1826
+ if (unsafePlugin.processedContentCache?.dependencyMode === "unsafe") {
1827
+ return {
1828
+ cacheable: false,
1829
+ reason: `plugin ${unsafePlugin.name} explicitly marks processed content cache as unsafe`
1830
+ };
1831
+ }
1832
+ return {
1833
+ cacheable: false,
1834
+ reason: `plugin ${unsafePlugin.name} can affect processed content without a processedContentCache contract`
1835
+ };
1836
+ }
1837
+ return { cacheable: true };
1838
+ }
1839
+ function canAffectProcessedContent(plugin) {
1840
+ return Boolean(
1841
+ plugin.remarkPlugins?.length || plugin.rehypePlugins?.length || plugin.extendMarkdownPipeline || plugin.extendHtmlPipeline || plugin.renderers?.length || plugin.onContentLoaded || plugin.onPostParsed || plugin.onPostProcessed
1842
+ );
1843
+ }
1844
+ function extractFrontmatter(markdown) {
1845
+ const vfile = new VFile({ value: markdown });
1846
+ matter(vfile);
1847
+ return vfile.data.matter ?? {};
1848
+ }
1849
+
1850
+ // src/plugin/plugin_cache.ts
1851
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
1852
+ import fs3 from "node:fs/promises";
1853
+ import path3 from "node:path";
1423
1854
  function createUnavailablePluginCache() {
1424
1855
  return {
1425
1856
  get: unavailableAtRuntime,
@@ -1429,20 +1860,20 @@ function createUnavailablePluginCache() {
1429
1860
  };
1430
1861
  }
1431
1862
  function createPluginCache(options) {
1432
- const pluginDirectory = path2.join(
1863
+ const pluginDirectory = path3.join(
1433
1864
  options.cacheDirectory,
1434
1865
  pluginNamespace(options.pluginName)
1435
1866
  );
1436
- const versionDirectory = path2.join(
1867
+ const versionDirectory2 = path3.join(
1437
1868
  pluginDirectory,
1438
- hash(options.cacheVersion ?? "1")
1869
+ hash3(options.cacheVersion ?? "1")
1439
1870
  );
1440
1871
  const writes = /* @__PURE__ */ new Map();
1441
1872
  return {
1442
1873
  async get(key) {
1443
1874
  let raw;
1444
1875
  try {
1445
- raw = await fs2.readFile(cachePath(versionDirectory, key), "utf8");
1876
+ raw = await fs3.readFile(cachePath(versionDirectory2, key), "utf8");
1446
1877
  } catch (error) {
1447
1878
  if (isNotFoundError2(error)) {
1448
1879
  options.tracer?.event("cache.miss", { plugin: options.pluginName });
@@ -1465,13 +1896,13 @@ function createPluginCache(options) {
1465
1896
  }
1466
1897
  },
1467
1898
  async set(key, value) {
1468
- const targetPath = cachePath(versionDirectory, key);
1899
+ const targetPath = cachePath(versionDirectory2, key);
1469
1900
  const serialized = JSON.stringify(value);
1470
1901
  if (serialized === void 0) {
1471
1902
  throw new TypeError("Plugin cache values must be JSON serializable.");
1472
1903
  }
1473
1904
  const previousWrite = writes.get(targetPath) ?? Promise.resolve();
1474
- const write = previousWrite.catch(() => void 0).then(() => writeAtomically(targetPath, versionDirectory, serialized));
1905
+ const write = previousWrite.catch(() => void 0).then(() => writeAtomically2(targetPath, versionDirectory2, serialized));
1475
1906
  writes.set(targetPath, write);
1476
1907
  try {
1477
1908
  await write;
@@ -1482,37 +1913,37 @@ function createPluginCache(options) {
1482
1913
  }
1483
1914
  },
1484
1915
  async delete(key) {
1485
- await fs2.rm(cachePath(versionDirectory, key), { force: true });
1916
+ await fs3.rm(cachePath(versionDirectory2, key), { force: true });
1486
1917
  },
1487
1918
  async clear() {
1488
- await fs2.rm(pluginDirectory, { recursive: true, force: true });
1919
+ await fs3.rm(pluginDirectory, { recursive: true, force: true });
1489
1920
  }
1490
1921
  };
1491
1922
  }
1492
- async function writeAtomically(targetPath, versionDirectory, serialized) {
1493
- await fs2.mkdir(versionDirectory, { recursive: true });
1923
+ async function writeAtomically2(targetPath, versionDirectory2, serialized) {
1924
+ await fs3.mkdir(versionDirectory2, { recursive: true });
1494
1925
  const temporaryPath = `${targetPath}.${randomUUID2()}.tmp`;
1495
1926
  try {
1496
- await fs2.writeFile(temporaryPath, serialized, "utf8");
1497
- await fs2.rename(temporaryPath, targetPath);
1927
+ await fs3.writeFile(temporaryPath, serialized, "utf8");
1928
+ await fs3.rename(temporaryPath, targetPath);
1498
1929
  } catch (error) {
1499
- await fs2.rm(temporaryPath, { force: true }).catch(() => void 0);
1930
+ await fs3.rm(temporaryPath, { force: true }).catch(() => void 0);
1500
1931
  throw error;
1501
1932
  }
1502
1933
  }
1503
1934
  function resolvePluginCacheDirectory(config) {
1504
- if (!config) return path2.resolve(process.cwd(), ".riebeckite", "cache");
1505
- if (config.buildDirectory) return path2.join(config.buildDirectory, "cache");
1506
- return path2.resolve(config.content.directory, "..", ".riebeckite", "cache");
1935
+ if (!config) return path3.resolve(process.cwd(), ".riebeckite", "cache");
1936
+ if (config.buildDirectory) return path3.join(config.buildDirectory, "cache");
1937
+ return path3.resolve(config.content.directory, "..", ".riebeckite", "cache");
1507
1938
  }
1508
- function cachePath(versionDirectory, key) {
1509
- return path2.join(versionDirectory, `${hash(key)}.json`);
1939
+ function cachePath(versionDirectory2, key) {
1940
+ return path3.join(versionDirectory2, `${hash3(key)}.json`);
1510
1941
  }
1511
1942
  function pluginNamespace(pluginName) {
1512
- return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash(pluginName);
1943
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash3(pluginName);
1513
1944
  }
1514
- function hash(value) {
1515
- return createHash2("sha256").update(value).digest("hex");
1945
+ function hash3(value) {
1946
+ return createHash5("sha256").update(value).digest("hex");
1516
1947
  }
1517
1948
  function isNotFoundError2(error) {
1518
1949
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
@@ -1524,16 +1955,20 @@ async function unavailableAtRuntime() {
1524
1955
  }
1525
1956
 
1526
1957
  // src/types/generated_output.ts
1958
+ var ATTACHMENTS_NAMESPACE = ATTACHMENTS_BASE_PATH.replace(/^\/+/, "");
1959
+ var STATIC_ASSETS_DIR = "assets";
1527
1960
  function createUnavailableGeneratedOutputSink() {
1961
+ const unavailable = () => {
1962
+ throw new Error(
1963
+ "Generated output is available only during a build lifecycle."
1964
+ );
1965
+ };
1528
1966
  return {
1529
- emit() {
1530
- throw new Error(
1531
- "Generated output is available only during a build lifecycle."
1532
- );
1533
- }
1967
+ emit: unavailable,
1968
+ emitAsset: unavailable
1534
1969
  };
1535
1970
  }
1536
- function normalizeGeneratedOutputPath(input) {
1971
+ function normalizeOutputPathSegments(input) {
1537
1972
  if (typeof input !== "string" || input.length === 0) {
1538
1973
  throw new Error("Generated output path must be a non-empty string.");
1539
1974
  }
@@ -1557,12 +1992,27 @@ function normalizeGeneratedOutputPath(input) {
1557
1992
  );
1558
1993
  }
1559
1994
  }
1560
- const normalized = segments.join("/");
1561
- if (normalized === "assets" || normalized.startsWith("assets/")) {
1995
+ return segments.join("/");
1996
+ }
1997
+ function normalizeGeneratedOutputPath(input) {
1998
+ const normalized = normalizeOutputPathSegments(input);
1999
+ if (normalized === STATIC_ASSETS_DIR || normalized.startsWith(`${STATIC_ASSETS_DIR}/`)) {
1562
2000
  throw new Error(
1563
2001
  `Generated output path must not conflict with the static assets namespace: "${input}".`
1564
2002
  );
1565
2003
  }
2004
+ return rejectInternalRiebeckitePath(normalized, input);
2005
+ }
2006
+ function normalizeContentAssetOutputPath(input) {
2007
+ const normalized = normalizeOutputPathSegments(input);
2008
+ if (normalized === ATTACHMENTS_NAMESPACE || normalized.startsWith(`${ATTACHMENTS_NAMESPACE}/`)) {
2009
+ throw new Error(
2010
+ `Generated output path must not conflict with the attachment namespace: "${input}".`
2011
+ );
2012
+ }
2013
+ return rejectInternalRiebeckitePath(normalized, input);
2014
+ }
2015
+ function rejectInternalRiebeckitePath(normalized, input) {
1566
2016
  if (normalized === ".riebeckite" || normalized.startsWith(".riebeckite/")) {
1567
2017
  throw new Error(
1568
2018
  `Generated output path must not target internal Riebeckite files: "${input}".`
@@ -1579,6 +2029,16 @@ var Pipeline = class {
1579
2029
  this.getMarkdownBySlug = getMarkdownBySlug;
1580
2030
  this.options = options;
1581
2031
  this.isBuildTime = isBuildTime;
2032
+ if (this.options.config && this.options.config.cache?.enabled !== false) {
2033
+ this.persistentCache = createPersistentContentCache({
2034
+ config: this.options.config,
2035
+ logger: this.options.observability?.logger,
2036
+ tracer: this.options.observability?.tracer
2037
+ });
2038
+ this.pipelineFingerprint = computePipelineFingerprint(
2039
+ this.options.config
2040
+ );
2041
+ }
1582
2042
  }
1583
2043
  contentIndex;
1584
2044
  permalinks;
@@ -1586,23 +2046,106 @@ var Pipeline = class {
1586
2046
  options;
1587
2047
  isBuildTime;
1588
2048
  pluginCaches = /* @__PURE__ */ new Map();
2049
+ persistentCache = null;
2050
+ pipelineFingerprint = null;
2051
+ resolvedPlugins = null;
1589
2052
  async execute(markDownContent, context = {}) {
1590
2053
  return await this.executeWithEmbedState(
1591
2054
  markDownContent,
1592
2055
  context,
1593
2056
  0,
1594
- new Set(context.sourceSlug ? [context.sourceSlug] : [])
2057
+ new Set(context.sourceSlug ? [context.sourceSlug] : []),
2058
+ this.options.contentSource ? createContentDependencyTracker(this.options.contentSource) : null
1595
2059
  );
1596
2060
  }
1597
- async executeWithEmbedState(markDownContent, context, embedDepth, embedTrail) {
2061
+ async executeWithEmbedState(markDownContent, context, embedDepth, embedTrail, rootDependencyTracker) {
2062
+ const dependencyTracker = rootDependencyTracker;
2063
+ const effectiveContentSource = dependencyTracker ? dependencyTracker.contentSource : this.options.contentSource;
2064
+ const sourceSlug = context.sourceSlug;
2065
+ if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && this.options.contentSource && this.options.config) {
2066
+ const cacheable = isPersistentlyCacheable(
2067
+ sourceSlug,
2068
+ this.options.config
2069
+ );
2070
+ if (cacheable.cacheable) {
2071
+ const frontmatter = extractFrontmatter(markDownContent);
2072
+ const cacheKey = computeContentCacheKey({
2073
+ slug: sourceSlug,
2074
+ source: markDownContent,
2075
+ frontmatter,
2076
+ pipelineFingerprint: this.pipelineFingerprint
2077
+ });
2078
+ const cachedEntry = await this.persistentCache.get(cacheKey);
2079
+ if (cachedEntry) {
2080
+ const validationTracker = createContentDependencyTracker(
2081
+ this.options.contentSource
2082
+ );
2083
+ let valid = true;
2084
+ for (const dep of cachedEntry.dependencies) {
2085
+ if (dep.kind === "content") {
2086
+ const content = await validationTracker.readContent(
2087
+ dep.id,
2088
+ async () => await this.getMarkdownBySlug?.(dep.id) ?? ""
2089
+ );
2090
+ if (fingerprintContent(content) !== dep.fingerprint) {
2091
+ valid = false;
2092
+ break;
2093
+ }
2094
+ } else if (dep.kind === "file") {
2095
+ const fileContent = await validationTracker.contentSource.read({
2096
+ path: dep.id
2097
+ });
2098
+ if (fingerprintContent(fileContent) !== dep.fingerprint) {
2099
+ valid = false;
2100
+ break;
2101
+ }
2102
+ } else if (dep.kind === "link") {
2103
+ if (fingerprintContent(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2104
+ valid = false;
2105
+ break;
2106
+ }
2107
+ }
2108
+ }
2109
+ if (valid) {
2110
+ this.reportContentDependencies(
2111
+ sourceSlug,
2112
+ cachedEntry.dependencies
2113
+ );
2114
+ this.options.observability?.tracer?.event(
2115
+ "persistentContentCache.hit",
2116
+ { key: cacheKey, slug: sourceSlug }
2117
+ );
2118
+ this.options.onPersistentContentCacheResult?.("hit");
2119
+ return {
2120
+ frontmatter: cachedEntry.value.frontmatter,
2121
+ html: cachedEntry.value.html
2122
+ };
2123
+ }
2124
+ }
2125
+ this.options.observability?.tracer?.event(
2126
+ "persistentContentCache.miss",
2127
+ { key: cacheKey, slug: sourceSlug }
2128
+ );
2129
+ this.options.onPersistentContentCacheResult?.("miss");
2130
+ } else {
2131
+ this.options.observability?.tracer?.event(
2132
+ "persistentContentCache.bypass",
2133
+ {
2134
+ slug: sourceSlug,
2135
+ ...cacheable.reason ? { reason: cacheable.reason } : {}
2136
+ }
2137
+ );
2138
+ this.options.onPersistentContentCacheResult?.("bypass");
2139
+ }
2140
+ }
1598
2141
  const processor = unified();
1599
- const plugins = resolvePlugins(this.options.plugins);
2142
+ const plugins = this.resolvePlugins();
1600
2143
  this.use(processor, remarkParse);
1601
2144
  this.use(processor, remarkDirective);
1602
2145
  this.use(processor, remarkFrontmatter, ["yaml", "toml"]);
1603
2146
  this.use(processor, function loadFrontmatter() {
1604
2147
  return (_tree, file2) => {
1605
- matter(file2);
2148
+ matter2(file2);
1606
2149
  };
1607
2150
  });
1608
2151
  this.use(processor, remarkMath);
@@ -1611,9 +2154,14 @@ var Pipeline = class {
1611
2154
  sourceSlug: context.sourceSlug,
1612
2155
  contentIndex: this.contentIndex,
1613
2156
  resolvePermalink: (slug) => this.getPermalink(slug),
1614
- renderNoteEmbed: this.createNoteEmbedRenderer(embedDepth, embedTrail),
1615
- renderContent: this.createContentRenderer(),
1616
- contentSource: this.options.contentSource
2157
+ isRoutable: this.options.isRoutable,
2158
+ renderNoteEmbed: this.createNoteEmbedRenderer(
2159
+ embedDepth,
2160
+ embedTrail,
2161
+ dependencyTracker
2162
+ ),
2163
+ renderContent: this.createContentRenderer(effectiveContentSource),
2164
+ contentSource: effectiveContentSource
1617
2165
  };
1618
2166
  for (const plugin of plugins) {
1619
2167
  for (const remarkPlugin of plugin.remarkPlugins ?? []) {
@@ -1628,7 +2176,10 @@ var Pipeline = class {
1628
2176
  markdownPipelineContext
1629
2177
  );
1630
2178
  }
1631
- this.use(processor, normalizeMarkdownLinks, markdownPipelineContext);
2179
+ this.use(processor, normalizeMarkdownLinks, {
2180
+ ...markdownPipelineContext,
2181
+ recordLinkResolution: dependencyTracker ? (id, value) => dependencyTracker.recordLinkResolution(id, value) : void 0
2182
+ });
1632
2183
  this.use(processor, remarkRehype, { allowDangerousHtml: true });
1633
2184
  this.use(processor, rehypeRaw);
1634
2185
  for (const plugin of plugins) {
@@ -1646,10 +2197,43 @@ var Pipeline = class {
1646
2197
  this.use(processor, rehypeFormat);
1647
2198
  this.use(processor, rehypeStringify, { allowDangerousHtml: true });
1648
2199
  const file = await processor.process(markDownContent.trim());
1649
- return {
2200
+ const result = {
1650
2201
  frontmatter: file.data.matter || {},
1651
2202
  html: String(file.value)
1652
2203
  };
2204
+ if (sourceSlug && dependencyTracker) {
2205
+ this.reportContentDependencies(
2206
+ sourceSlug,
2207
+ dependencyTracker.dependencies()
2208
+ );
2209
+ }
2210
+ if (embedDepth === 0 && this.persistentCache && this.pipelineFingerprint && sourceSlug && dependencyTracker && this.options.config) {
2211
+ const cacheable = isPersistentlyCacheable(
2212
+ sourceSlug,
2213
+ this.options.config
2214
+ );
2215
+ if (cacheable.cacheable) {
2216
+ const frontmatter = extractFrontmatter(markDownContent);
2217
+ const cacheKey = computeContentCacheKey({
2218
+ slug: sourceSlug,
2219
+ source: markDownContent,
2220
+ frontmatter,
2221
+ pipelineFingerprint: this.pipelineFingerprint
2222
+ });
2223
+ const dependencies = dependencyTracker.dependencies();
2224
+ const entry = {
2225
+ schemaVersion: CONTENT_CACHE_SCHEMA_VERSION,
2226
+ key: cacheKey,
2227
+ dependencies,
2228
+ value: result
2229
+ };
2230
+ await this.persistentCache.set(cacheKey, entry);
2231
+ }
2232
+ }
2233
+ return result;
2234
+ }
2235
+ reportContentDependencies(slug, dependencies) {
2236
+ this.options.onContentDependencies?.(slug, dependencies);
1653
2237
  }
1654
2238
  getPermalink(slug) {
1655
2239
  const permalink = this.permalinks.get(slug);
@@ -1658,6 +2242,14 @@ var Pipeline = class {
1658
2242
  }
1659
2243
  return permalink;
1660
2244
  }
2245
+ resolveLinkDependency(id) {
2246
+ return resolveLinkDependencyValue(
2247
+ id,
2248
+ this.contentIndex,
2249
+ (slug) => this.permalinks.get(slug),
2250
+ this.options.isRoutable
2251
+ );
2252
+ }
1661
2253
  use(processor, plugin, options) {
1662
2254
  const register = processor.use.bind(processor);
1663
2255
  if (options === void 0) {
@@ -1666,26 +2258,37 @@ var Pipeline = class {
1666
2258
  register(plugin, options);
1667
2259
  }
1668
2260
  }
1669
- createNoteEmbedRenderer(embedDepth, embedTrail) {
2261
+ createNoteEmbedRenderer(embedDepth, embedTrail, dependencyTracker) {
1670
2262
  if (!this.getMarkdownBySlug || embedDepth >= 3) return void 0;
1671
2263
  return async (slug, fragment) => {
2264
+ if (this.options.isRoutable && !this.options.isRoutable(slug)) {
2265
+ return null;
2266
+ }
1672
2267
  if (embedTrail.has(slug)) return null;
1673
2268
  const sourceMarkdown = await this.getMarkdownBySlug?.(slug);
1674
2269
  const markdown = sourceMarkdown ? selectEmbedMarkdownFragment(sourceMarkdown, fragment) : null;
1675
2270
  if (!markdown) return null;
1676
2271
  const nextEmbedTrail = new Set(embedTrail);
1677
2272
  nextEmbedTrail.add(slug);
2273
+ if (dependencyTracker) {
2274
+ await dependencyTracker.readContent(slug, async () => markdown);
2275
+ }
1678
2276
  const content = await this.executeWithEmbedState(
1679
2277
  markdown,
1680
2278
  { sourceSlug: slug },
1681
2279
  embedDepth + 1,
1682
- nextEmbedTrail
2280
+ nextEmbedTrail,
2281
+ dependencyTracker
1683
2282
  );
1684
2283
  return content.html;
1685
2284
  };
1686
2285
  }
1687
- createContentRenderer() {
1688
- const renderers = resolvePlugins(this.options.plugins).flatMap(
2286
+ resolvePlugins() {
2287
+ this.resolvedPlugins ??= resolvePlugins(this.options.plugins);
2288
+ return this.resolvedPlugins;
2289
+ }
2290
+ createContentRenderer(contentSource) {
2291
+ const renderers = this.resolvePlugins().flatMap(
1689
2292
  (plugin) => (plugin.renderers ?? []).map((renderer) => ({ plugin, renderer }))
1690
2293
  );
1691
2294
  if (renderers.length === 0) return void 0;
@@ -1707,7 +2310,7 @@ var Pipeline = class {
1707
2310
  output: createUnavailableGeneratedOutputSink(),
1708
2311
  logger: observability.logger.child({ plugin: plugin.name }),
1709
2312
  tracer: observability.tracer,
1710
- contentSource: this.options.contentSource,
2313
+ contentSource,
1711
2314
  ...input
1712
2315
  })
1713
2316
  );
@@ -1750,6 +2353,21 @@ function normalizeMarkdownLinks(context) {
1750
2353
  });
1751
2354
  };
1752
2355
  }
2356
+ var LINK_INDEX_PREFIX = "index:";
2357
+ var LINK_LOCATION_PREFIX = "location:";
2358
+ function resolveLinkDependencyValue(id, contentIndex, lookupPermalink, isRoutable) {
2359
+ if (id.startsWith(LINK_INDEX_PREFIX)) {
2360
+ return contentIndex.get(id.slice(LINK_INDEX_PREFIX.length)) ?? "";
2361
+ }
2362
+ if (id.startsWith(LINK_LOCATION_PREFIX)) {
2363
+ const slug = id.slice(LINK_LOCATION_PREFIX.length);
2364
+ const permalink = lookupPermalink(slug);
2365
+ if (permalink === void 0) return "";
2366
+ if (isRoutable && !isRoutable(slug)) return "";
2367
+ return permalink;
2368
+ }
2369
+ return "";
2370
+ }
1753
2371
  function visitMarkdownLinkNodes(node, visitor) {
1754
2372
  const linkNode = node;
1755
2373
  if ((node.type === "link" || node.type === "definition") && typeof linkNode.url === "string") {
@@ -1762,7 +2380,7 @@ function visitMarkdownLinkNodes(node, visitor) {
1762
2380
  function normalizeMarkdownLinkUrl(url, context) {
1763
2381
  if (!context.sourceSlug) return null;
1764
2382
  if (!isMarkdownContentLink(url)) return null;
1765
- const [pathAndQuery, hash3 = ""] = splitOnce(url, "#");
2383
+ const [pathAndQuery, hash4 = ""] = splitOnce(url, "#");
1766
2384
  const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
1767
2385
  const decodedPath = decodeUriPath(rawPath);
1768
2386
  if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
@@ -1773,15 +2391,35 @@ function normalizeMarkdownLinkUrl(url, context) {
1773
2391
  if (!resolvedSlug) return null;
1774
2392
  const indexedSlug = getExactContentSlug(context, resolvedSlug);
1775
2393
  if (!indexedSlug) return null;
1776
- const suffix = `${query ? `?${query}` : ""}${hash3 ? `#${hash3}` : ""}`;
2394
+ recordLinkResolution(context, LINK_LOCATION_PREFIX, indexedSlug);
2395
+ if (context.isRoutable && !context.isRoutable(indexedSlug)) return null;
2396
+ const suffix = `${query ? `?${query}` : ""}${hash4 ? `#${hash4}` : ""}`;
1777
2397
  return `${context.resolvePermalink(indexedSlug)}${suffix}`;
1778
2398
  }
1779
- function getExactContentSlug(context, slug) {
1780
- try {
2399
+ function recordLinkResolution(context, prefix, key) {
2400
+ if (!context.recordLinkResolution) return;
2401
+ const value = prefix === LINK_INDEX_PREFIX ? context.contentIndex.get(key) ?? "" : resolveLinkDependencyValue(
2402
+ `${prefix}${key}`,
2403
+ context.contentIndex,
2404
+ (slug) => {
2405
+ try {
2406
+ return context.resolvePermalink(slug);
2407
+ } catch {
2408
+ return void 0;
2409
+ }
2410
+ },
2411
+ context.isRoutable
2412
+ );
2413
+ context.recordLinkResolution(`${prefix}${key}`, value);
2414
+ }
2415
+ function getExactContentSlug(context, slug) {
2416
+ try {
1781
2417
  context.resolvePermalink(slug);
1782
2418
  return slug;
1783
2419
  } catch {
1784
- return context.contentIndex.get(slug.toLowerCase()) ?? null;
2420
+ const indexKey = slug.toLowerCase();
2421
+ recordLinkResolution(context, LINK_INDEX_PREFIX, indexKey);
2422
+ return context.contentIndex.get(indexKey) ?? null;
1785
2423
  }
1786
2424
  }
1787
2425
  function isMarkdownContentLink(url) {
@@ -1805,9 +2443,9 @@ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
1805
2443
  }
1806
2444
  return normalizedParts.join("/").replace(/\.md$/i, "");
1807
2445
  }
1808
- function decodeUriPath(path4) {
2446
+ function decodeUriPath(path5) {
1809
2447
  try {
1810
- return decodeURI(path4);
2448
+ return decodeURI(path5);
1811
2449
  } catch {
1812
2450
  return null;
1813
2451
  }
@@ -1895,17 +2533,27 @@ var GeneratedOutputRegistry = class {
1895
2533
  outputs = [];
1896
2534
  owners = /* @__PURE__ */ new Map();
1897
2535
  sinkFor(pluginName) {
2536
+ const register = (path5, output) => {
2537
+ const owner = this.owners.get(path5);
2538
+ if (owner) {
2539
+ throw new Error(
2540
+ `Duplicate generated output path "${path5}" declared by "${owner}" and "${pluginName}".`
2541
+ );
2542
+ }
2543
+ this.owners.set(path5, pluginName);
2544
+ this.outputs.push({
2545
+ path: path5,
2546
+ content: output.content,
2547
+ dependencies: output.dependencies,
2548
+ owner: pluginName
2549
+ });
2550
+ };
1898
2551
  return {
1899
2552
  emit: (output) => {
1900
- const path4 = normalizeGeneratedOutputPath(output.path);
1901
- const owner = this.owners.get(path4);
1902
- if (owner) {
1903
- throw new Error(
1904
- `Duplicate generated output path "${path4}" declared by "${owner}" and "${pluginName}".`
1905
- );
1906
- }
1907
- this.owners.set(path4, pluginName);
1908
- this.outputs.push({ path: path4, content: output.content, owner: pluginName });
2553
+ register(normalizeGeneratedOutputPath(output.path), output);
2554
+ },
2555
+ emitAsset: (output) => {
2556
+ register(normalizeContentAssetOutputPath(output.path), output);
1909
2557
  }
1910
2558
  };
1911
2559
  }
@@ -1988,6 +2636,7 @@ var PluginRuntime = class {
1988
2636
  pluginCaches = /* @__PURE__ */ new Map();
1989
2637
  generatedOutputs = new GeneratedOutputRegistry();
1990
2638
  isBuildTime = false;
2639
+ resolvedPlugins = null;
1991
2640
  getDiagnostics() {
1992
2641
  return this.diagnostics;
1993
2642
  }
@@ -2108,20 +2757,26 @@ var PluginRuntime = class {
2108
2757
  }
2109
2758
  async resolvePage(pathname, manifest, contentIndex) {
2110
2759
  const matches = [];
2111
- const normalizedPathname = normalizePagePath(pathname);
2112
- const context = {
2113
- ...this.createContext(contentIndex),
2114
- manifest,
2115
- pathname: normalizedPathname
2116
- };
2760
+ const requestedPathname = normalizePagePath(pathname);
2117
2761
  for (const plugin of this.plugins()) {
2118
2762
  for (const pageType of plugin.pageTypes ?? []) {
2763
+ const pagePathname = normalizePageTypePath(pathname, pageType);
2764
+ const context = {
2765
+ ...this.createContext(contentIndex),
2766
+ manifest,
2767
+ pathname: pagePathname
2768
+ };
2119
2769
  const page = await pageType.resolve(
2120
2770
  this.createPluginContext(plugin, context)
2121
2771
  );
2122
2772
  if (page) {
2123
2773
  matches.push({
2124
- page: { ...page, type: pageType.id, pluginName: plugin.name },
2774
+ page: {
2775
+ ...page,
2776
+ pathname: pageType.directoryIndex ? normalizeDirectoryPagePath(page.pathname) : page.pathname,
2777
+ type: pageType.id,
2778
+ pluginName: plugin.name
2779
+ },
2125
2780
  priority: pageType.priority ?? 0
2126
2781
  });
2127
2782
  }
@@ -2134,7 +2789,7 @@ var PluginRuntime = class {
2134
2789
  );
2135
2790
  if (highest.length !== 1) {
2136
2791
  throw new Error(
2137
- `Multiple plugin page types match ${normalizedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
2792
+ `Multiple plugin page types match ${requestedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
2138
2793
  );
2139
2794
  }
2140
2795
  return highest[0].page;
@@ -2146,12 +2801,60 @@ var PluginRuntime = class {
2146
2801
  for (const pageType of plugin.pageTypes ?? []) {
2147
2802
  const declared = pageType.paths;
2148
2803
  if (!declared) continue;
2149
- paths.push(
2150
- ...typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared
2151
- );
2804
+ const resolved = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2805
+ for (const rawPath of resolved) {
2806
+ paths.push(normalizePageTypePath(rawPath, pageType));
2807
+ }
2808
+ }
2809
+ }
2810
+ return [...new Set(paths)];
2811
+ }
2812
+ async getPageRoutes(manifest, contentIndex) {
2813
+ const routes = [];
2814
+ const context = { ...this.createContext(contentIndex), manifest };
2815
+ for (const plugin of this.plugins()) {
2816
+ for (const pageType of plugin.pageTypes ?? []) {
2817
+ const declared = pageType.paths;
2818
+ if (!declared) continue;
2819
+ const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2820
+ for (const pathname of paths) {
2821
+ routes.push({
2822
+ pathname: normalizePageTypePath(pathname, pageType),
2823
+ pluginName: plugin.name,
2824
+ pageType: pageType.id
2825
+ });
2826
+ }
2827
+ }
2828
+ }
2829
+ return routes;
2830
+ }
2831
+ async getPageOutputs(manifest, contentIndex) {
2832
+ const outputs = [];
2833
+ const context = { ...this.createContext(contentIndex), manifest };
2834
+ for (const plugin of this.plugins()) {
2835
+ for (const pageType of plugin.pageTypes ?? []) {
2836
+ const declared = pageType.paths;
2837
+ if (!declared) continue;
2838
+ const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2839
+ for (const rawPath of paths) {
2840
+ const pathname = normalizePageTypePath(rawPath, pageType);
2841
+ const dependencyContext = { ...context, pathname };
2842
+ const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
2843
+ this.createPluginContext(plugin, dependencyContext)
2844
+ ) : pageType.outputDependencies : [{ type: "unknown" }];
2845
+ outputs.push({
2846
+ kind: "plugin-page",
2847
+ path: pathname,
2848
+ producer: `plugin:${plugin.name}:page:${pageType.id}`,
2849
+ dependencies
2850
+ });
2851
+ }
2152
2852
  }
2153
2853
  }
2154
- return [...new Set(paths.map(normalizePagePath))];
2854
+ return uniqueOutputs(outputs);
2855
+ }
2856
+ getContentOutputDependencies() {
2857
+ return this.plugins().flatMap((plugin) => plugin.outputDependencies ?? []);
2155
2858
  }
2156
2859
  async collectDiagnostics(contentIndex) {
2157
2860
  const tracer = this.observability().tracer;
@@ -2209,6 +2912,7 @@ var PluginRuntime = class {
2209
2912
  return {
2210
2913
  config: this.pipelineOptions.config,
2211
2914
  contentIndex,
2915
+ contentSource: this.pipelineOptions.contentSource,
2212
2916
  diagnostics: this.diagnostics
2213
2917
  };
2214
2918
  }
@@ -2258,27 +2962,33 @@ var PluginRuntime = class {
2258
2962
  return this.pipelineOptions.observability ?? noopObservability;
2259
2963
  }
2260
2964
  plugins() {
2261
- const plugins = resolvePlugins(this.pipelineOptions.plugins);
2262
- const pageTypes = /* @__PURE__ */ new Map();
2263
- for (const plugin of plugins) {
2264
- const declaredPageTypes = plugin.pageTypes;
2265
- if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
2266
- throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
2267
- }
2268
- for (const pageType of declaredPageTypes ?? []) {
2269
- validatePageType(plugin.name, pageType);
2270
- const previous = pageTypes.get(pageType.id);
2271
- if (previous) {
2272
- throw new Error(
2273
- `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
2274
- );
2275
- }
2276
- pageTypes.set(pageType.id, plugin.name);
2277
- }
2965
+ if (this.resolvedPlugins === null) {
2966
+ const resolved = resolvePlugins(this.pipelineOptions.plugins);
2967
+ collectPageTypeOwners(resolved);
2968
+ this.resolvedPlugins = resolved;
2278
2969
  }
2279
- return plugins;
2970
+ return this.resolvedPlugins;
2280
2971
  }
2281
2972
  };
2973
+ function collectPageTypeOwners(plugins) {
2974
+ const pageTypes = /* @__PURE__ */ new Map();
2975
+ for (const plugin of plugins) {
2976
+ const declaredPageTypes = plugin.pageTypes;
2977
+ if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
2978
+ throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
2979
+ }
2980
+ for (const pageType of declaredPageTypes ?? []) {
2981
+ validatePageType(plugin.name, pageType);
2982
+ const previous = pageTypes.get(pageType.id);
2983
+ if (previous) {
2984
+ throw new Error(
2985
+ `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
2986
+ );
2987
+ }
2988
+ pageTypes.set(pageType.id, plugin.name);
2989
+ }
2990
+ }
2991
+ }
2282
2992
  function validatePageType(pluginName, pageType) {
2283
2993
  if (typeof pageType !== "object" || pageType === null) {
2284
2994
  throw new TypeError(`Plugin ${pluginName} pageTypes must contain objects`);
@@ -2304,24 +3014,48 @@ function validatePageType(pluginName, pageType) {
2304
3014
  `Plugin ${pluginName} page type "${candidate.id}" priority must be a finite number`
2305
3015
  );
2306
3016
  }
3017
+ if (candidate.outputDependencies !== void 0 && !Array.isArray(candidate.outputDependencies) && typeof candidate.outputDependencies !== "function") {
3018
+ throw new TypeError(
3019
+ `Plugin ${pluginName} page type "${candidate.id}" outputDependencies must be an array or function`
3020
+ );
3021
+ }
3022
+ if (candidate.directoryIndex !== void 0 && typeof candidate.directoryIndex !== "boolean") {
3023
+ throw new TypeError(
3024
+ `Plugin ${pluginName} page type "${candidate.id}" directoryIndex must be a boolean`
3025
+ );
3026
+ }
3027
+ }
3028
+ function normalizePageTypePath(pathname, pageType) {
3029
+ return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
3030
+ }
3031
+ function normalizeDirectoryPagePath(pathname) {
3032
+ const path5 = normalizePagePath(pathname);
3033
+ return path5 === "/" ? path5 : `${path5}/`;
2307
3034
  }
2308
3035
  function normalizePagePath(pathname) {
2309
- const path4 = `/${pathname.split("/").filter(Boolean).join("/")}`;
2310
- return path4 === "/" ? path4 : path4.replace(/\/$/, "");
3036
+ const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3037
+ return path5 === "/" ? path5 : path5.replace(/\/$/, "");
2311
3038
  }
2312
3039
  function countSeverity(diagnostics, severity) {
2313
3040
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
2314
3041
  }
3042
+ function uniqueOutputs(outputs) {
3043
+ const byPath = /* @__PURE__ */ new Map();
3044
+ for (const output of outputs) byPath.set(output.path, output);
3045
+ return [...byPath.values()].sort(
3046
+ (left, right) => left.path.localeCompare(right.path)
3047
+ );
3048
+ }
2315
3049
 
2316
3050
  // src/content/affected_content.ts
2317
- function determineAffectedContent(changeSet, previousState, currentPaths) {
3051
+ function determineAffectedContent(changeSet, previousState, currentPaths, currentContentIndex) {
2318
3052
  const changedPaths = [
2319
3053
  ...changeSet.added,
2320
3054
  ...changeSet.changed,
2321
3055
  ...changeSet.removed
2322
3056
  ];
2323
3057
  const direct = new Set(
2324
- changedPaths.filter(isMarkdownPath2).map(toDependencyKey)
3058
+ changedPaths.filter(isMarkdownPath2).map(toDependencyId)
2325
3059
  );
2326
3060
  const dependent = /* @__PURE__ */ new Set();
2327
3061
  if (!previousState) {
@@ -2329,40 +3063,81 @@ function determineAffectedContent(changeSet, previousState, currentPaths) {
2329
3063
  return { direct, dependent };
2330
3064
  }
2331
3065
  if (changeSet.added.length > 0 || changeSet.removed.length > 0) {
2332
- addAllNotes(dependent, currentPaths, direct);
3066
+ const changedIndexKeys = findChangedIndexKeys(
3067
+ previousState.contentIndex,
3068
+ currentContentIndex
3069
+ );
3070
+ for (const [path5, entry] of Object.entries(previousState.entries)) {
3071
+ if (!isMarkdownPath2(path5)) continue;
3072
+ const slug = toDependencyId(path5);
3073
+ if (direct.has(slug) || dependent.has(slug)) continue;
3074
+ if (!entry.linkTargets?.some((target) => changedIndexKeys.has(target))) {
3075
+ continue;
3076
+ }
3077
+ dependent.add(slug);
3078
+ }
2333
3079
  }
2334
- const pending = changedPaths.map(toDependencyKey);
2335
- const seen = new Set(pending);
3080
+ const dependentsByDependency = buildDependentsByDependency(previousState);
3081
+ const pending = changedPaths.map(toDependency);
3082
+ const seen = new Set(pending.map(dependencyKey2));
2336
3083
  while (pending.length > 0) {
2337
3084
  const changed = pending.pop();
2338
3085
  if (changed === void 0) continue;
2339
- for (const [path4, entry] of Object.entries(previousState.entries)) {
2340
- if (!isMarkdownPath2(path4)) continue;
2341
- const slug = toDependencyKey(path4);
3086
+ for (const slug of dependentsByDependency.get(dependencyKey2(changed)) ?? []) {
2342
3087
  if (direct.has(slug) || dependent.has(slug)) continue;
2343
- if (!entry.dependencies.includes(changed)) continue;
2344
3088
  dependent.add(slug);
2345
- if (!seen.has(slug)) {
2346
- seen.add(slug);
2347
- pending.push(slug);
3089
+ const dependency = { kind: "content", id: slug };
3090
+ if (!seen.has(dependencyKey2(dependency))) {
3091
+ seen.add(dependencyKey2(dependency));
3092
+ pending.push(dependency);
2348
3093
  }
2349
3094
  }
2350
3095
  }
2351
3096
  return { direct, dependent };
2352
3097
  }
3098
+ function findChangedIndexKeys(previousIndex, currentIndex) {
3099
+ const changed = /* @__PURE__ */ new Set();
3100
+ for (const [key, previousValue] of Object.entries(previousIndex)) {
3101
+ if (currentIndex.get(key) !== previousValue) changed.add(key);
3102
+ }
3103
+ for (const key of currentIndex.keys()) {
3104
+ if (!(key in previousIndex)) changed.add(key);
3105
+ }
3106
+ return changed;
3107
+ }
2353
3108
  function addAllNotes(target, currentPaths, exclude) {
2354
- for (const path4 of currentPaths) {
2355
- if (!isMarkdownPath2(path4)) continue;
2356
- const slug = toDependencyKey(path4);
3109
+ for (const path5 of currentPaths) {
3110
+ if (!isMarkdownPath2(path5)) continue;
3111
+ const slug = toDependencyId(path5);
2357
3112
  if (exclude?.has(slug)) continue;
2358
3113
  target.add(slug);
2359
3114
  }
2360
3115
  }
2361
- function isMarkdownPath2(path4) {
2362
- return path4.endsWith(".md");
3116
+ function isMarkdownPath2(path5) {
3117
+ return path5.endsWith(".md");
3118
+ }
3119
+ function toDependencyId(path5) {
3120
+ return path5.replace(/\.md$/, "");
3121
+ }
3122
+ function toDependency(path5) {
3123
+ return isMarkdownPath2(path5) ? { kind: "content", id: toDependencyId(path5) } : { kind: "file", id: path5.replace(/\\/g, "/") };
3124
+ }
3125
+ function buildDependentsByDependency(state2) {
3126
+ const dependents = /* @__PURE__ */ new Map();
3127
+ for (const [path5, entry] of Object.entries(state2.entries)) {
3128
+ if (!isMarkdownPath2(path5)) continue;
3129
+ const slug = toDependencyId(path5);
3130
+ for (const dependency of entry.dependencies) {
3131
+ const key = dependencyKey2(dependency);
3132
+ const values = dependents.get(key) ?? /* @__PURE__ */ new Set();
3133
+ values.add(slug);
3134
+ dependents.set(key, values);
3135
+ }
3136
+ }
3137
+ return dependents;
2363
3138
  }
2364
- function toDependencyKey(path4) {
2365
- return path4.replace(/\.md$/, "");
3139
+ function dependencyKey2(dependency) {
3140
+ return `${dependency.kind}:${dependency.id}`;
2366
3141
  }
2367
3142
 
2368
3143
  // src/content/content_change_set.ts
@@ -2373,18 +3148,18 @@ function diffContentEntries(previous, current) {
2373
3148
  const added = [];
2374
3149
  const changed = [];
2375
3150
  const unchanged = [];
2376
- for (const [path4, fingerprint] of currentFingerprints) {
2377
- const previousEntry = previous.entries[path4];
3151
+ for (const [path5, fingerprint] of currentFingerprints) {
3152
+ const previousEntry = previous.entries[path5];
2378
3153
  if (!previousEntry) {
2379
- added.push(path4);
3154
+ added.push(path5);
2380
3155
  } else if (previousEntry.fingerprint !== fingerprint) {
2381
- changed.push(path4);
3156
+ changed.push(path5);
2382
3157
  } else {
2383
- unchanged.push(path4);
3158
+ unchanged.push(path5);
2384
3159
  }
2385
3160
  }
2386
3161
  const removed = Object.keys(previous.entries).filter(
2387
- (path4) => !currentFingerprints.has(path4)
3162
+ (path5) => !currentFingerprints.has(path5)
2388
3163
  );
2389
3164
  return sortChangeSet({ added, changed, removed, unchanged });
2390
3165
  }
@@ -2408,119 +3183,6 @@ function sortChangeSet(changeSet) {
2408
3183
  };
2409
3184
  }
2410
3185
 
2411
- // src/content/content_build_coordinator.ts
2412
- var ContentBuildCoordinator = class {
2413
- constructor(dependencies) {
2414
- this.dependencies = dependencies;
2415
- }
2416
- dependencies;
2417
- preparation = null;
2418
- getPreparation(incremental) {
2419
- this.preparation ??= this.prepare(incremental);
2420
- return this.preparation;
2421
- }
2422
- async commit(preparation, manifest) {
2423
- const entriesBySlug = new Map(
2424
- manifest.entries.map((entry) => [entry.slug, entry])
2425
- );
2426
- const state = {
2427
- version: CONTENT_BUILD_STATE_VERSION,
2428
- entries: Object.fromEntries(
2429
- preparation.currentEntries.map(({ entry, fingerprint }) => {
2430
- const manifestEntry = entriesBySlug.get(toSlug(entry.path));
2431
- return [
2432
- entry.path,
2433
- {
2434
- fingerprint,
2435
- dependencies: collectDependencies2(manifestEntry)
2436
- }
2437
- ];
2438
- })
2439
- ),
2440
- contentIndex: Object.fromEntries(
2441
- [...manifest.contentIndex.entries()].sort(
2442
- ([left], [right]) => left.localeCompare(right)
2443
- )
2444
- )
2445
- };
2446
- try {
2447
- await saveContentBuildState(this.dependencies.buildStatePath, state);
2448
- } catch {
2449
- }
2450
- }
2451
- async prepare(incremental) {
2452
- const currentEntries = await fingerprintContentEntries(
2453
- await this.dependencies.getEntries(),
2454
- (entry) => this.dependencies.readEntry(entry)
2455
- );
2456
- const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
2457
- const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
2458
- const affected = determineAffectedContent(
2459
- changeSet,
2460
- previousState,
2461
- currentEntries.map(({ entry }) => entry.path)
2462
- );
2463
- this.dependencies.observability.tracer.event("build.incremental", {
2464
- incremental: incremental !== false,
2465
- added: changeSet.added.length,
2466
- changed: changeSet.changed.length,
2467
- removed: changeSet.removed.length,
2468
- unchanged: changeSet.unchanged.length,
2469
- affected: affected.direct.size + affected.dependent.size
2470
- });
2471
- return { previousState, currentEntries, changeSet };
2472
- }
2473
- };
2474
- function toSlug(path4) {
2475
- return path4.replace(/\.md$/, "");
2476
- }
2477
- function collectDependencies2(manifestEntry) {
2478
- if (!manifestEntry) return [];
2479
- return [
2480
- ...new Set(
2481
- manifestEntry.links.filter(
2482
- (link) => link.slug !== null
2483
- ).map((link) => link.slug)
2484
- )
2485
- ].sort();
2486
- }
2487
-
2488
- // src/content/content_entry_reader.ts
2489
- var ContentEntryReader = class {
2490
- constructor(source, observability) {
2491
- this.source = source;
2492
- this.observability = observability;
2493
- }
2494
- source;
2495
- observability;
2496
- entries = null;
2497
- texts = /* @__PURE__ */ new Map();
2498
- async getEntries() {
2499
- this.entries ??= await this.observability.tracer.span(
2500
- "content.scan",
2501
- {},
2502
- () => this.source.scan()
2503
- );
2504
- return this.entries;
2505
- }
2506
- async readText(logicalPath) {
2507
- const entry = (await this.getEntries()).find(
2508
- (current) => current.path === logicalPath
2509
- );
2510
- if (!entry) throw new Error(`Content entry was not found: ${logicalPath}`);
2511
- return await this.read(entry);
2512
- }
2513
- read(entry) {
2514
- const cached = this.texts.get(entry.path);
2515
- if (cached) return cached;
2516
- const content = this.source.read(entry).then(
2517
- (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
2518
- );
2519
- this.texts.set(entry.path, content);
2520
- return content;
2521
- }
2522
- };
2523
-
2524
3186
  // src/utils/tags.ts
2525
3187
  function normalizeTag(raw) {
2526
3188
  const cleaned = raw.replace(/[/-]+$/, "");
@@ -2581,7 +3243,7 @@ function stripYamlQuotes(value) {
2581
3243
  }
2582
3244
 
2583
3245
  // src/content/content_index_builder.ts
2584
- var ContentIndexBuilder = class {
3246
+ var ContentIndexBuilder = class _ContentIndexBuilder {
2585
3247
  constructor(source) {
2586
3248
  this.source = source;
2587
3249
  }
@@ -2593,7 +3255,26 @@ var ContentIndexBuilder = class {
2593
3255
  }
2594
3256
  return index;
2595
3257
  }
3258
+ static buildFromAliases(contentEntries, aliasesByPath) {
3259
+ const index = /* @__PURE__ */ new Map();
3260
+ for (const contentEntry of contentEntries) {
3261
+ _ContentIndexBuilder.indexContentEntryFromAliases(
3262
+ index,
3263
+ contentEntry,
3264
+ aliasesByPath.get(contentEntry.path) ?? []
3265
+ );
3266
+ }
3267
+ return index;
3268
+ }
2596
3269
  async indexContentEntry(index, contentEntry, read) {
3270
+ const aliases = contentEntry.path.toLowerCase().endsWith(".md") ? extractFrontmatterAliases(readText(await read(contentEntry))) : [];
3271
+ _ContentIndexBuilder.indexContentEntryFromAliases(
3272
+ index,
3273
+ contentEntry,
3274
+ aliases
3275
+ );
3276
+ }
3277
+ static indexContentEntryFromAliases(index, contentEntry, aliases) {
2597
3278
  const contentPath = contentEntry.path;
2598
3279
  const ext = contentPath.split(".").pop()?.toLowerCase() ?? "";
2599
3280
  const value = ext === "md" ? contentPath.replace(/\.md$/, "") : contentPath;
@@ -2606,10 +3287,7 @@ var ContentIndexBuilder = class {
2606
3287
  }
2607
3288
  }
2608
3289
  if (ext === "md") {
2609
- const markdown = readText(await read(contentEntry));
2610
- for (const alias of extractFrontmatterAliases(markdown)) {
2611
- addIndexEntry(index, alias, value);
2612
- }
3290
+ for (const alias of aliases) addIndexEntry(index, alias, value);
2613
3291
  }
2614
3292
  }
2615
3293
  };
@@ -2621,72 +3299,362 @@ function addIndexEntry(index, key, value) {
2621
3299
  if (!index.has(normalizedKey)) index.set(normalizedKey, value);
2622
3300
  }
2623
3301
 
2624
- // src/content/content_location_resolver.ts
2625
- var ContentLocationResolver = class {
3302
+ // src/content/content_build_coordinator.ts
3303
+ var ContentBuildCoordinator = class {
2626
3304
  constructor(dependencies) {
2627
3305
  this.dependencies = dependencies;
2628
3306
  }
2629
3307
  dependencies;
2630
- locations = null;
2631
- async getLocations() {
2632
- if (this.locations) return this.locations;
2633
- const [entries, contentIndex] = await Promise.all([
2634
- this.dependencies.getEntries(),
2635
- this.dependencies.getContentIndex()
2636
- ]);
2637
- await this.dependencies.pluginRuntime.startBuild(contentIndex);
2638
- const inputs = await Promise.all(
2639
- entries.filter((entry) => entry.path.endsWith(".md")).map(async (entry) => ({
2640
- slug: toSlug2(entry.path),
2641
- path: entry.path,
2642
- markdown: await this.dependencies.readEntry(entry)
2643
- }))
3308
+ preparation = null;
3309
+ getPreparation(incremental, pipelineFingerprint, fullContentRegenerationRequired = false) {
3310
+ this.preparation ??= this.prepare(
3311
+ incremental,
3312
+ pipelineFingerprint,
3313
+ fullContentRegenerationRequired
2644
3314
  );
2645
- const locations = new Map(
2646
- inputs.map((input) => [input.slug, resolveDefaultContentLocation(input)])
3315
+ return this.preparation;
3316
+ }
3317
+ async commit(preparation, manifest, pipelineFingerprint, outputs = [], manifestEntries = manifest.entries, trackedDependencies = /* @__PURE__ */ new Map()) {
3318
+ const entriesBySlug = new Map(
3319
+ manifest.entries.map((entry) => [entry.slug, entry])
2647
3320
  );
2648
- for (const location of await this.dependencies.pluginRuntime.resolveContentLocations(
2649
- inputs,
2650
- contentIndex
2651
- )) {
2652
- if (!locations.has(location.slug)) {
2653
- throw new Error(
2654
- `Content public location references an unknown slug: ${location.slug}`
2655
- );
2656
- }
2657
- locations.set(location.slug, location);
3321
+ const state2 = {
3322
+ version: CONTENT_BUILD_STATE_VERSION,
3323
+ entries: Object.fromEntries(
3324
+ preparation.currentEntries.map(({ entry, fingerprint }) => {
3325
+ const manifestEntry = entriesBySlug.get(toSlug(entry.path));
3326
+ return [
3327
+ entry.path,
3328
+ {
3329
+ fingerprint,
3330
+ aliases: preparation.currentContentAliases.get(entry.path) ?? [],
3331
+ dependencies: collectDependencies2(
3332
+ manifestEntry,
3333
+ trackedDependencies.get(toSlug(entry.path)) ?? preparation.previousState?.entries[entry.path]?.dependencies ?? []
3334
+ ),
3335
+ linkTargets: collectLinkTargets(manifestEntry)
3336
+ }
3337
+ ];
3338
+ })
3339
+ ),
3340
+ contentIndex: Object.fromEntries(
3341
+ [...manifest.contentIndex.entries()].sort(
3342
+ ([left], [right]) => left.localeCompare(right)
3343
+ )
3344
+ ),
3345
+ pipelineFingerprint,
3346
+ manifestEntries,
3347
+ outputs
3348
+ };
3349
+ try {
3350
+ await saveContentBuildState(this.dependencies.buildStatePath, state2);
3351
+ } catch (error) {
3352
+ this.dependencies.observability.logger.warn(
3353
+ "Incremental build state could not be persisted; the next build regenerates every note instead of reusing stale output.",
3354
+ {
3355
+ path: this.dependencies.buildStatePath,
3356
+ reason: error instanceof Error ? error.message : String(error)
3357
+ }
3358
+ );
2658
3359
  }
2659
- await this.dependencies.pluginRuntime.extendContentLocations(
2660
- inputs,
2661
- locations,
2662
- contentIndex
2663
- );
2664
- this.locations = locations;
2665
- return locations;
2666
3360
  }
2667
- async getPermalinks() {
2668
- return new Map(
2669
- Array.from(
2670
- (await this.getLocations()).entries(),
2671
- ([slug, location]) => [slug, location.permalink]
3361
+ async prepare(incremental, pipelineFingerprint, fullContentRegenerationRequired) {
3362
+ const entries = await this.dependencies.observability.tracer.span(
3363
+ "content.discovery",
3364
+ {},
3365
+ () => this.dependencies.getEntries()
3366
+ );
3367
+ const currentEntries = await this.dependencies.observability.tracer.span(
3368
+ "content.fingerprint",
3369
+ {},
3370
+ () => fingerprintContentEntries(
3371
+ entries,
3372
+ (entry) => this.dependencies.readEntry(entry)
2672
3373
  )
2673
3374
  );
2674
- }
2675
- populateRedirects(manifest, locations) {
2676
- for (const [slug, location] of locations) {
2677
- for (const redirect of location.redirects ?? []) {
3375
+ const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
3376
+ const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
3377
+ const currentContentAliases = await this.getContentAliases(
3378
+ currentEntries,
3379
+ previousState,
3380
+ changeSet
3381
+ );
3382
+ const currentContentIndex = await this.dependencies.observability.tracer.span(
3383
+ "content.index",
3384
+ {},
3385
+ () => ContentIndexBuilder.buildFromAliases(
3386
+ currentEntries.map(({ entry }) => entry),
3387
+ currentContentAliases
3388
+ )
3389
+ );
3390
+ const affected = !fullContentRegenerationRequired && previousState?.pipelineFingerprint === pipelineFingerprint ? determineAffectedContent(
3391
+ changeSet,
3392
+ previousState,
3393
+ currentEntries.map(({ entry }) => entry.path),
3394
+ currentContentIndex
3395
+ ) : determineAffectedContent(
3396
+ allContentChanged(currentEntries),
3397
+ void 0,
3398
+ currentEntries.map(({ entry }) => entry.path),
3399
+ currentContentIndex
3400
+ );
3401
+ this.dependencies.observability.tracer.event("build.incremental", {
3402
+ incremental: incremental !== false,
3403
+ added: changeSet.added.length,
3404
+ changed: changeSet.changed.length,
3405
+ removed: changeSet.removed.length,
3406
+ unchanged: changeSet.unchanged.length,
3407
+ affected: affected.direct.size + affected.dependent.size
3408
+ });
3409
+ return {
3410
+ previousState,
3411
+ previousManifestEntriesBySlug: new Map(
3412
+ (previousState?.manifestEntries ?? []).map((entry) => [
3413
+ entry.slug,
3414
+ entry
3415
+ ])
3416
+ ),
3417
+ currentEntries,
3418
+ currentContentAliases,
3419
+ currentContentIndex,
3420
+ changeSet,
3421
+ affectedContent: affected
3422
+ };
3423
+ }
3424
+ async getContentAliases(currentEntries, previousState, changeSet) {
3425
+ const changedPaths = /* @__PURE__ */ new Set([...changeSet.added, ...changeSet.changed]);
3426
+ const aliases = /* @__PURE__ */ new Map();
3427
+ for (const { entry } of currentEntries) {
3428
+ if (!entry.path.toLowerCase().endsWith(".md")) continue;
3429
+ const previousAliases = previousState?.entries[entry.path]?.aliases;
3430
+ if (!changedPaths.has(entry.path) && previousAliases) {
3431
+ aliases.set(entry.path, previousAliases);
3432
+ continue;
3433
+ }
3434
+ aliases.set(
3435
+ entry.path,
3436
+ extractFrontmatterAliases(
3437
+ readText2(await this.dependencies.readEntry(entry))
3438
+ )
3439
+ );
3440
+ }
3441
+ return aliases;
3442
+ }
3443
+ };
3444
+ function readText2(content) {
3445
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
3446
+ }
3447
+ function toSlug(path5) {
3448
+ return path5.replace(/\.md$/, "");
3449
+ }
3450
+ function collectDependencies2(manifestEntry, trackedDependencies) {
3451
+ if (!manifestEntry) return [];
3452
+ return [
3453
+ ...new Map(
3454
+ [
3455
+ ...manifestEntry.links.filter(
3456
+ (link) => link.slug !== null
3457
+ ).map((link) => ({ kind: "content", id: link.slug })),
3458
+ ...manifestEntry.assets.map((asset) => ({
3459
+ kind: "file",
3460
+ id: asset.path.replace(/\\/g, "/")
3461
+ })),
3462
+ ...trackedDependencies.filter(
3463
+ (dependency) => dependency.kind === "content" || dependency.kind === "file"
3464
+ ).map((dependency) => ({
3465
+ kind: dependency.kind,
3466
+ id: dependency.id.replace(/\\/g, "/")
3467
+ }))
3468
+ ].map((dependency) => [
3469
+ `${dependency.kind}:${dependency.id}`,
3470
+ dependency
3471
+ ])
3472
+ ).values()
3473
+ ].toSorted(
3474
+ (left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)
3475
+ );
3476
+ }
3477
+ function collectLinkTargets(manifestEntry) {
3478
+ if (!manifestEntry) return [];
3479
+ return [
3480
+ ...new Set(manifestEntry.links.map((link) => link.raw.toLowerCase()))
3481
+ ].sort();
3482
+ }
3483
+
3484
+ // src/content/content_entry_reader.ts
3485
+ var ContentEntryReader = class {
3486
+ constructor(source, observability) {
3487
+ this.source = source;
3488
+ this.observability = observability;
3489
+ }
3490
+ source;
3491
+ observability;
3492
+ entries = null;
3493
+ entriesPromise = null;
3494
+ entriesByPath = null;
3495
+ texts = /* @__PURE__ */ new Map();
3496
+ async getEntries() {
3497
+ if (this.entries) return this.entries;
3498
+ if (this.entriesPromise) return await this.entriesPromise;
3499
+ this.entriesPromise = this.observability.tracer.span(
3500
+ "content.scan",
3501
+ {},
3502
+ () => this.source.scan()
3503
+ );
3504
+ try {
3505
+ this.entries = await this.entriesPromise;
3506
+ this.entriesByPath = new Map(
3507
+ this.entries.map((entry) => [entry.path, entry])
3508
+ );
3509
+ return this.entries;
3510
+ } finally {
3511
+ this.entriesPromise = null;
3512
+ }
3513
+ }
3514
+ async readText(logicalPath) {
3515
+ await this.getEntries();
3516
+ const entry = this.entriesByPath?.get(logicalPath);
3517
+ if (!entry) throw new Error(`Content entry was not found: ${logicalPath}`);
3518
+ return await this.read(entry);
3519
+ }
3520
+ read(entry) {
3521
+ const cached = this.texts.get(entry.path);
3522
+ if (cached) return cached;
3523
+ const content = this.source.read(entry).then(
3524
+ (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
3525
+ ).finally(() => this.texts.delete(entry.path));
3526
+ this.texts.set(entry.path, content);
3527
+ return content;
3528
+ }
3529
+ };
3530
+
3531
+ // src/content/content_location_resolver.ts
3532
+ var ContentLocationResolver = class {
3533
+ constructor(dependencies) {
3534
+ this.dependencies = dependencies;
3535
+ }
3536
+ dependencies;
3537
+ locations = null;
3538
+ locationsPromise = null;
3539
+ permalinks = null;
3540
+ permalinksPromise = null;
3541
+ async getLocations() {
3542
+ if (this.locations) return this.locations;
3543
+ if (this.locationsPromise) return await this.locationsPromise;
3544
+ this.locationsPromise = this.dependencies.observability.tracer.span(
3545
+ "content.locations",
3546
+ {},
3547
+ () => this.resolveLocations()
3548
+ );
3549
+ try {
3550
+ return await this.locationsPromise;
3551
+ } finally {
3552
+ this.locationsPromise = null;
3553
+ }
3554
+ }
3555
+ async resolveLocations() {
3556
+ const [entries, contentIndex] = await Promise.all([
3557
+ this.dependencies.getEntries(),
3558
+ this.dependencies.getContentIndex()
3559
+ ]);
3560
+ await this.dependencies.pluginRuntime.startBuild(contentIndex);
3561
+ const inputs = await mapConcurrent2(
3562
+ entries.filter((entry) => entry.path.endsWith(".md")),
3563
+ 64,
3564
+ async (entry) => ({
3565
+ slug: toSlug2(entry.path),
3566
+ path: entry.path,
3567
+ markdown: await this.dependencies.readEntry(entry)
3568
+ })
3569
+ );
3570
+ const locations = new Map(
3571
+ inputs.map((input) => [input.slug, resolveDefaultContentLocation(input)])
3572
+ );
3573
+ for (const location of await this.dependencies.pluginRuntime.resolveContentLocations(
3574
+ inputs,
3575
+ contentIndex
3576
+ )) {
3577
+ if (!locations.has(location.slug)) {
3578
+ throw new Error(
3579
+ `Content public location references an unknown slug: ${location.slug}`
3580
+ );
3581
+ }
3582
+ locations.set(location.slug, location);
3583
+ }
3584
+ await this.dependencies.pluginRuntime.extendContentLocations(
3585
+ inputs,
3586
+ locations,
3587
+ contentIndex
3588
+ );
3589
+ this.locations = locations;
3590
+ return locations;
3591
+ }
3592
+ async getPermalinks() {
3593
+ if (this.permalinks) return this.permalinks;
3594
+ if (this.permalinksPromise) return await this.permalinksPromise;
3595
+ this.permalinksPromise = this.createPermalinks();
3596
+ try {
3597
+ this.permalinks = await this.permalinksPromise;
3598
+ return this.permalinks;
3599
+ } finally {
3600
+ this.permalinksPromise = null;
3601
+ }
3602
+ }
3603
+ async createPermalinks() {
3604
+ return new Map(
3605
+ Array.from(
3606
+ (await this.getLocations()).entries(),
3607
+ ([slug, location]) => [slug, location.permalink]
3608
+ )
3609
+ );
3610
+ }
3611
+ populateRedirects(manifest, locations) {
3612
+ for (const [slug, location] of locations) {
3613
+ for (const redirect of location.redirects ?? []) {
2678
3614
  manifest.redirects.set(redirect.path, { ...redirect, slug });
2679
3615
  }
2680
3616
  }
2681
3617
  }
2682
3618
  };
2683
- function toSlug2(path4) {
2684
- return path4.replace(/\.md$/, "");
3619
+ function toSlug2(path5) {
3620
+ return path5.replace(/\.md$/, "");
3621
+ }
3622
+ async function mapConcurrent2(values, concurrency, map) {
3623
+ const results = new Array(values.length);
3624
+ let nextIndex = 0;
3625
+ async function worker() {
3626
+ while (nextIndex < values.length) {
3627
+ const index = nextIndex;
3628
+ nextIndex += 1;
3629
+ const value = values[index];
3630
+ if (value !== void 0) results[index] = await map(value);
3631
+ }
3632
+ }
3633
+ await Promise.all(
3634
+ Array.from(
3635
+ { length: Math.min(concurrency, values.length) },
3636
+ () => worker()
3637
+ )
3638
+ );
3639
+ return results;
2685
3640
  }
2686
3641
 
2687
3642
  // src/content/file_system_content_source.ts
2688
- import fs3 from "node:fs/promises";
2689
- import path3 from "node:path";
3643
+ import fs4 from "node:fs/promises";
3644
+ import path4 from "node:path";
3645
+ var INTERNAL_CONTENT_IGNORE_PATTERNS = [
3646
+ ".git",
3647
+ ".git/**",
3648
+ ".github",
3649
+ ".github/**",
3650
+ ".obsidian",
3651
+ ".obsidian/**",
3652
+ "node_modules",
3653
+ "node_modules/**",
3654
+ "**/.DS_Store",
3655
+ "**/Thumbs.db",
3656
+ "**/desktop.ini"
3657
+ ];
2690
3658
  var FileSystemContentSource = class {
2691
3659
  constructor(contentDirectory, exclude = []) {
2692
3660
  this.contentDirectory = contentDirectory;
@@ -2695,33 +3663,41 @@ var FileSystemContentSource = class {
2695
3663
  contentDirectory;
2696
3664
  exclude;
2697
3665
  async scan() {
2698
- return await this.scanDirectory(this.contentDirectory, "");
3666
+ try {
3667
+ return await this.scanDirectory(this.contentDirectory, "");
3668
+ } catch (error) {
3669
+ if (isMissingDirectoryError(error, this.contentDirectory)) {
3670
+ throw missingContentDirectoryError(this.contentDirectory);
3671
+ }
3672
+ throw error;
3673
+ }
2699
3674
  }
2700
3675
  async read(entry) {
2701
3676
  const filePath = this.resolveFilePath(entry.path);
2702
- const stats = await fs3.lstat(filePath);
3677
+ const stats = await fs4.lstat(filePath);
2703
3678
  if (!stats.isFile() || stats.isSymbolicLink()) {
2704
3679
  throw new Error(`Content entry is not a regular file: ${entry.path}`);
2705
3680
  }
2706
- return await fs3.readFile(filePath);
3681
+ return await fs4.readFile(filePath);
2707
3682
  }
2708
- async scanDirectory(directory, parentPath) {
2709
- const directoryEntries = await fs3.readdir(directory, {
3683
+ async scanDirectory(directory, parentPath2) {
3684
+ const directoryEntries = await fs4.readdir(directory, {
2710
3685
  withFileTypes: true
2711
3686
  });
2712
3687
  const entries = [];
2713
3688
  for (const directoryEntry of directoryEntries) {
2714
3689
  if (directoryEntry.isSymbolicLink()) continue;
2715
3690
  const logicalPath = normalizeContentPath(
2716
- parentPath ? `${parentPath}/${directoryEntry.name}` : directoryEntry.name
3691
+ parentPath2 ? `${parentPath2}/${directoryEntry.name}` : directoryEntry.name
2717
3692
  );
2718
- const filePath = path3.join(directory, directoryEntry.name);
3693
+ const filePath = path4.join(directory, directoryEntry.name);
3694
+ if (this.isExcluded(logicalPath)) continue;
2719
3695
  if (directoryEntry.isDirectory()) {
2720
3696
  entries.push(...await this.scanDirectory(filePath, logicalPath));
2721
3697
  continue;
2722
3698
  }
2723
- if (!directoryEntry.isFile() || this.isExcluded(logicalPath)) continue;
2724
- const stats = await fs3.stat(filePath);
3699
+ if (!directoryEntry.isFile()) continue;
3700
+ const stats = await fs4.stat(filePath);
2725
3701
  entries.push({
2726
3702
  path: logicalPath,
2727
3703
  metadata: {
@@ -2733,26 +3709,44 @@ var FileSystemContentSource = class {
2733
3709
  return entries;
2734
3710
  }
2735
3711
  isExcluded(logicalPath) {
2736
- if (isExcluded(this.exclude, logicalPath)) return true;
2737
- return logicalPath.endsWith(".md") && isExcluded(this.exclude, logicalPath.slice(0, -3));
3712
+ return isIgnoredContentPath(logicalPath, this.exclude);
2738
3713
  }
2739
3714
  resolveFilePath(logicalPath) {
2740
3715
  const normalizedPath = normalizeLogicalPath(logicalPath);
2741
- const filePath = path3.resolve(this.contentDirectory, normalizedPath);
2742
- const relativePath = path3.relative(this.contentDirectory, filePath);
2743
- if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
3716
+ const filePath = path4.resolve(this.contentDirectory, normalizedPath);
3717
+ const relativePath = path4.relative(this.contentDirectory, filePath);
3718
+ if (relativePath.startsWith("..") || path4.isAbsolute(relativePath)) {
2744
3719
  throw new Error(`Invalid content path: ${logicalPath}`);
2745
3720
  }
2746
3721
  return filePath;
2747
3722
  }
2748
3723
  };
3724
+ function isIgnoredContentPath(logicalPath, exclude = []) {
3725
+ if (isExcluded(INTERNAL_CONTENT_IGNORE_PATTERNS, logicalPath)) return true;
3726
+ if (isExcluded(exclude, logicalPath)) return true;
3727
+ return logicalPath.endsWith(".md") && isExcluded(exclude, logicalPath.slice(0, -3));
3728
+ }
2749
3729
  function normalizeLogicalPath(logicalPath) {
2750
3730
  const normalizedPath = normalizeContentPath(logicalPath);
2751
- if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path3.isAbsolute(normalizedPath) || path3.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
3731
+ if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path4.isAbsolute(normalizedPath) || path4.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
2752
3732
  throw new Error(`Invalid content path: ${logicalPath}`);
2753
3733
  }
2754
3734
  return normalizedPath;
2755
3735
  }
3736
+ function isMissingDirectoryError(error, directory) {
3737
+ return error instanceof Error && "code" in error && error.code === "ENOENT" && "path" in error && error.path === directory;
3738
+ }
3739
+ function missingContentDirectoryError(directory) {
3740
+ const relative = path4.relative(process.cwd(), directory);
3741
+ const display = relative && !relative.startsWith("..") ? relative : directory;
3742
+ const error = new Error(
3743
+ `Could not find the content directory:
3744
+
3745
+ ${display}`
3746
+ );
3747
+ error.hint = "Check `content.directory` in your Riebeckite configuration.";
3748
+ return error;
3749
+ }
2756
3750
 
2757
3751
  // src/content/content_links.ts
2758
3752
  var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
@@ -2808,7 +3802,7 @@ function readOptionalStableId(value, field) {
2808
3802
 
2809
3803
  // src/content/manifest_builder.ts
2810
3804
  var ManifestBuilder = class {
2811
- createEntry(slug, markdown, processed, contentIndex, location) {
3805
+ createEntry(slug, markdown, processed, contentIndex, location, publishing) {
2812
3806
  const links = extractContentLinks(markdown, contentIndex);
2813
3807
  const assets = links.filter(
2814
3808
  (link) => (link.kind === "image" || link.kind === "attachment") && link.slug !== null
@@ -2820,7 +3814,9 @@ var ManifestBuilder = class {
2820
3814
  permalink: location.permalink,
2821
3815
  publicLocation: location,
2822
3816
  title: getManifestTitle(slug, processed.frontmatter.title),
3817
+ aliases: extractFrontmatterAliases(markdown),
2823
3818
  frontmatter: processed.frontmatter,
3819
+ publishing,
2824
3820
  html: processed.html,
2825
3821
  tags: uniqueStrings([
2826
3822
  ...normalizeFrontmatterTags(processed.frontmatter.tags),
@@ -2834,6 +3830,7 @@ var ManifestBuilder = class {
2834
3830
  build(entries, contentIndex) {
2835
3831
  const bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
2836
3832
  const byContentId = createContentIdIndex(entries);
3833
+ const byAlias = createAliasIndex(entries);
2837
3834
  const byPermalink = new Map(
2838
3835
  entries.map((entry) => [entry.permalink, entry])
2839
3836
  );
@@ -2858,9 +3855,12 @@ var ManifestBuilder = class {
2858
3855
  const manifest = {
2859
3856
  entries,
2860
3857
  publicEntries: [...entries],
3858
+ discoverableEntries: [...entries],
2861
3859
  bySlug,
2862
3860
  byContentId,
3861
+ byAlias,
2863
3862
  byPermalink,
3863
+ byRoutablePermalink: new Map(byPermalink),
2864
3864
  redirects: /* @__PURE__ */ new Map(),
2865
3865
  publicRedirects: /* @__PURE__ */ new Map(),
2866
3866
  byTag,
@@ -2872,11 +3872,21 @@ var ManifestBuilder = class {
2872
3872
  clientEntries: [],
2873
3873
  diagnostics: [],
2874
3874
  generatedOutputs: [],
3875
+ folderLocations: /* @__PURE__ */ new Map(),
3876
+ pageRoutes: [],
2875
3877
  pagePaths: []
2876
3878
  };
2877
3879
  return { ...manifest, graph: createContentGraph(manifest) };
2878
3880
  }
2879
3881
  };
3882
+ function createAliasIndex(entries) {
3883
+ const byAlias = /* @__PURE__ */ new Map();
3884
+ for (const entry of entries) {
3885
+ for (const alias of entry.aliases ?? [])
3886
+ appendToMap(byAlias, alias.toLowerCase(), entry);
3887
+ }
3888
+ return byAlias;
3889
+ }
2880
3890
  function createContentIdIndex(entries) {
2881
3891
  const byContentId = /* @__PURE__ */ new Map();
2882
3892
  for (const entry of entries) {
@@ -2913,21 +3923,283 @@ function appendUniqueToMap(map, key, value) {
2913
3923
  map.set(key, values);
2914
3924
  }
2915
3925
 
3926
+ // src/content/output_dependency.ts
3927
+ function determineOutputChanges(input) {
3928
+ const current = buildOutputInventory(
3929
+ input.manifest,
3930
+ input.pluginPageOutputs,
3931
+ input.contentOutputDependencies
3932
+ );
3933
+ const previous = new Map(
3934
+ (input.previousState?.outputs ?? []).map((output) => [output.path, output])
3935
+ );
3936
+ const changedContent = collectChangedContent(input);
3937
+ const changedTags = collectChangedTags(
3938
+ input.previousState,
3939
+ input.manifest,
3940
+ changedContent
3941
+ );
3942
+ const changedFolders = collectChangedFolders(
3943
+ input.previousState,
3944
+ input.manifest,
3945
+ changedContent
3946
+ );
3947
+ const affected = /* @__PURE__ */ new Map();
3948
+ const unchanged = [];
3949
+ for (const output of current) {
3950
+ if (isOutputAffected(output, changedContent, changedTags, changedFolders)) {
3951
+ affected.set(output.path, output);
3952
+ } else {
3953
+ unchanged.push(output);
3954
+ }
3955
+ }
3956
+ for (const output of current) {
3957
+ const previousOutput = previous.get(output.path);
3958
+ if (!previousOutput || output.kind !== previousOutput.kind) {
3959
+ affected.set(output.path, output);
3960
+ }
3961
+ }
3962
+ const currentPaths = new Set(current.map((output) => output.path));
3963
+ const removed = [...previous.values()].filter(
3964
+ (output) => !currentPaths.has(output.path)
3965
+ );
3966
+ const fullRegenerationRequired = current.some(
3967
+ (output) => output.dependencies.some((dependency) => dependency.type === "unknown")
3968
+ );
3969
+ const affectedValues = [...affected.values()].sort(compareOutput);
3970
+ const unchangedValues = unchanged.filter((output) => !affected.has(output.path)).sort(compareOutput);
3971
+ return {
3972
+ affected: affectedValues,
3973
+ removed: removed.sort(compareOutput),
3974
+ unchanged: unchangedValues,
3975
+ fullRegenerationRequired,
3976
+ candidateOutputCount: current.length,
3977
+ affectedOutputCount: affectedValues.length,
3978
+ removedOutputCount: removed.length,
3979
+ unchangedOutputCount: unchangedValues.length
3980
+ };
3981
+ }
3982
+ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDependencies = []) {
3983
+ const outputs = /* @__PURE__ */ new Map();
3984
+ for (const entry of manifest.publicEntries) {
3985
+ addOutput(outputs, contentOutput(entry, contentOutputDependencies));
3986
+ }
3987
+ for (const [path5, redirect] of manifest.publicRedirects) {
3988
+ addOutput(outputs, {
3989
+ kind: "redirect",
3990
+ path: htmlOutputPath(path5),
3991
+ producer: `content:${redirect.slug}:redirect`,
3992
+ dependencies: [{ type: "content", slug: redirect.slug }]
3993
+ });
3994
+ }
3995
+ for (const output of pluginPageOutputs) {
3996
+ addOutput(outputs, { ...output, path: htmlOutputPath(output.path) });
3997
+ }
3998
+ for (const output of manifest.generatedOutputs) {
3999
+ addOutput(outputs, {
4000
+ kind: "generated",
4001
+ path: output.path,
4002
+ producer: `plugin:${output.owner}`,
4003
+ dependencies: output.dependencies ?? [{ type: "unknown" }]
4004
+ });
4005
+ }
4006
+ return [...outputs.values()].sort(compareOutput);
4007
+ }
4008
+ function htmlOutputPath(pathname) {
4009
+ const normalized = normalizePublicPath(pathname);
4010
+ if (normalized === "/") return "index.html";
4011
+ const withoutSlash = normalized.replace(/^\//, "");
4012
+ if (pathname.endsWith("/")) return `${withoutSlash}/index.html`;
4013
+ return `${withoutSlash}.html`;
4014
+ }
4015
+ function contentOutput(entry, dependencies) {
4016
+ return {
4017
+ kind: "content",
4018
+ path: htmlOutputPath(entry.permalink),
4019
+ producer: `content:${entry.slug}`,
4020
+ dependencies: [{ type: "content", slug: entry.slug }, ...dependencies]
4021
+ };
4022
+ }
4023
+ function isOutputAffected(output, changedContent, changedTags, changedFolders) {
4024
+ for (const dependency of output.dependencies) {
4025
+ if (dependency.type === "unknown") return true;
4026
+ if (dependency.type === "global") return changedContent.size > 0;
4027
+ if (dependency.type === "content" && changedContent.has(dependency.slug)) {
4028
+ return true;
4029
+ }
4030
+ if (dependency.type === "tag" && changedTags.has(dependency.tag))
4031
+ return true;
4032
+ if (dependency.type === "folder" && changedFolders.has(dependency.folder)) {
4033
+ return true;
4034
+ }
4035
+ }
4036
+ return false;
4037
+ }
4038
+ function collectChangedContent(input) {
4039
+ const changed = /* @__PURE__ */ new Set([
4040
+ ...input.affectedContent.direct,
4041
+ ...input.affectedContent.dependent,
4042
+ ...input.changeSet.removed.filter(isMarkdownPath3).map(toSlug3)
4043
+ ]);
4044
+ const previousEntries = input.previousState?.manifestEntries ?? [];
4045
+ const currentEntries = input.manifest.entries;
4046
+ const previousBySlug = new Map(
4047
+ previousEntries.map((entry) => [entry.slug, entry])
4048
+ );
4049
+ const currentBySlug = new Map(
4050
+ currentEntries.map((entry) => [entry.slug, entry])
4051
+ );
4052
+ for (const slug of changed) {
4053
+ for (const target of linkTargets(previousBySlug.get(slug)))
4054
+ changed.add(target);
4055
+ for (const target of linkTargets(currentBySlug.get(slug)))
4056
+ changed.add(target);
4057
+ }
4058
+ return changed;
4059
+ }
4060
+ function collectChangedTags(previousState, manifest, changedContent) {
4061
+ const previousBySlug = new Map(
4062
+ (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
4063
+ );
4064
+ const currentBySlug = new Map(
4065
+ manifest.entries.map((entry) => [entry.slug, entry])
4066
+ );
4067
+ const tags = /* @__PURE__ */ new Set();
4068
+ for (const slug of changedContent) {
4069
+ for (const tag of previousBySlug.get(slug)?.tags ?? []) tags.add(tag);
4070
+ for (const tag of currentBySlug.get(slug)?.tags ?? []) tags.add(tag);
4071
+ }
4072
+ return tags;
4073
+ }
4074
+ function collectChangedFolders(previousState, manifest, changedContent) {
4075
+ const previousBySlug = new Map(
4076
+ (previousState?.manifestEntries ?? []).map((entry) => [entry.slug, entry])
4077
+ );
4078
+ const currentBySlug = new Map(
4079
+ manifest.entries.map((entry) => [entry.slug, entry])
4080
+ );
4081
+ const folders = /* @__PURE__ */ new Set();
4082
+ for (const slug of changedContent) {
4083
+ addFolderAncestors(folders, previousBySlug.get(slug)?.slug ?? slug);
4084
+ addFolderAncestors(folders, currentBySlug.get(slug)?.slug ?? slug);
4085
+ }
4086
+ return folders;
4087
+ }
4088
+ function addFolderAncestors(folders, slug) {
4089
+ const segments = slug.split("/");
4090
+ segments.pop();
4091
+ while (true) {
4092
+ folders.add(segments.join("/"));
4093
+ if (segments.length === 0) return;
4094
+ segments.pop();
4095
+ }
4096
+ }
4097
+ function linkTargets(entry) {
4098
+ if (!entry) return [];
4099
+ return entry.links.filter(
4100
+ (link) => link.slug !== null
4101
+ ).map((link) => link.slug);
4102
+ }
4103
+ function addOutput(outputs, output) {
4104
+ outputs.set(output.path, output);
4105
+ }
4106
+ function normalizePublicPath(pathname) {
4107
+ const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4108
+ return path5 === "/" ? path5 : path5.replace(/\/$/, "");
4109
+ }
4110
+ function isMarkdownPath3(path5) {
4111
+ return path5.endsWith(".md");
4112
+ }
4113
+ function toSlug3(path5) {
4114
+ return path5.replace(/\.md$/, "");
4115
+ }
4116
+ function compareOutput(left, right) {
4117
+ return left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind);
4118
+ }
4119
+
4120
+ // src/content/publishing.ts
4121
+ function resolvePublishingState(frontmatter, options) {
4122
+ const explicitVisibility = resolveExplicitVisibility(frontmatter?.visibility);
4123
+ const publishAt = resolvePublishAt(frontmatter?.publishAt);
4124
+ if (explicitVisibility === "draft")
4125
+ return state("draft", false, false, publishAt);
4126
+ if (publishAt && publishAt.getTime() > options.buildTime.getTime()) {
4127
+ return state("scheduled", false, false, publishAt);
4128
+ }
4129
+ if (explicitVisibility === "unlisted") {
4130
+ return state("unlisted", true, false, publishAt);
4131
+ }
4132
+ if (explicitVisibility === "public" || publishAt) {
4133
+ return state("public", true, true, publishAt);
4134
+ }
4135
+ const routable = isPublishable(options.strategy, frontmatter);
4136
+ return state(routable ? "public" : "draft", routable, routable, publishAt);
4137
+ }
4138
+ function resolvePublishingBuildTime(input) {
4139
+ if (input === void 0) return /* @__PURE__ */ new Date();
4140
+ if (input instanceof Date) {
4141
+ if (Number.isNaN(input.getTime()))
4142
+ throw new Error("Invalid publishing build time.");
4143
+ return new Date(input.getTime());
4144
+ }
4145
+ const parsed = new Date(input);
4146
+ if (Number.isNaN(parsed.getTime()))
4147
+ throw new Error("Invalid publishing build time.");
4148
+ return parsed;
4149
+ }
4150
+ function resolveExplicitVisibility(value) {
4151
+ if (value === void 0) return void 0;
4152
+ if (value === "public" || value === "unlisted" || value === "draft")
4153
+ return value;
4154
+ throw new Error(
4155
+ `Invalid content visibility "${String(value)}". Expected public, unlisted, or draft.`
4156
+ );
4157
+ }
4158
+ function resolvePublishAt(value) {
4159
+ if (value === void 0) return void 0;
4160
+ if (!(typeof value === "string" || value instanceof Date)) {
4161
+ throw new Error("Invalid publishAt. Expected an ISO 8601 date string.");
4162
+ }
4163
+ const parsed = value instanceof Date ? value : new Date(value);
4164
+ if (Number.isNaN(parsed.getTime())) {
4165
+ throw new Error(
4166
+ `Invalid publishAt "${String(value)}". Expected an ISO 8601 date string.`
4167
+ );
4168
+ }
4169
+ return new Date(parsed.getTime());
4170
+ }
4171
+ function state(visibility, routable, discoverable, publishAt) {
4172
+ return {
4173
+ visibility,
4174
+ routable,
4175
+ discoverable,
4176
+ ...publishAt ? { publishAt } : {}
4177
+ };
4178
+ }
4179
+
2916
4180
  // src/content/content_manager.ts
4181
+ var CONTENT_PROCESSING_CONCURRENCY = 16;
2917
4182
  var ContentManager = class {
2918
4183
  constructor(content, exclude = [], pipelineOptions = {}) {
2919
4184
  this.pipelineOptions = pipelineOptions;
2920
4185
  this.pipelineOptions = {
2921
4186
  ...pipelineOptions,
2922
- plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins
4187
+ plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins,
4188
+ isRoutable: (slug) => this.isRoutable(slug)
2923
4189
  };
4190
+ this.publishingBuildTime = resolvePublishingBuildTime(
4191
+ pipelineOptions.publishingBuildTime
4192
+ );
2924
4193
  this.source = typeof content === "string" ? pipelineOptions.config?.content.source ?? new FileSystemContentSource(content, [
2925
4194
  ...exclude,
2926
4195
  CONTENT_BUILD_STATE_EXCLUDE
2927
4196
  ]) : content;
2928
4197
  this.pipelineOptions = {
2929
4198
  ...this.pipelineOptions,
2930
- contentSource: this.source
4199
+ contentSource: this.source,
4200
+ onContentDependencies: (slug, dependencies) => {
4201
+ this.trackedContentDependencies.set(slug, dependencies);
4202
+ }
2931
4203
  };
2932
4204
  this.contentIndexBuilder = new ContentIndexBuilder(this.source);
2933
4205
  this.pluginRuntime = new PluginRuntime(this.pipelineOptions);
@@ -2941,7 +4213,8 @@ var ContentManager = class {
2941
4213
  getEntries: () => this.entryReader.getEntries(),
2942
4214
  readEntry: (entry) => this.entryReader.read(entry),
2943
4215
  getContentIndex: () => this.getContentIndex(),
2944
- pluginRuntime: this.pluginRuntime
4216
+ pluginRuntime: this.pluginRuntime,
4217
+ observability
2945
4218
  });
2946
4219
  this.buildCoordinator = new ContentBuildCoordinator({
2947
4220
  buildStatePath,
@@ -2956,13 +4229,22 @@ var ContentManager = class {
2956
4229
  manifestBuilder = new ManifestBuilder();
2957
4230
  pluginRuntime;
2958
4231
  contentIndex = null;
4232
+ contentIndexPromise = null;
2959
4233
  contentCache = /* @__PURE__ */ new Map();
2960
4234
  manifest = null;
4235
+ manifestPromise = null;
4236
+ pipelineFingerprint = null;
2961
4237
  pipeline = null;
2962
4238
  entryReader;
2963
4239
  locationResolver;
2964
4240
  buildCoordinator;
2965
4241
  isBuildTime = false;
4242
+ publishingBuildTime;
4243
+ routableSlugs = null;
4244
+ routableSlugsPromise = null;
4245
+ processedContentCount = 0;
4246
+ outputChangeSet = null;
4247
+ trackedContentDependencies = /* @__PURE__ */ new Map();
2966
4248
  async getAllPosts() {
2967
4249
  return (await this.scan()).filter((entry) => entry.path.endsWith(".md")).map((entry) => ({ slug: entry.path.replace(/\.md$/, "") }));
2968
4250
  }
@@ -2974,17 +4256,33 @@ var ContentManager = class {
2974
4256
  }
2975
4257
  async getContentIndex(preparation) {
2976
4258
  if (this.contentIndex) return this.contentIndex;
4259
+ if (!preparation && this.contentIndexPromise)
4260
+ return await this.contentIndexPromise;
2977
4261
  if (preparation?.previousState && !hasContentChanges(preparation.changeSet)) {
2978
4262
  this.contentIndex = new Map(
2979
4263
  Object.entries(preparation.previousState.contentIndex)
2980
4264
  );
2981
4265
  return this.contentIndex;
2982
4266
  }
2983
- this.contentIndex = await this.contentIndexBuilder.build(
2984
- await this.entryReader.getEntries(),
2985
- (entry) => this.entryReader.read(entry)
4267
+ if (preparation) {
4268
+ this.contentIndex = preparation.currentContentIndex;
4269
+ return this.contentIndex;
4270
+ }
4271
+ const promise = this.observability().tracer.span(
4272
+ "content.index",
4273
+ {},
4274
+ async () => await this.contentIndexBuilder.build(
4275
+ await this.entryReader.getEntries(),
4276
+ (entry) => this.entryReader.read(entry)
4277
+ )
2986
4278
  );
2987
- return this.contentIndex;
4279
+ if (!preparation) this.contentIndexPromise = promise;
4280
+ try {
4281
+ this.contentIndex = await promise;
4282
+ return this.contentIndex;
4283
+ } finally {
4284
+ if (!preparation) this.contentIndexPromise = null;
4285
+ }
2988
4286
  }
2989
4287
  async getProcessedContent(slug) {
2990
4288
  const cached = this.contentCache.get(slug);
@@ -3001,6 +4299,7 @@ var ContentManager = class {
3001
4299
  ]);
3002
4300
  await this.pluginRuntime.startBuild(contentIndex);
3003
4301
  await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4302
+ await this.ensureRoutableSlugs();
3004
4303
  this.pipeline ??= new Pipeline(
3005
4304
  contentIndex,
3006
4305
  await this.locationResolver.getPermalinks(),
@@ -3011,6 +4310,12 @@ var ContentManager = class {
3011
4310
  const content = await this.pipeline.execute(rawPost, {
3012
4311
  sourceSlug: slug
3013
4312
  });
4313
+ this.processedContentCount += 1;
4314
+ if (this.processedContentCount % 100 === 0) {
4315
+ this.observability().tracer.event("content.process.sample", {
4316
+ processed: this.processedContentCount
4317
+ });
4318
+ }
3014
4319
  await this.pluginRuntime.runPostHook(
3015
4320
  "onPostParsed",
3016
4321
  slug,
@@ -3031,9 +4336,24 @@ var ContentManager = class {
3031
4336
  );
3032
4337
  }
3033
4338
  async getManifest(options) {
4339
+ if (this.manifest) return this.manifest;
4340
+ if (this.manifestPromise) return await this.manifestPromise;
4341
+ this.manifestPromise = this.buildManifest(options);
4342
+ try {
4343
+ return await this.manifestPromise;
4344
+ } catch (error) {
4345
+ this.manifestPromise = null;
4346
+ throw error;
4347
+ }
4348
+ }
4349
+ async buildManifest(options) {
3034
4350
  if (this.manifest) return this.manifest;
3035
4351
  this.enableBuildTime();
3036
- const preparation = options ? await this.buildCoordinator.getPreparation(options.incremental) : void 0;
4352
+ const preparation = options ? await this.buildCoordinator.getPreparation(
4353
+ options.incremental,
4354
+ this.getPipelineFingerprint(),
4355
+ this.requiresFullContentRegeneration()
4356
+ ) : void 0;
3037
4357
  return await this.observability().tracer.span(
3038
4358
  "content.manifest",
3039
4359
  {},
@@ -3043,26 +4363,38 @@ var ContentManager = class {
3043
4363
  this.getContentIndex(preparation),
3044
4364
  this.getContentLocations()
3045
4365
  ]);
3046
- const entries = await Promise.all(
3047
- posts.map(async (post) => {
3048
- const [rawPost, processed] = await Promise.all([
3049
- this.getPost(post.slug),
3050
- this.getProcessedContent(post.slug)
3051
- ]);
3052
- const location = locations.get(post.slug);
3053
- if (!location) {
3054
- throw new Error(
3055
- `Content public location was not resolved: ${post.slug}`
4366
+ const entries = await this.observability().tracer.span(
4367
+ "content.manifest.entries",
4368
+ {},
4369
+ () => mapConcurrent3(
4370
+ posts,
4371
+ CONTENT_PROCESSING_CONCURRENCY,
4372
+ async (post) => {
4373
+ const reused = this.reuseManifestEntry(post.slug, preparation);
4374
+ if (reused) return reused;
4375
+ const [rawPost, processed] = await Promise.all([
4376
+ this.getPost(post.slug),
4377
+ this.getProcessedContent(post.slug)
4378
+ ]);
4379
+ const location = locations.get(post.slug);
4380
+ if (!location) {
4381
+ throw new Error(
4382
+ `Content public location was not resolved: ${post.slug}`
4383
+ );
4384
+ }
4385
+ return this.manifestBuilder.createEntry(
4386
+ post.slug,
4387
+ rawPost,
4388
+ processed,
4389
+ contentIndex,
4390
+ location,
4391
+ resolvePublishingState(processed.frontmatter, {
4392
+ strategy: this.publishStrategy(),
4393
+ buildTime: this.publishingBuildTime
4394
+ })
3056
4395
  );
3057
4396
  }
3058
- return this.manifestBuilder.createEntry(
3059
- post.slug,
3060
- rawPost,
3061
- processed,
3062
- contentIndex,
3063
- location
3064
- );
3065
- })
4397
+ )
3066
4398
  );
3067
4399
  await this.pluginRuntime.runGraphHook(entries, contentIndex);
3068
4400
  const manifest = await this.observability().tracer.span(
@@ -3070,12 +4402,17 @@ var ContentManager = class {
3070
4402
  {},
3071
4403
  () => this.manifestBuilder.build(entries, contentIndex)
3072
4404
  );
4405
+ const cacheManifestEntries = structuredClone(manifest.entries);
3073
4406
  this.locationResolver.populateRedirects(manifest, locations);
3074
- await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
3075
4407
  this.applyPublicView(manifest);
3076
4408
  manifest.pagePaths = [
3077
4409
  ...await this.pluginRuntime.getPagePaths(manifest, contentIndex)
3078
4410
  ];
4411
+ manifest.pageRoutes = [
4412
+ ...await this.pluginRuntime.getPageRoutes(manifest, contentIndex)
4413
+ ];
4414
+ await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
4415
+ this.synchronizeManifestHtml(manifest, cacheManifestEntries);
3079
4416
  manifest.assets = this.pluginRuntime.collectAssets();
3080
4417
  manifest.clientEntries = this.pluginRuntime.collectClientEntries();
3081
4418
  manifest.diagnostics = [
@@ -3084,8 +4421,39 @@ var ContentManager = class {
3084
4421
  ];
3085
4422
  await this.pluginRuntime.runBuildEnd(manifest, contentIndex);
3086
4423
  manifest.generatedOutputs = this.pluginRuntime.collectGeneratedOutputs();
4424
+ const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4425
+ manifest,
4426
+ contentIndex
4427
+ );
4428
+ this.outputChangeSet = preparation ? determineOutputChanges({
4429
+ manifest,
4430
+ previousState: preparation.previousState,
4431
+ changeSet: preparation.changeSet,
4432
+ affectedContent: preparation.affectedContent,
4433
+ pluginPageOutputs,
4434
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4435
+ }) : null;
4436
+ if (this.outputChangeSet) {
4437
+ this.observability().tracer.event("build.incremental.outputs", {
4438
+ candidateOutputCount: this.outputChangeSet.candidateOutputCount,
4439
+ affectedOutputCount: this.outputChangeSet.affectedOutputCount,
4440
+ removedOutputCount: this.outputChangeSet.removedOutputCount,
4441
+ unchangedOutputCount: this.outputChangeSet.unchangedOutputCount,
4442
+ fullRegenerationRequired: this.outputChangeSet.fullRegenerationRequired
4443
+ });
4444
+ }
3087
4445
  if (preparation)
3088
- await this.buildCoordinator.commit(preparation, manifest);
4446
+ await this.buildCoordinator.commit(
4447
+ preparation,
4448
+ manifest,
4449
+ this.getPipelineFingerprint(),
4450
+ this.outputChangeSet ? [
4451
+ ...this.outputChangeSet.affected,
4452
+ ...this.outputChangeSet.unchanged
4453
+ ] : [],
4454
+ cacheManifestEntries,
4455
+ this.trackedContentDependencies
4456
+ );
3089
4457
  this.manifest = manifest;
3090
4458
  return manifest;
3091
4459
  }
@@ -3094,6 +4462,34 @@ var ContentManager = class {
3094
4462
  async build(options = {}) {
3095
4463
  return await this.getManifest(options);
3096
4464
  }
4465
+ async getOutputChangeSet(options = {}) {
4466
+ await this.getManifest(options);
4467
+ if (!this.outputChangeSet) {
4468
+ const manifest = await this.getManifest();
4469
+ const contentIndex = await this.getContentIndex();
4470
+ const pluginPageOutputs = await this.pluginRuntime.getPageOutputs(
4471
+ manifest,
4472
+ contentIndex
4473
+ );
4474
+ return determineOutputChanges({
4475
+ manifest,
4476
+ previousState: void 0,
4477
+ changeSet: {
4478
+ added: manifest.entries.map((entry) => `${entry.slug}.md`),
4479
+ changed: [],
4480
+ removed: [],
4481
+ unchanged: []
4482
+ },
4483
+ affectedContent: {
4484
+ direct: new Set(manifest.entries.map((entry) => entry.slug)),
4485
+ dependent: /* @__PURE__ */ new Set()
4486
+ },
4487
+ pluginPageOutputs,
4488
+ contentOutputDependencies: this.pluginRuntime.getContentOutputDependencies()
4489
+ });
4490
+ }
4491
+ return this.outputChangeSet;
4492
+ }
3097
4493
  async getBacklinks(targetSlug) {
3098
4494
  const graph = await this.getContentGraph();
3099
4495
  return graph.incomingSlugs(targetSlug).map((slug) => ({
@@ -3148,19 +4544,106 @@ var ContentManager = class {
3148
4544
  return await this.locationResolver.getLocations();
3149
4545
  }
3150
4546
  applyPublicView(manifest) {
3151
- const strategy = this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4547
+ const strategy = this.publishStrategy();
4548
+ for (const entry of manifest.entries) {
4549
+ entry.publishing = resolvePublishingState(entry.frontmatter, {
4550
+ strategy,
4551
+ buildTime: this.publishingBuildTime
4552
+ });
4553
+ }
3152
4554
  manifest.publicEntries = manifest.entries.filter(
3153
- (entry) => isPublishable(strategy, entry.frontmatter)
4555
+ (entry) => entry.publishing.routable
4556
+ );
4557
+ manifest.discoverableEntries = manifest.entries.filter(
4558
+ (entry) => entry.publishing.discoverable
3154
4559
  );
3155
4560
  const publicSlugs = new Set(
3156
4561
  manifest.publicEntries.map((entry) => entry.slug)
3157
4562
  );
4563
+ const publicPermalinks = new Map(
4564
+ manifest.publicEntries.map((entry) => [entry.permalink, entry])
4565
+ );
4566
+ manifest.byRoutablePermalink = publicPermalinks;
3158
4567
  manifest.publicRedirects = new Map(
3159
4568
  [...manifest.redirects].filter(
3160
4569
  ([, redirect]) => publicSlugs.has(redirect.slug)
3161
4570
  )
3162
4571
  );
3163
4572
  }
4573
+ synchronizeManifestHtml(manifest, preHookEntries) {
4574
+ const preHookHtml = new Map(
4575
+ preHookEntries.map((entry) => [entry.slug, entry.html])
4576
+ );
4577
+ for (const entry of manifest.entries) {
4578
+ const changed = preHookHtml.get(entry.slug) !== entry.html;
4579
+ const cached = this.contentCache.get(entry.slug);
4580
+ if (cached) {
4581
+ if (changed) cached.html = entry.html;
4582
+ continue;
4583
+ }
4584
+ this.contentCache.set(entry.slug, {
4585
+ frontmatter: entry.frontmatter,
4586
+ html: entry.html
4587
+ });
4588
+ }
4589
+ }
4590
+ publishStrategy() {
4591
+ return this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
4592
+ }
4593
+ reuseManifestEntry(slug, preparation) {
4594
+ if (!preparation?.previousState?.manifestEntries) return null;
4595
+ if (preparation.previousState.pipelineFingerprint !== this.getPipelineFingerprint()) {
4596
+ return null;
4597
+ }
4598
+ if (preparation.affectedContent.direct.has(slug)) return null;
4599
+ if (preparation.affectedContent.dependent.has(slug)) return null;
4600
+ const entry = preparation.previousManifestEntriesBySlug.get(slug);
4601
+ if (!entry) return null;
4602
+ this.observability().tracer.event("content.reuse", { slug });
4603
+ return cloneManifestEntry(entry);
4604
+ }
4605
+ getPipelineFingerprint() {
4606
+ if (!this.pipelineOptions.config) return void 0;
4607
+ this.pipelineFingerprint ??= computePipelineFingerprint(
4608
+ this.pipelineOptions.config
4609
+ );
4610
+ return this.pipelineFingerprint;
4611
+ }
4612
+ requiresFullContentRegeneration() {
4613
+ const config = this.pipelineOptions.config;
4614
+ return config ? !isPersistentlyCacheable("", config).cacheable : false;
4615
+ }
4616
+ isRoutable(slug) {
4617
+ if (!this.routableSlugs) {
4618
+ throw new Error(`Publishing state was not resolved: ${slug}`);
4619
+ }
4620
+ return this.routableSlugs.has(slug);
4621
+ }
4622
+ async ensureRoutableSlugs() {
4623
+ if (this.routableSlugs) return this.routableSlugs;
4624
+ if (this.routableSlugsPromise) return await this.routableSlugsPromise;
4625
+ this.routableSlugsPromise = this.resolveRoutableSlugs();
4626
+ try {
4627
+ this.routableSlugs = await this.routableSlugsPromise;
4628
+ return this.routableSlugs;
4629
+ } finally {
4630
+ this.routableSlugsPromise = null;
4631
+ }
4632
+ }
4633
+ async resolveRoutableSlugs() {
4634
+ const strategy = this.publishStrategy();
4635
+ const posts = await this.getAllPosts();
4636
+ const routableSlugs = /* @__PURE__ */ new Set();
4637
+ await mapConcurrent3(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
4638
+ const markdown = await this.getPost(post.slug);
4639
+ const publishing = resolvePublishingState(readFrontmatter(markdown), {
4640
+ strategy,
4641
+ buildTime: this.publishingBuildTime
4642
+ });
4643
+ if (publishing.routable) routableSlugs.add(post.slug);
4644
+ });
4645
+ return routableSlugs;
4646
+ }
3164
4647
  enableBuildTime() {
3165
4648
  this.isBuildTime = true;
3166
4649
  this.pluginRuntime.enableBuildTime();
@@ -3169,6 +4652,44 @@ var ContentManager = class {
3169
4652
  return this.pipelineOptions.observability ?? noopObservability;
3170
4653
  }
3171
4654
  };
4655
+ function readFrontmatter(markdown) {
4656
+ const file = new VFile2({ value: markdown });
4657
+ matter3(file);
4658
+ return file.data.matter ?? {};
4659
+ }
4660
+ function cloneManifestEntry(entry) {
4661
+ return {
4662
+ ...entry,
4663
+ publicLocation: { ...entry.publicLocation },
4664
+ frontmatter: { ...entry.frontmatter },
4665
+ aliases: [...entry.aliases ?? []],
4666
+ tags: [...entry.tags],
4667
+ links: entry.links.map((link) => ({ ...link })),
4668
+ backlinks: [...entry.backlinks],
4669
+ assets: entry.assets.map((asset) => ({ ...asset })),
4670
+ ...entry.headTags ? { headTags: [...entry.headTags] } : {},
4671
+ ...entry.bodySlots ? { bodySlots: { ...entry.bodySlots } } : {}
4672
+ };
4673
+ }
4674
+ async function mapConcurrent3(values, concurrency, map) {
4675
+ const results = new Array(values.length);
4676
+ let nextIndex = 0;
4677
+ async function worker() {
4678
+ while (nextIndex < values.length) {
4679
+ const index = nextIndex;
4680
+ nextIndex += 1;
4681
+ const value = values[index];
4682
+ if (value !== void 0) results[index] = await map(value);
4683
+ }
4684
+ }
4685
+ await Promise.all(
4686
+ Array.from(
4687
+ { length: Math.min(concurrency, values.length) },
4688
+ () => worker()
4689
+ )
4690
+ );
4691
+ return results;
4692
+ }
3172
4693
 
3173
4694
  // src/content/content_source.ts
3174
4695
  async function readContentSourceEntry(source, logicalPath) {
@@ -3179,6 +4700,89 @@ async function getContentSourceEntry(source, logicalPath) {
3179
4700
  return (await source.scan()).find((candidate) => candidate.path === logicalPath) ?? null;
3180
4701
  }
3181
4702
 
4703
+ // src/content/folder_entry.ts
4704
+ function resolveFolderEntry(manifest, folder, options = {}) {
4705
+ const base = normalizeFolder2(folder);
4706
+ const scope = options.scope ?? "routable";
4707
+ const candidates = [];
4708
+ for (const [kind, slug] of folderEntrySlugs(base)) {
4709
+ if (slug === null) continue;
4710
+ const entry = manifest.bySlug.get(slug);
4711
+ if (!entry || !isInScope(entry, scope)) continue;
4712
+ candidates.push({ kind, entry });
4713
+ }
4714
+ if (candidates.length === 0) return { type: "none" };
4715
+ const candidate = candidates[0];
4716
+ if (candidates.length === 1 && candidate) {
4717
+ return { type: "resolved", kind: candidate.kind, entry: candidate.entry };
4718
+ }
4719
+ return { type: "ambiguous", candidates };
4720
+ }
4721
+ function resolveFolderLocation(manifest, folder) {
4722
+ const resolution = resolveFolderEntry(manifest, folder);
4723
+ const candidates = resolution.type === "ambiguous" ? resolution.candidates.filter((candidate) => candidate.kind !== "folder") : resolution.type === "resolved" && resolution.kind !== "folder" ? [{ kind: resolution.kind, entry: resolution.entry }] : [];
4724
+ if (candidates.length === 1) {
4725
+ const candidate = candidates[0];
4726
+ if (candidate) return { type: "content", entry: candidate.entry };
4727
+ }
4728
+ if (candidates.length > 1) return { type: "ambiguous", candidates };
4729
+ const location = manifest.folderLocations.get(normalizeFolder2(folder));
4730
+ return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4731
+ }
4732
+ function resolvePublicFolderLocation(manifest, pathname) {
4733
+ const normalizedPathname = normalizePathname(pathname);
4734
+ const entry = manifest.publicEntries.find(
4735
+ (candidate) => normalizePathname(candidate.publicLocation.permalink) === normalizedPathname
4736
+ );
4737
+ if (entry) return { type: "content", entry };
4738
+ const location = [...manifest.folderLocations.values()].find(
4739
+ (candidate) => normalizePathname(candidate.pathname) === normalizedPathname
4740
+ );
4741
+ return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4742
+ }
4743
+ function resolveGeneratedFolderLocation(manifest, folder) {
4744
+ const normalizedFolder = normalizeFolder2(folder);
4745
+ if (!normalizedFolder) return null;
4746
+ const candidates = /* @__PURE__ */ new Set();
4747
+ const prefix = `${normalizedFolder}/`;
4748
+ for (const entry of manifest.publicEntries) {
4749
+ if (!entry.slug.startsWith(prefix)) continue;
4750
+ const remainder = entry.slug.slice(prefix.length).split("/").filter(Boolean);
4751
+ const pathname = parentPath(
4752
+ entry.publicLocation.permalink,
4753
+ remainder.length
4754
+ );
4755
+ if (pathname) candidates.add(pathname);
4756
+ }
4757
+ return candidates.size === 1 ? [...candidates][0] ?? null : null;
4758
+ }
4759
+ function folderEntrySlugs(base) {
4760
+ const prefix = base === "" ? "" : `${base}/`;
4761
+ return [
4762
+ ["readme", `${prefix}README`],
4763
+ ["index", `${prefix}index`],
4764
+ ["folder", base === "" ? null : base]
4765
+ ];
4766
+ }
4767
+ function isInScope(entry, scope) {
4768
+ if (scope === "all") return true;
4769
+ if (scope === "discoverable") return entry.publishing.discoverable;
4770
+ return entry.publishing.routable;
4771
+ }
4772
+ function normalizeFolder2(folder) {
4773
+ return folder.split("/").filter(Boolean).join("/");
4774
+ }
4775
+ function normalizePathname(pathname) {
4776
+ const segments = pathname.split("/").filter(Boolean);
4777
+ return segments.length === 0 ? "/" : `/${segments.join("/")}`;
4778
+ }
4779
+ function parentPath(pathname, levels) {
4780
+ const segments = pathname.split("/").filter(Boolean);
4781
+ if (segments.length < levels) return null;
4782
+ const parent = segments.slice(0, -levels);
4783
+ return parent.length === 0 ? "/" : `/${parent.join("/")}/`;
4784
+ }
4785
+
3182
4786
  // src/content/graph_layout.ts
3183
4787
  function buildGraphEdges(nodes, visibleSlugs = new Set(nodes.map((node) => node.slug))) {
3184
4788
  const edges = /* @__PURE__ */ new Map();
@@ -3215,9 +4819,143 @@ function layoutRadialGraph(nodes, options) {
3215
4819
  });
3216
4820
  return layout;
3217
4821
  }
4822
+ function layoutForceGraph(nodes, options) {
4823
+ const safeOptions = resolveForceGraphLayoutOptions(options);
4824
+ const layoutNodes = [...nodes].sort((a, b) => a.slug.localeCompare(b.slug));
4825
+ const layout = createInitialLayout(layoutNodes, safeOptions);
4826
+ const visibleSlugs = new Set(layoutNodes.map((node) => node.slug));
4827
+ const edges = buildGraphEdges(layoutNodes, visibleSlugs).filter(
4828
+ (edge) => edge.source !== edge.target
4829
+ );
4830
+ const velocities = new Map(
4831
+ layoutNodes.map((node) => [node.slug, { x: 0, y: 0 }])
4832
+ );
4833
+ const centerX = safeOptions.width / 2;
4834
+ const centerY = safeOptions.height / 2;
4835
+ for (let iteration = 0; iteration < safeOptions.iterations; iteration++) {
4836
+ const alpha = 1 - iteration / safeOptions.iterations;
4837
+ applyRepulsion(
4838
+ layoutNodes,
4839
+ layout,
4840
+ velocities,
4841
+ safeOptions.repulsion * alpha
4842
+ );
4843
+ applyLinks(edges, layout, velocities, safeOptions.linkDistance, alpha);
4844
+ applyCentering(layoutNodes, layout, velocities, centerX, centerY, alpha);
4845
+ integrate(
4846
+ layoutNodes,
4847
+ layout,
4848
+ velocities,
4849
+ safeOptions,
4850
+ safeOptions.damping
4851
+ );
4852
+ }
4853
+ return layout;
4854
+ }
4855
+ function resolveForceGraphLayoutOptions(options) {
4856
+ const minNodeRadius = clampFinite(options.minNodeRadius ?? 8, 1, 64);
4857
+ const maxNodeRadius = Math.max(
4858
+ minNodeRadius,
4859
+ clampFinite(options.maxNodeRadius ?? 14, minNodeRadius, 96)
4860
+ );
4861
+ return {
4862
+ width: clampFinite(options.width, maxNodeRadius * 2, 2e4),
4863
+ height: clampFinite(options.height, maxNodeRadius * 2, 2e4),
4864
+ centerSlug: options.centerSlug,
4865
+ linkDistance: clampFinite(options.linkDistance ?? 84, 1, 2e3),
4866
+ repulsion: clampFinite(options.repulsion ?? 1800, 0, 1e5),
4867
+ iterations: Math.round(clampFinite(options.iterations ?? 160, 0, 1e3)),
4868
+ damping: clampFinite(options.damping ?? 0.82, 0, 1),
4869
+ minNodeRadius,
4870
+ maxNodeRadius
4871
+ };
4872
+ }
4873
+ function createInitialLayout(nodes, options) {
4874
+ return layoutRadialGraph(nodes, {
4875
+ width: options.width,
4876
+ height: options.height,
4877
+ centerSlug: options.centerSlug ?? nodes[0]?.slug ?? "",
4878
+ radiusRatio: 0.28,
4879
+ minNodeRadius: options.minNodeRadius,
4880
+ maxNodeRadius: options.maxNodeRadius
4881
+ });
4882
+ }
4883
+ function applyRepulsion(nodes, layout, velocities, strength) {
4884
+ for (let sourceIndex = 0; sourceIndex < nodes.length; sourceIndex++) {
4885
+ for (let targetIndex = sourceIndex + 1; targetIndex < nodes.length; targetIndex++) {
4886
+ const source = layout.get(nodes[sourceIndex]?.slug ?? "");
4887
+ const target = layout.get(nodes[targetIndex]?.slug ?? "");
4888
+ const sourceVelocity = velocities.get(nodes[sourceIndex]?.slug ?? "");
4889
+ const targetVelocity = velocities.get(nodes[targetIndex]?.slug ?? "");
4890
+ if (!source || !target || !sourceVelocity || !targetVelocity) continue;
4891
+ const dx = target.x - source.x || 0.01;
4892
+ const dy = target.y - source.y || 0.01;
4893
+ const distanceSquared = Math.max(dx * dx + dy * dy, 64);
4894
+ const force = strength / distanceSquared;
4895
+ const distance = Math.sqrt(distanceSquared);
4896
+ const fx = dx / distance * force;
4897
+ const fy = dy / distance * force;
4898
+ sourceVelocity.x -= fx;
4899
+ sourceVelocity.y -= fy;
4900
+ targetVelocity.x += fx;
4901
+ targetVelocity.y += fy;
4902
+ }
4903
+ }
4904
+ }
4905
+ function applyLinks(edges, layout, velocities, linkDistance, alpha) {
4906
+ for (const edge of edges) {
4907
+ const source = layout.get(edge.source);
4908
+ const target = layout.get(edge.target);
4909
+ const sourceVelocity = velocities.get(edge.source);
4910
+ const targetVelocity = velocities.get(edge.target);
4911
+ if (!source || !target || !sourceVelocity || !targetVelocity) continue;
4912
+ const dx = target.x - source.x || 0.01;
4913
+ const dy = target.y - source.y || 0.01;
4914
+ const distance = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
4915
+ const force = (distance - linkDistance) * 0.018 * alpha;
4916
+ const fx = dx / distance * force;
4917
+ const fy = dy / distance * force;
4918
+ sourceVelocity.x += fx;
4919
+ sourceVelocity.y += fy;
4920
+ targetVelocity.x -= fx;
4921
+ targetVelocity.y -= fy;
4922
+ }
4923
+ }
4924
+ function applyCentering(nodes, layout, velocities, centerX, centerY, alpha) {
4925
+ for (const node of nodes) {
4926
+ const point = layout.get(node.slug);
4927
+ const velocity = velocities.get(node.slug);
4928
+ if (!point || !velocity) continue;
4929
+ velocity.x += (centerX - point.x) * 4e-3 * alpha;
4930
+ velocity.y += (centerY - point.y) * 4e-3 * alpha;
4931
+ }
4932
+ }
4933
+ function integrate(nodes, layout, velocities, options, damping) {
4934
+ for (const node of nodes) {
4935
+ const point = layout.get(node.slug);
4936
+ const velocity = velocities.get(node.slug);
4937
+ if (!point || !velocity) continue;
4938
+ velocity.x *= damping;
4939
+ velocity.y *= damping;
4940
+ point.x = clamp(
4941
+ point.x + velocity.x,
4942
+ point.radius,
4943
+ options.width - point.radius
4944
+ );
4945
+ point.y = clamp(
4946
+ point.y + velocity.y,
4947
+ point.radius,
4948
+ options.height - point.radius
4949
+ );
4950
+ }
4951
+ }
3218
4952
  function clamp(value, min, max) {
3219
4953
  return Math.min(Math.max(value, min), max);
3220
4954
  }
4955
+ function clampFinite(value, min, max) {
4956
+ if (!Number.isFinite(value)) return min;
4957
+ return clamp(value, min, max);
4958
+ }
3221
4959
 
3222
4960
  // src/content/read_only_content_graph.ts
3223
4961
  async function readOnlyContentGraph(source, locations) {
@@ -3227,7 +4965,7 @@ async function readOnlyContentGraph(source, locations) {
3227
4965
  const markdownContents = await Promise.all(
3228
4966
  markdownEntries.map(async (entry) => ({
3229
4967
  entry,
3230
- markdown: await readText2(source, entry)
4968
+ markdown: await readText3(source, entry)
3231
4969
  }))
3232
4970
  );
3233
4971
  const graphEntries = markdownContents.map(
@@ -3246,7 +4984,15 @@ function createGraphEntry(entry, markdown, contentIndex, locations) {
3246
4984
  permalink: location.permalink,
3247
4985
  publicLocation: location,
3248
4986
  title: slug,
4987
+ aliases: [],
3249
4988
  frontmatter: {},
4989
+ publishing: resolvePublishingState(
4990
+ {},
4991
+ {
4992
+ strategy: "selective",
4993
+ buildTime: /* @__PURE__ */ new Date(0)
4994
+ }
4995
+ ),
3250
4996
  html: "",
3251
4997
  tags: [],
3252
4998
  links: extractContentLinks(markdown, contentIndex),
@@ -3254,60 +5000,11 @@ function createGraphEntry(entry, markdown, contentIndex, locations) {
3254
5000
  assets: []
3255
5001
  };
3256
5002
  }
3257
- async function readText2(source, entry) {
5003
+ async function readText3(source, entry) {
3258
5004
  const content = await source.read(entry);
3259
5005
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3260
5006
  }
3261
5007
 
3262
- // src/plugin/plugin_memo.ts
3263
- import { createHash as createHash3 } from "node:crypto";
3264
- function createPluginMemo(cache, options = {}) {
3265
- return {
3266
- async memo(key, dependencies, compute) {
3267
- const memoKey = `${key}:${hash2(stableStringify(dependencies))}`;
3268
- try {
3269
- const cached = await cache.get(memoKey);
3270
- if (cached !== void 0) return cached;
3271
- } catch (error) {
3272
- warn(
3273
- options.logger,
3274
- "Plugin cache read failed; recomputing.",
3275
- key,
3276
- error
3277
- );
3278
- }
3279
- const value = await compute();
3280
- try {
3281
- await cache.set(memoKey, value);
3282
- } catch (error) {
3283
- warn(
3284
- options.logger,
3285
- "Plugin cache write failed; result not cached.",
3286
- key,
3287
- error
3288
- );
3289
- }
3290
- return value;
3291
- }
3292
- };
3293
- }
3294
- function stableStringify(value) {
3295
- if (value === null || typeof value !== "object") return JSON.stringify(value);
3296
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
3297
- const keys = Object.keys(value).sort();
3298
- const body = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",");
3299
- return `{${body}}`;
3300
- }
3301
- function hash2(value) {
3302
- return createHash3("sha256").update(value).digest("hex");
3303
- }
3304
- function warn(logger, message, key, error) {
3305
- logger?.warn(message, {
3306
- memoKey: key,
3307
- error: error instanceof Error ? error.message : String(error)
3308
- });
3309
- }
3310
-
3311
5008
  // src/types/content_manifest.ts
3312
5009
  function appendContentBodySlot(entry, slot, html) {
3313
5010
  if (!html.trim()) return;
@@ -3320,12 +5017,12 @@ ${html}` : html
3320
5017
  }
3321
5018
 
3322
5019
  // src/types/plugin_endpoint.ts
3323
- function defineEndpoint(path4, handler, options = {}) {
5020
+ function defineEndpoint(path5, handler, options = {}) {
3324
5021
  if (options.cacheControl === void 0) {
3325
- return { path: path4, handler };
5022
+ return { path: path5, handler };
3326
5023
  }
3327
5024
  return {
3328
- path: path4,
5025
+ path: path5,
3329
5026
  handler: async (context) => {
3330
5027
  const response = await handler(context);
3331
5028
  const headers = { ...response.headers };
@@ -3367,6 +5064,7 @@ export {
3367
5064
  ContentManager,
3368
5065
  FileSystemContentSource,
3369
5066
  IMAGE_EXTENSIONS,
5067
+ INTERNAL_CONTENT_IGNORE_PATTERNS,
3370
5068
  NoopLogger,
3371
5069
  NoopTracer,
3372
5070
  Pipeline,
@@ -3376,6 +5074,7 @@ export {
3376
5074
  attachmentUrl,
3377
5075
  buildContentCollections,
3378
5076
  buildGraphEdges,
5077
+ buildOutputInventory,
3379
5078
  calculateReadingTime,
3380
5079
  collectConfigDeprecationDiagnostics,
3381
5080
  createClientEntry,
@@ -3388,6 +5087,7 @@ export {
3388
5087
  defineEndpoint,
3389
5088
  definePlugin,
3390
5089
  defineTheme,
5090
+ determineOutputChanges,
3391
5091
  escapeHtml,
3392
5092
  escapeHtmlAttribute,
3393
5093
  escapeScriptJson,
@@ -3398,14 +5098,18 @@ export {
3398
5098
  getExtension,
3399
5099
  getResolvedPluginMetadata,
3400
5100
  groupContentEntries,
5101
+ htmlOutputPath,
3401
5102
  isAttachmentPath,
3402
5103
  isExcluded,
5104
+ isIgnoredContentPath,
3403
5105
  isImagePath,
3404
5106
  isMarkdownPath,
3405
5107
  isPublishable,
3406
5108
  isPublished,
5109
+ layoutForceGraph,
3407
5110
  layoutRadialGraph,
3408
5111
  loadContentBuildState,
5112
+ normalizeContentAssetOutputPath,
3409
5113
  normalizeContentPath,
3410
5114
  normalizeGeneratedOutputPath,
3411
5115
  normalizeTag,
@@ -3420,7 +5124,13 @@ export {
3420
5124
  resolveContentQueryPagination,
3421
5125
  resolveContentStableId,
3422
5126
  resolveDefaultContentLocation,
5127
+ resolveFolderEntry,
5128
+ resolveFolderLocation,
5129
+ resolveGeneratedFolderLocation,
3423
5130
  resolvePlugins,
5131
+ resolvePublicFolderLocation,
5132
+ resolvePublishingBuildTime,
5133
+ resolvePublishingState,
3424
5134
  serializePublicClientConfig,
3425
5135
  stableStringify,
3426
5136
  stripHtml,