@silicajs/core 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  slugifySegment,
33
33
  stripNumericPrefix,
34
34
  tagToHref
35
- } from "./chunk-FOKUARB2.js";
35
+ } from "./chunk-2TXWBKM4.js";
36
36
 
37
37
  // src/config.ts
38
38
  import path from "path";
@@ -106,9 +106,30 @@ function resolveConfig(config = {}, projectRoot = process.cwd()) {
106
106
  filters: {
107
107
  removeDrafts: config.filters?.removeDrafts ?? true,
108
108
  explicitPublish: config.filters?.explicitPublish ?? false
109
+ },
110
+ render: {
111
+ prerender: resolvePrerenderConfig(config.render?.prerender),
112
+ cache: {
113
+ storage: config.render?.cache?.storage ?? "filesystem",
114
+ directory: config.render?.cache?.directory
115
+ }
109
116
  }
110
117
  };
111
118
  }
119
+ function resolvePrerenderConfig(prerender) {
120
+ if (!prerender || prerender === "all") return { strategy: "all" };
121
+ if (prerender === "none") return { strategy: "none" };
122
+ if ("strategy" in prerender && prerender.strategy === "all") {
123
+ return { ...prerender, strategy: "all" };
124
+ }
125
+ if ("strategy" in prerender && prerender.strategy === "none") {
126
+ return { ...prerender, strategy: "none" };
127
+ }
128
+ if ("strategy" in prerender && prerender.strategy === "custom") {
129
+ return { ...prerender, strategy: "custom" };
130
+ }
131
+ return { ...prerender, strategy: "depth" };
132
+ }
112
133
 
113
134
  // src/files.ts
114
135
  import path2 from "path";
