@riebeckite/core 0.0.14 → 0.0.15

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