@@ -173,18 +194,277 @@ function isMarkdownFile(filePath) {
173
194
  import crypto from "crypto";
174
195
  import { execFile } from "child_process";
175
196
  import os from "os";
176
- import path3 from "path";
197
+ import path4 from "path";
177
198
  import { promisify } from "util";
178
199
  import { Worker } from "worker_threads";
200
+ import fs3 from "fs-extra";
201
+
202
+ // src/vault-db.ts
203
+ import path3 from "path";
204
+ import Database from "better-sqlite3";
179
205
  import fs2 from "fs-extra";
180
206
  import {
181
- buildSearchDatabase,
182
- SEARCH_DATABASE_FILENAME
207
+ buildSearchTables,
208
+ makeExcerpt
183
209
  } from "@silicajs/search";
210
+ var VAULT_DATABASE_FILENAME = "vault.db";
211
+ var VAULT_DATABASE_VERSION = "1";
212
+ async function writeVaultDatabase(projectRoot, input) {
213
+ const silicaRoot = path3.join(projectRoot, ".silica");
214
+ await fs2.ensureDir(silicaRoot);
215
+ const databasePath = path3.join(silicaRoot, VAULT_DATABASE_FILENAME);
216
+ const temporaryPath = path3.join(silicaRoot, `${VAULT_DATABASE_FILENAME}.tmp`);
217
+ await removeDatabaseFiles(temporaryPath);
218
+ const db = new Database(temporaryPath);
219
+ try {
220
+ db.pragma("journal_mode = DELETE");
221
+ db.pragma("foreign_keys = ON");
222
+ db.pragma("synchronous = OFF");
223
+ createVaultDatabaseSchema(db);
224
+ populateVaultDatabase(db, input);
225
+ db.exec("VACUUM");
226
+ } finally {
227
+ db.close();
228
+ }
229
+ await removeDatabaseFiles(databasePath);
230
+ await fs2.rename(temporaryPath, databasePath);
231
+ await removeDatabaseSidecars(temporaryPath);
232
+ return databasePath;
233
+ }
234
+ function createVaultDatabaseSchema(db) {
235
+ db.exec(`
236
+ CREATE TABLE vault_metadata (
237
+ key TEXT PRIMARY KEY,
238
+ value TEXT NOT NULL
239
+ );
240
+
241
+ CREATE TABLE notes (
242
+ slug TEXT PRIMARY KEY,
243
+ file TEXT NOT NULL,
244
+ relative_file TEXT NOT NULL,
245
+ title TEXT NOT NULL,
246
+ menu_label TEXT NOT NULL,
247
+ description TEXT,
248
+ generated_description TEXT,
249
+ frontmatter_json TEXT NOT NULL,
250
+ tags_json TEXT NOT NULL,
251
+ search_excerpt TEXT NOT NULL DEFAULT '',
252
+ created TEXT,
253
+ modified TEXT,
254
+ sort_key TEXT,
255
+ listed INTEGER NOT NULL,
256
+ content_hash TEXT NOT NULL,
257
+ render_hash TEXT NOT NULL,
258
+ prerender INTEGER NOT NULL
259
+ );
260
+
261
+ CREATE TABLE note_tags (
262
+ slug TEXT NOT NULL,
263
+ tag TEXT NOT NULL,
264
+ PRIMARY KEY (slug, tag),
265
+ FOREIGN KEY (slug) REFERENCES notes(slug) ON DELETE CASCADE
266
+ );
267
+
268
+ CREATE TABLE links (
269
+ source_slug TEXT NOT NULL,
270
+ target_slug TEXT NOT NULL,
271
+ kind TEXT NOT NULL CHECK (kind IN ('link', 'embed')),
272
+ PRIMARY KEY (source_slug, target_slug, kind)
273
+ );
274
+
275
+ CREATE TABLE broken_links (
276
+ source_slug TEXT NOT NULL,
277
+ target TEXT NOT NULL
278
+ );
279
+
280
+ CREATE TABLE slug_aliases (
281
+ strategy_key TEXT NOT NULL,
282
+ alias TEXT NOT NULL,
283
+ slug TEXT NOT NULL,
284
+ sort_key TEXT,
285
+ PRIMARY KEY (strategy_key, alias, slug)
286
+ );
287
+
288
+ CREATE INDEX notes_prerender_idx ON notes(prerender, slug);
289
+ CREATE INDEX notes_listed_sort_idx ON notes(listed, sort_key, slug);
290
+ CREATE INDEX links_target_idx ON links(target_slug, kind, source_slug);
291
+ CREATE INDEX links_source_idx ON links(source_slug, kind, target_slug);
292
+ CREATE INDEX note_tags_tag_idx ON note_tags(tag, slug);
293
+ CREATE INDEX slug_aliases_lookup_idx
294
+ ON slug_aliases(strategy_key, alias, sort_key, slug);
295
+ `);
296
+ }
297
+ function populateVaultDatabase(db, input) {
298
+ const prerenderSlugs = new Set(input.prerender.slugs);
299
+ const searchBySlug = new Map(
300
+ input.searchRecords.map((record) => [record.slug, record])
301
+ );
302
+ const insertMetadata = db.prepare(`
303
+ INSERT INTO vault_metadata (key, value) VALUES (?, ?)
304
+ `);
305
+ const insertNote = db.prepare(`
306
+ INSERT INTO notes (
307
+ slug,
308
+ file,
309
+ relative_file,
310
+ title,
311
+ menu_label,
312
+ description,
313
+ generated_description,
314
+ frontmatter_json,
315
+ tags_json,
316
+ search_excerpt,
317
+ created,
318
+ modified,
319
+ sort_key,
320
+ listed,
321
+ content_hash,
322
+ render_hash,
323
+ prerender
324
+ )
325
+ VALUES (
326
+ @slug,
327
+ @file,
328
+ @relativeFile,
329
+ @title,
330
+ @menuLabel,
331
+ @description,
332
+ @generatedDescription,
333
+ @frontmatterJson,
334
+ @tagsJson,
335
+ @searchExcerpt,
336
+ @created,
337
+ @modified,
338
+ @sortKey,
339
+ @listed,
340
+ @contentHash,
341
+ @renderHash,
342
+ @prerender
343
+ )
344
+ `);
345
+ const insertTag = db.prepare(`
346
+ INSERT OR IGNORE INTO note_tags (slug, tag) VALUES (?, ?)
347
+ `);
348
+ const insertLink = db.prepare(`
349
+ INSERT OR IGNORE INTO links (source_slug, target_slug, kind)
350
+ VALUES (?, ?, ?)
351
+ `);
352
+ const insertBrokenLink = db.prepare(`
353
+ INSERT INTO broken_links (source_slug, target) VALUES (?, ?)
354
+ `);
355
+ const insertAlias = db.prepare(`
356
+ INSERT OR IGNORE INTO slug_aliases (strategy_key, alias, slug, sort_key)
357
+ VALUES (?, ?, ?, ?)
358
+ `);
359
+ const insertAll = db.transaction(() => {
360
+ insertMetadata.run("version", VAULT_DATABASE_VERSION);
361
+ insertMetadata.run("generatedAt", input.manifest.generatedAt);
362
+ insertMetadata.run("contentDir", input.manifest.contentDir);
363
+ insertMetadata.run("configJson", JSON.stringify(input.config));
364
+ insertMetadata.run(
365
+ "renderEnvironmentHash",
366
+ input.cacheState.renderEnvironmentHash
367
+ );
368
+ insertMetadata.run("configHash", input.cacheState.configHash);
369
+ insertMetadata.run("navigationHash", input.cacheState.navigationHash);
370
+ insertMetadata.run("tagIndexHash", input.cacheState.tagIndexHash);
371
+ insertMetadata.run("rendererVersion", input.cacheState.rendererVersion);
372
+ insertMetadata.run("cacheStateJson", JSON.stringify(input.cacheState));
373
+ for (const entry of input.manifest.entries) {
374
+ const searchRecord = searchBySlug.get(entry.slug);
375
+ insertNote.run({
376
+ slug: entry.slug,
377
+ file: entry.file,
378
+ relativeFile: entry.relativeFile,
379
+ title: entry.title,
380
+ menuLabel: entry.menuLabel,
381
+ description: entry.description,
382
+ generatedDescription: entry.generatedDescription,
383
+ frontmatterJson: JSON.stringify(entry.frontmatter),
384
+ tagsJson: JSON.stringify(entry.tags),
385
+ searchExcerpt: searchRecord ? makeExcerpt(
386
+ searchRecord.content,
387
+ searchRecord.description ?? searchRecord.title
388
+ ) : "",
389
+ created: entry.created,
390
+ modified: entry.modified,
391
+ sortKey: entry.sortKey,
392
+ listed: isListedEntry(entry) ? 1 : 0,
393
+ contentHash: entry.contentHash,
394
+ renderHash: input.renderHashes[entry.slug] ?? "missing",
395
+ prerender: prerenderSlugs.has(entry.slug) ? 1 : 0
396
+ });
397
+ for (const tag of entry.tags) {
398
+ for (const hierarchyTag of tagHierarchy(tag)) {
399
+ insertTag.run(entry.slug, hierarchyTag);
400
+ }
401
+ }
402
+ }
403
+ for (const [source, targets] of Object.entries(input.graph.links)) {
404
+ for (const target of targets) {
405
+ insertLink.run(source, target, "link");
406
+ }
407
+ }
408
+ for (const entry of input.manifest.entries) {
409
+ for (const target of entry.embeds) {
410
+ insertLink.run(entry.slug, target, "embed");
411
+ }
412
+ }
413
+ for (const brokenLink of input.graph.brokenLinks) {
414
+ insertBrokenLink.run(brokenLink.source, brokenLink.target);
415
+ }
416
+ for (const entry of input.manifest.entries) {
417
+ for (const [strategy, alias] of makeSlugAliases(entry.slug)) {
418
+ insertAlias.run(
419
+ strategy,
420
+ alias,
421
+ entry.slug,
422
+ entry.sortKey ?? entry.slug
423
+ );
424
+ }
425
+ }
426
+ });
427
+ insertAll();
428
+ buildSearchTables(db, input.searchRecords);
429
+ }
430
+ function isListedEntry(entry) {
431
+ return entry.frontmatter.listed !== false;
432
+ }
433
+ function tagHierarchy(tag) {
434
+ const normalized = tag.trim().replace(/^#/, "").toLowerCase();
435
+ const segments = normalized.split("/").filter(Boolean);
436
+ return segments.map((_, index) => segments.slice(0, index + 1).join("/"));
437
+ }
438
+ function makeSlugAliases(slug) {
439
+ const aliases = /* @__PURE__ */ new Map();
440
+ const simplified = slug === "index" ? "" : slug.replace(/\/index$/, "");
441
+ const basename = simplified.split("/").at(-1) ?? "";
442
+ aliases.set(`absolute:${slug}`, slug);
443
+ if (simplified) aliases.set(`absolute:${simplified}`, simplified);
444
+ if (basename) aliases.set(`shortest:${basename}`, basename);
445
+ return [...aliases.entries()].map(([key, alias]) => [
446
+ key.split(":")[0] ?? "shortest",
447
+ alias
448
+ ]);
449
+ }
450
+ async function removeDatabaseFiles(databasePath) {
451
+ await fs2.remove(databasePath);
452
+ await removeDatabaseSidecars(databasePath);
453
+ }
454
+ async function removeDatabaseSidecars(databasePath) {
455
+ await Promise.all([
456
+ fs2.remove(`${databasePath}-wal`),
457
+ fs2.remove(`${databasePath}-shm`),
458
+ fs2.remove(`${databasePath}-journal`)
459
+ ]);
460
+ }
461
+
462
+ // src/precompute.ts
184
463
  var execFileAsync = promisify(execFile);
185
464
  var MIN_PARALLEL_ANALYSIS_FILES = 64;
186
465
  var ANALYSIS_BATCH_SIZE = 16;
187
466
  var MAX_ANALYSIS_WORKERS = 12;
467
+ var RENDER_CACHE_SCHEMA_VERSION = "silica-render-v1";
188
468
  async function precompute(options = {}) {
189
469
  const projectRoot = options.projectRoot ?? process.cwd();
190
470
  const config = options.config ?? await loadConfig(projectRoot);
@@ -199,16 +479,16 @@ async function precompute(options = {}) {
199
479
  const graphLinks = {};
200
480
  const brokenLinks = [];
201
481
  const searchRecords = [];
202
- const runtimeContentRoot = path3.join(projectRoot, ".silica/content");
482
+ const runtimeContentRoot = path4.join(projectRoot, ".silica/content");
203
483
  const relativeGitPaths = markdownFiles.map(
204
- (file) => normalizeGitPath(path3.join(config.contentDir, file.relativePath))
484
+ (file) => normalizeGitPath(path4.join(config.contentDir, file.relativePath))
205
485
  );
206
486
  const gitDatesByPath = await getGitDatesForFiles(
207
487
  projectRoot,
208
488
  relativeGitPaths
209
489
  );
210
- await fs2.ensureDir(path3.join(projectRoot, ".silica"));
211
- await fs2.ensureDir(path3.join(projectRoot, ".silica/next/public/silica"));
490
+ await fs3.ensureDir(path4.join(projectRoot, ".silica"));
491
+ await fs3.ensureDir(path4.join(projectRoot, ".silica/next/public/silica"));
212
492
  await writeRuntimeMarkdown(runtimeContentRoot, markdownFiles);
213
493
  const analyses = await analyzeMarkdownFiles(markdownFiles, config, allSlugs, {
214
494
  concurrency: options.analysisConcurrency,
@@ -216,7 +496,7 @@ async function precompute(options = {}) {
216
496
  });
217
497
  for (const [index, file] of markdownFiles.entries()) {
218
498
  const gitDates = gitDatesByPath.get(
219
- normalizeGitPath(path3.join(config.contentDir, file.relativePath))
499
+ normalizeGitPath(path4.join(config.contentDir, file.relativePath))
220
500
  ) ?? {};
221
501
  const analysis = analyses[index];
222
502
  const title = analysis.title ?? titleFromFilePath(file.relativePath, config.ordering);
@@ -229,7 +509,7 @@ async function precompute(options = {}) {
229
509
  description: analysis.description,
230
510
  generatedDescription: analysis.generatedDescription,
231
511
  tags: analysis.tags,
232
- file: normalizeGitPath(path3.join(".silica/content", file.relativePath)),
512
+ file: normalizeGitPath(path4.join(".silica/content", file.relativePath)),
233
513
  relativeFile: file.relativePath,
234
514
  sortKey,
235
515
  created: stringifyDate(
@@ -238,12 +518,14 @@ async function precompute(options = {}) {
238
518
  modified: stringifyDate(
239
519
  getDate(file.frontmatter.modified) ?? gitDates.modified ?? file.stats.mtime
240
520
  ),
241
- frontmatter: file.frontmatter
521
+ frontmatter: file.frontmatter,
522
+ contentHash: hashString(file.raw),
523
+ embeds: analysis.embeds
242
524
  };
243
525
  entries.push(entry);
244
526
  graphLinks[file.slug] = analysis.links;
245
527
  brokenLinks.push(...analysis.brokenLinks);
246
- if (isListedEntry(entry)) {
528
+ if (isListedEntry2(entry)) {
247
529
  searchRecords.push({
248
530
  id: file.slug,
249
531
  slug: file.slug,
@@ -257,27 +539,18 @@ async function precompute(options = {}) {
257
539
  await copyAssets(projectRoot, config, scan.assets);
258
540
  const manifest = makeManifest(config, entries);
259
541
  const graph = makeGraph(graphLinks, brokenLinks);
260
- const buildId = crypto.randomUUID();
261
- await buildSearchDatabase(
262
- searchRecords,
263
- path3.join(projectRoot, ".silica", SEARCH_DATABASE_FILENAME)
264
- );
265
- await fs2.remove(path3.join(projectRoot, ".silica/search-index.json"));
266
- await writeJson(
267
- path3.join(projectRoot, ".silica/manifest.json"),
268
- serializeManifest(manifest)
269
- );
270
- await writeJson(
271
- path3.join(projectRoot, ".silica/navigation.json"),
272
- makeNavigation(manifest)
273
- );
274
- await writeJson(path3.join(projectRoot, ".silica/graph.json"), graph);
275
- await writeJson(path3.join(projectRoot, ".silica/config.json"), config);
276
- await fs2.writeFile(
277
- path3.join(projectRoot, ".silica/build-id.txt"),
278
- `${buildId}
279
- `
280
- );
542
+ const renderHashes = makeRenderHashes(manifest, graph);
543
+ const cacheState = await makeRenderCacheState(projectRoot, config, manifest);
544
+ const prerender = makePrerenderManifest(manifest, graph, config);
545
+ await writeVaultDatabase(projectRoot, {
546
+ config,
547
+ manifest,
548
+ graph,
549
+ renderHashes,
550
+ cacheState,
551
+ prerender,
552
+ searchRecords
553
+ });
281
554
  await writeSitemapAndRobots(projectRoot, config, manifest);
282
555
  if (config.wikilinks.strict && brokenLinks.length > 0) {
283
556
  const message = brokenLinks.map((link) => `${link.source} -> ${link.target}`).join("\n");
@@ -288,7 +561,8 @@ ${message}`);
288
561
  manifest,
289
562
  graph,
290
563
  searchRecords,
291
- buildId,
564
+ prerender,
565
+ cacheState,
292
566
  brokenLinks
293
567
  };
294
568
  }
@@ -411,31 +685,180 @@ function getRequestedAnalysisConcurrency(requestedConcurrency, available) {
411
685
  return Math.max(1, Math.floor(requestedConcurrency));
412
686
  }
413
687
  async function writeRuntimeMarkdown(runtimeContentRoot, files) {
414
- await fs2.emptyDir(runtimeContentRoot);
688
+ await fs3.emptyDir(runtimeContentRoot);
415
689
  for (const file of files) {
416
- const destination = path3.join(runtimeContentRoot, file.relativePath);
417
- await fs2.ensureDir(path3.dirname(destination));
418
- await fs2.writeFile(destination, file.raw);
690
+ const destination = path4.join(runtimeContentRoot, file.relativePath);
691
+ await fs3.ensureDir(path4.dirname(destination));
692
+ await fs3.writeFile(destination, file.raw);
419
693
  }
420
694
  }
421
- function serializeManifest(manifest) {
695
+ async function makeRenderCacheState(projectRoot, config, manifest) {
696
+ const themeHash = await getThemeHash(projectRoot, config.theme);
697
+ const configHash = hashStable({
698
+ title: config.title,
699
+ description: config.description,
700
+ logo: config.logo,
701
+ baseUrl: config.baseUrl,
702
+ contentDir: config.contentDir,
703
+ theme: config.theme,
704
+ auth: config.auth,
705
+ wikilinks: config.wikilinks,
706
+ tags: config.tags,
707
+ ordering: config.ordering,
708
+ filters: config.filters
709
+ });
710
+ const navigationHash = hashStable({
711
+ entries: manifest.entries.filter(isListedEntry2).map((entry) => ({
712
+ slug: entry.slug,
713
+ menuLabel: entry.menuLabel,
714
+ sortKey: entry.sortKey
715
+ }))
716
+ });
717
+ const tagIndexHash = hashStable({
718
+ entries: manifest.entries.filter(isListedEntry2).map((entry) => ({
719
+ slug: entry.slug,
720
+ title: entry.title,
721
+ description: entry.description,
722
+ tags: entry.tags
723
+ }))
724
+ });
422
725
  return {
423
- version: manifest.version,
424
- generatedAt: manifest.generatedAt,
425
- contentDir: manifest.contentDir,
426
- entries: manifest.entries
726
+ version: 1,
727
+ renderEnvironmentHash: hashStable({
728
+ version: RENDER_CACHE_SCHEMA_VERSION,
729
+ configHash,
730
+ themeHash
731
+ }),
732
+ configHash,
733
+ navigationHash,
734
+ tagIndexHash,
735
+ themeHash,
736
+ rendererVersion: RENDER_CACHE_SCHEMA_VERSION,
737
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
738
+ };
739
+ }
740
+ function makeRenderHashes(manifest, graph) {
741
+ const memo = /* @__PURE__ */ new Map();
742
+ const renderHashForSlug = (slug, seen = /* @__PURE__ */ new Set()) => {
743
+ const cached = memo.get(slug);
744
+ if (cached) return cached;
745
+ const entry = manifest.bySlug[slug];
746
+ if (!entry) return hashStable({ missing: slug });
747
+ if (seen.has(slug)) {
748
+ return hashStable({ slug, contentHash: entry.contentHash, cycle: true });
749
+ }
750
+ const nextSeen = new Set(seen).add(slug);
751
+ const embedded = entry.embeds.map((target) => ({
752
+ slug: target,
753
+ renderHash: renderHashForSlug(target, nextSeen)
754
+ }));
755
+ const backlinks = (graph.backlinks[slug] ?? []).map((source) => ({
756
+ slug: source,
757
+ title: manifest.bySlug[source]?.title ?? source
758
+ }));
759
+ const renderHash = hashStable({
760
+ entry: {
761
+ slug: entry.slug,
762
+ title: entry.title,
763
+ menuLabel: entry.menuLabel,
764
+ description: entry.description,
765
+ generatedDescription: entry.generatedDescription,
766
+ tags: entry.tags,
767
+ relativeFile: entry.relativeFile,
768
+ sortKey: entry.sortKey,
769
+ created: entry.created,
770
+ modified: entry.modified,
771
+ frontmatter: entry.frontmatter,
772
+ contentHash: entry.contentHash
773
+ },
774
+ embedded,
775
+ backlinks
776
+ });
777
+ memo.set(slug, renderHash);
778
+ return renderHash;
427
779
  };
780
+ for (const slug of manifest.allSlugs) {
781
+ renderHashForSlug(slug);
782
+ }
783
+ return Object.fromEntries(memo);
428
784
  }
429
- function makeNavigation(manifest) {
785
+ function makePrerenderManifest(manifest, graph, config) {
430
786
  return {
431
787
  version: 1,
432
- entries: manifest.entries.filter(isListedEntry).map((entry) => ({
433
- slug: entry.slug,
434
- title: entry.menuLabel,
435
- sortKey: entry.sortKey
436
- }))
788
+ slugs: selectPrerenderSlugs(manifest, graph, config)
437
789
  };
438
790
  }
791
+ function selectPrerenderSlugs(manifest, graph, config) {
792
+ const prerender = config.render.prerender;
793
+ const entries = manifest.entries;
794
+ const scoreBySlug = /* @__PURE__ */ new Map();
795
+ let candidates = [];
796
+ if (prerender.strategy === "all") {
797
+ candidates = entries;
798
+ } else if (prerender.strategy === "depth") {
799
+ candidates = entries.filter(
800
+ (entry) => entry.slug === "index" || getSlugDepth(entry.slug) <= prerender.depth
801
+ );
802
+ } else if (prerender.strategy === "custom") {
803
+ const context = { manifest, graph };
804
+ candidates = entries.filter((entry) => {
805
+ const selected2 = prerender.select?.(entry, context);
806
+ if (typeof selected2 === "number" && Number.isFinite(selected2)) {
807
+ scoreBySlug.set(entry.slug, selected2);
808
+ return true;
809
+ }
810
+ return selected2 === true;
811
+ });
812
+ }
813
+ const selected = new Set(
814
+ [...candidates].sort((left, right) => {
815
+ const scoreDelta = (scoreBySlug.get(right.slug) ?? 0) - (scoreBySlug.get(left.slug) ?? 0);
816
+ return scoreDelta || compareManifestEntries(left, right);
817
+ }).slice(0, prerender.limit ?? candidates.length).map((entry) => entry.slug)
818
+ );
819
+ for (const slug of prerender.include ?? []) {
820
+ if (manifest.bySlug[slug]) selected.add(slug);
821
+ }
822
+ for (const slug of prerender.exclude ?? []) {
823
+ selected.delete(slug);
824
+ }
825
+ return manifest.entries.map((entry) => entry.slug).filter((slug) => selected.has(slug));
826
+ }
827
+ function getSlugDepth(slug) {
828
+ const segments = slug.split("/").filter(Boolean);
829
+ return Math.max(0, segments.length - 1);
830
+ }
831
+ async function getThemeHash(projectRoot, theme) {
832
+ const themeName = getThemeName(theme);
833
+ if (!themeName?.startsWith(".")) return void 0;
834
+ const themeRoot = path4.resolve(projectRoot, themeName);
835
+ if (!await fs3.pathExists(themeRoot)) return void 0;
836
+ const files = await readThemeFiles(themeRoot);
837
+ return hashStable(files);
838
+ }
839
+ function getThemeName(theme) {
840
+ if (typeof theme === "string") return theme;
841
+ if (typeof theme === "object" && theme !== null) return theme.name;
842
+ return void 0;
843
+ }
844
+ async function readThemeFiles(root, current = root) {
845
+ const entries = await fs3.readdir(current, { withFileTypes: true });
846
+ const results = [];
847
+ for (const entry of entries.sort(
848
+ (left, right) => left.name.localeCompare(right.name)
849
+ )) {
850
+ const absolutePath = path4.join(current, entry.name);
851
+ if (entry.isDirectory()) {
852
+ results.push(...await readThemeFiles(root, absolutePath));
853
+ } else if (entry.isFile()) {
854
+ results.push({
855
+ path: normalizeGitPath(path4.relative(root, absolutePath)),
856
+ content: await fs3.readFile(absolutePath, "utf8")
857
+ });
858
+ }
859
+ }
860
+ return results;
861
+ }
439
862
  async function getGitDates(projectRoot, relativePath) {
440
863
  return (await getGitDatesForFiles(projectRoot, [normalizeGitPath(relativePath)])).get(normalizeGitPath(relativePath)) ?? {};
441
864
  }
@@ -476,6 +899,26 @@ async function getGitDatesForFiles(projectRoot, relativePaths) {
476
899
  function normalizeGitPath(relativePath) {
477
900
  return relativePath.replace(/\\/g, "/");
478
901
  }
902
+ function hashString(value) {
903
+ return crypto.createHash("sha256").update(value).digest("hex");
904
+ }
905
+ function hashStable(value) {
906
+ return hashString(stableStringify(value));
907
+ }
908
+ function stableStringify(value) {
909
+ if (value === void 0) return "undefined";
910
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
911
+ if (Array.isArray(value)) {
912
+ return `[${value.map(stableStringify).join(",")}]`;
913
+ }
914
+ if (value && typeof value === "object") {
915
+ const entries = Object.entries(value).filter(([, entryValue]) => typeof entryValue !== "function").sort(([left], [right]) => left.localeCompare(right));
916
+ return `{${entries.map(
917
+ ([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`
918
+ ).join(",")}}`;
919
+ }
920
+ return JSON.stringify(value);
921
+ }
479
922
  function filterPublished(files, config) {
480
923
  return files.filter((file) => {
481
924
  if (config.filters.removeDrafts && file.frontmatter.draft === true)
@@ -527,33 +970,33 @@ function makeGraph(links, brokenLinks) {
527
970
  };
528
971
  }
529
972
  async function copyAssets(projectRoot, config, assets) {
530
- const destinationRoot = path3.join(projectRoot, ".silica/next/public/silica");
531
- await fs2.emptyDir(destinationRoot);
973
+ const destinationRoot = path4.join(projectRoot, ".silica/next/public/silica");
974
+ await fs3.emptyDir(destinationRoot);
532
975
  for (const asset of assets) {
533
- await fs2.ensureDir(
534
- path3.dirname(path3.join(destinationRoot, asset.relativePath))
976
+ await fs3.ensureDir(
977
+ path4.dirname(path4.join(destinationRoot, asset.relativePath))
535
978
  );
536
- await fs2.copyFile(
979
+ await fs3.copyFile(
537
980
  asset.absolutePath,
538
- path3.join(destinationRoot, asset.relativePath)
981
+ path4.join(destinationRoot, asset.relativePath)
539
982
  );
540
983
  }
541
- await fs2.ensureDir(path3.join(projectRoot, ".silica/next/public"));
542
- await fs2.writeFile(path3.join(destinationRoot, ".gitkeep"), "");
984
+ await fs3.ensureDir(path4.join(projectRoot, ".silica/next/public"));
985
+ await fs3.writeFile(path4.join(destinationRoot, ".gitkeep"), "");
543
986
  }
544
987
  async function writeSitemapAndRobots(projectRoot, config, manifest) {
545
- const publicRoot = path3.join(projectRoot, ".silica/next/public");
546
- await fs2.ensureDir(publicRoot);
988
+ const publicRoot = path4.join(projectRoot, ".silica/next/public");
989
+ await fs3.ensureDir(publicRoot);
547
990
  const baseUrl = (config.baseUrl ?? "http://localhost:3000").replace(
548
991
  /\/$/,
549
992
  ""
550
993
  );
551
- const urls = manifest.entries.filter(isListedEntry).map(
994
+ const urls = manifest.entries.filter(isListedEntry2).map(
552
995
  (entry) => ` <url><loc>${baseUrl}${slugToHref(entry.slug)}</loc></url>`
553
996
  ).join("\n");
554
- if (!await fs2.pathExists(path3.join(projectRoot, "public/sitemap.xml"))) {
555
- await fs2.writeFile(
556
- path3.join(publicRoot, "sitemap.xml"),
997
+ if (!await fs3.pathExists(path4.join(projectRoot, "public/sitemap.xml"))) {
998
+ await fs3.writeFile(
999
+ path4.join(publicRoot, "sitemap.xml"),
557
1000
  `<?xml version="1.0" encoding="UTF-8"?>
558
1001
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
559
1002
  ${urls}
@@ -561,9 +1004,9 @@ ${urls}
561
1004
  `
562
1005
  );
563
1006
  }
564
- if (!await fs2.pathExists(path3.join(projectRoot, "public/robots.txt"))) {
565
- await fs2.writeFile(
566
- path3.join(publicRoot, "robots.txt"),
1007
+ if (!await fs3.pathExists(path4.join(projectRoot, "public/robots.txt"))) {
1008
+ await fs3.writeFile(
1009
+ path4.join(publicRoot, "robots.txt"),
567
1010
  `User-agent: *
568
1011
  Allow: /
569
1012
  Sitemap: ${baseUrl}/sitemap.xml
@@ -571,10 +1014,6 @@ Sitemap: ${baseUrl}/sitemap.xml
571
1014
  );
572
1015
  }
573
1016
  }
574
- async function writeJson(filePath, value) {
575
- await fs2.ensureDir(path3.dirname(filePath));
576
- await fs2.writeJson(filePath, value, { spaces: 2 });
577
- }
578
1017
  function getDate(value) {
579
1018
  if (value instanceof Date) return value;
580
1019
  if (typeof value === "string" || typeof value === "number") {
@@ -583,14 +1022,14 @@ function getDate(value) {
583
1022
  }
584
1023
  return void 0;
585
1024
  }
586
- function isListedEntry(entry) {
1025
+ function isListedEntry2(entry) {
587
1026
  return entry.frontmatter.listed !== false;
588
1027
  }
589
1028
  function stringifyDate(value) {
590
1029
  return value?.toISOString();
591
1030
  }
592
1031
  function titleFromFilePath(relativePath, ordering) {
593
- const stem = path3.posix.basename(normalizeGitPath(relativePath)).replace(/\.(md|markdown|mdx)$/i, "");
1032
+ const stem = path4.posix.basename(normalizeGitPath(relativePath)).replace(/\.(md|markdown|mdx)$/i, "");
594
1033
  const title = ordering.numericPrefixes ? stripNumericPrefix(stem) : stem;
595
1034
  return /^index$/i.test(title) ? "Home" : title;
596
1035
  }