@silicajs/core 0.3.1 → 0.5.0

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
@@ -1,16 +1,19 @@
1
+ import {
2
+ formatPropertyLabel,
3
+ formatPropertyValue,
4
+ getMenuLabel,
5
+ getPageProperties
6
+ } from "./chunk-HFUUGJO6.js";
1
7
  import {
2
8
  analyzeMarkdown,
3
9
  asFilePath,
4
10
  asFullSlug,
5
11
  asRelativeURL,
6
12
  asSimpleSlug,
7
- formatPropertyLabel,
8
- formatPropertyValue,
13
+ createWikiLinkResolutionIndex,
9
14
  generateDescriptionFromContent,
10
15
  getDescription,
11
- getMenuLabel,
12
16
  getMetaDescription,
13
- getPageProperties,
14
17
  getTitle,
15
18
  hasNumericPrefixInPath,
16
19
  hrefToSlug,
@@ -29,7 +32,7 @@ import {
29
32
  slugifySegment,
30
33
  stripNumericPrefix,
31
34
  tagToHref
32
- } from "./chunk-7KWXP3FM.js";
35
+ } from "./chunk-BEINUFYU.js";
33
36
 
34
37
  // src/config.ts
35
38
  import path from "path";
@@ -103,9 +106,30 @@ function resolveConfig(config = {}, projectRoot = process.cwd()) {
103
106
  filters: {
104
107
  removeDrafts: config.filters?.removeDrafts ?? true,
105
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
+ }
106
116
  }
107
117
  };
108
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
+ }
109
133
 
110
134
  // src/files.ts
111
135
  import path2 from "path";
@@ -169,47 +193,312 @@ function isMarkdownFile(filePath) {
169
193
  // src/precompute.ts
170
194
  import crypto from "crypto";
171
195
  import { execFile } from "child_process";
172
- import path3 from "path";
196
+ import os from "os";
197
+ import path4 from "path";
173
198
  import { promisify } from "util";
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";
174
205
  import fs2 from "fs-extra";
175
206
  import {
176
- buildSearchDatabase,
177
- SEARCH_DATABASE_FILENAME
207
+ buildSearchTables,
208
+ makeExcerpt
178
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
179
463
  var execFileAsync = promisify(execFile);
464
+ var MIN_PARALLEL_ANALYSIS_FILES = 64;
465
+ var ANALYSIS_BATCH_SIZE = 16;
466
+ var MAX_ANALYSIS_WORKERS = 12;
467
+ var RENDER_CACHE_SCHEMA_VERSION = "silica-render-v1";
180
468
  async function precompute(options = {}) {
181
469
  const projectRoot = options.projectRoot ?? process.cwd();
182
470
  const config = options.config ?? await loadConfig(projectRoot);
183
471
  const scan = await scanContent(projectRoot, config);
184
472
  const markdownFiles = filterPublished(scan.markdown, config);
185
473
  const allSlugs = markdownFiles.map((file) => file.slug);
474
+ const wikilinkIndex = createWikiLinkResolutionIndex(
475
+ allSlugs,
476
+ config.ordering
477
+ );
186
478
  const entries = [];
187
479
  const graphLinks = {};
188
480
  const brokenLinks = [];
189
481
  const searchRecords = [];
190
- const runtimeContentRoot = path3.join(projectRoot, ".silica/content");
482
+ const runtimeContentRoot = path4.join(projectRoot, ".silica/content");
191
483
  const relativeGitPaths = markdownFiles.map(
192
- (file) => normalizeGitPath(path3.join(config.contentDir, file.relativePath))
484
+ (file) => normalizeGitPath(path4.join(config.contentDir, file.relativePath))
193
485
  );
194
486
  const gitDatesByPath = await getGitDatesForFiles(
195
487
  projectRoot,
196
488
  relativeGitPaths
197
489
  );
198
- await fs2.ensureDir(path3.join(projectRoot, ".silica"));
199
- 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"));
200
492
  await writeRuntimeMarkdown(runtimeContentRoot, markdownFiles);
201
- for (const file of markdownFiles) {
493
+ const analyses = await analyzeMarkdownFiles(markdownFiles, config, allSlugs, {
494
+ concurrency: options.analysisConcurrency,
495
+ wikilinkIndex
496
+ });
497
+ for (const [index, file] of markdownFiles.entries()) {
202
498
  const gitDates = gitDatesByPath.get(
203
- normalizeGitPath(path3.join(config.contentDir, file.relativePath))
499
+ normalizeGitPath(path4.join(config.contentDir, file.relativePath))
204
500
  ) ?? {};
205
- const analysis = await analyzeMarkdown(file.raw, {
206
- slug: asFullSlug(file.slug),
207
- allSlugs,
208
- assetBaseUrl: "/silica",
209
- wikilinkStrategy: config.wikilinks.strategy,
210
- tags: config.tags,
211
- ordering: config.ordering
212
- });
501
+ const analysis = analyses[index];
213
502
  const title = analysis.title ?? titleFromFilePath(file.relativePath, config.ordering);
214
503
  const menuLabel = getMenuLabel(file.frontmatter, title);
215
504
  const sortKey = config.ordering.numericPrefixes && hasNumericPrefixInPath(file.relativePath) ? numericPrefixSortKey(file.relativePath) : void 0;
@@ -220,7 +509,7 @@ async function precompute(options = {}) {
220
509
  description: analysis.description,
221
510
  generatedDescription: analysis.generatedDescription,
222
511
  tags: analysis.tags,
223
- file: normalizeGitPath(path3.join(".silica/content", file.relativePath)),
512
+ file: normalizeGitPath(path4.join(".silica/content", file.relativePath)),
224
513
  relativeFile: file.relativePath,
225
514
  sortKey,
226
515
  created: stringifyDate(
@@ -229,12 +518,14 @@ async function precompute(options = {}) {
229
518
  modified: stringifyDate(
230
519
  getDate(file.frontmatter.modified) ?? gitDates.modified ?? file.stats.mtime
231
520
  ),
232
- frontmatter: file.frontmatter
521
+ frontmatter: file.frontmatter,
522
+ contentHash: hashString(file.raw),
523
+ embeds: analysis.embeds
233
524
  };
234
525
  entries.push(entry);
235
526
  graphLinks[file.slug] = analysis.links;
236
527
  brokenLinks.push(...analysis.brokenLinks);
237
- if (isListedEntry(entry)) {
528
+ if (isListedEntry2(entry)) {
238
529
  searchRecords.push({
239
530
  id: file.slug,
240
531
  slug: file.slug,
@@ -248,27 +539,18 @@ async function precompute(options = {}) {
248
539
  await copyAssets(projectRoot, config, scan.assets);
249
540
  const manifest = makeManifest(config, entries);
250
541
  const graph = makeGraph(graphLinks, brokenLinks);
251
- const buildId = crypto.randomUUID();
252
- await buildSearchDatabase(
253
- searchRecords,
254
- path3.join(projectRoot, ".silica", SEARCH_DATABASE_FILENAME)
255
- );
256
- await fs2.remove(path3.join(projectRoot, ".silica/search-index.json"));
257
- await writeJson(
258
- path3.join(projectRoot, ".silica/manifest.json"),
259
- serializeManifest(manifest)
260
- );
261
- await writeJson(
262
- path3.join(projectRoot, ".silica/navigation.json"),
263
- makeNavigation(manifest)
264
- );
265
- await writeJson(path3.join(projectRoot, ".silica/graph.json"), graph);
266
- await writeJson(path3.join(projectRoot, ".silica/config.json"), config);
267
- await fs2.writeFile(
268
- path3.join(projectRoot, ".silica/build-id.txt"),
269
- `${buildId}
270
- `
271
- );
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
+ });
272
554
  await writeSitemapAndRobots(projectRoot, config, manifest);
273
555
  if (config.wikilinks.strict && brokenLinks.length > 0) {
274
556
  const message = brokenLinks.map((link) => `${link.source} -> ${link.target}`).join("\n");
@@ -279,36 +561,304 @@ ${message}`);
279
561
  manifest,
280
562
  graph,
281
563
  searchRecords,
282
- buildId,
564
+ prerender,
565
+ cacheState,
283
566
  brokenLinks
284
567
  };
285
568
  }
569
+ async function analyzeMarkdownFiles(files, config, allSlugs, options) {
570
+ const workerCount = getAnalysisWorkerCount(files.length, options.concurrency);
571
+ if (workerCount <= 1) {
572
+ return analyzeMarkdownFilesSerial(files, config, options.wikilinkIndex);
573
+ }
574
+ return analyzeMarkdownFilesParallel(files, config, allSlugs, workerCount);
575
+ }
576
+ async function analyzeMarkdownFilesSerial(files, config, wikilinkIndex) {
577
+ const analyses = [];
578
+ for (const file of files) {
579
+ analyses.push(
580
+ await analyzeMarkdown(file.raw, {
581
+ slug: asFullSlug(file.slug),
582
+ wikilinkIndex,
583
+ assetBaseUrl: "/silica",
584
+ wikilinkStrategy: config.wikilinks.strategy,
585
+ tags: config.tags,
586
+ ordering: config.ordering
587
+ })
588
+ );
589
+ }
590
+ return analyses;
591
+ }
592
+ function analyzeMarkdownFilesParallel(files, config, allSlugs, workerCount) {
593
+ return new Promise((resolve, reject) => {
594
+ const workerUrl = new URL("./precompute-worker.js", import.meta.url);
595
+ const workers = [];
596
+ const analyses = new Array(files.length);
597
+ let nextIndex = 0;
598
+ let completed = 0;
599
+ let settled = false;
600
+ const rejectOnce = (error) => {
601
+ if (settled) return;
602
+ settled = true;
603
+ for (const worker of workers) {
604
+ void worker.terminate();
605
+ }
606
+ reject(error);
607
+ };
608
+ const resolveIfDone = () => {
609
+ if (completed < files.length || settled) return;
610
+ settled = true;
611
+ for (const worker of workers) {
612
+ void worker.terminate();
613
+ }
614
+ resolve(analyses);
615
+ };
616
+ const sendNext = (worker) => {
617
+ if (settled || nextIndex >= files.length) return;
618
+ const start = nextIndex;
619
+ const batch = files.slice(start, start + ANALYSIS_BATCH_SIZE).map((file, offset) => ({
620
+ index: start + offset,
621
+ slug: file.slug,
622
+ raw: file.raw
623
+ }));
624
+ nextIndex += batch.length;
625
+ worker.postMessage({
626
+ id: start,
627
+ files: batch
628
+ });
629
+ };
630
+ for (let index = 0; index < workerCount; index += 1) {
631
+ const worker = new Worker(workerUrl, {
632
+ workerData: {
633
+ allSlugs,
634
+ wikilinkStrategy: config.wikilinks.strategy,
635
+ tags: config.tags,
636
+ ordering: config.ordering
637
+ }
638
+ });
639
+ workers.push(worker);
640
+ worker.on("message", (message) => {
641
+ if (message.error) {
642
+ rejectOnce(new Error(message.error));
643
+ return;
644
+ }
645
+ for (const result of message.results ?? []) {
646
+ analyses[result.index] = result.analysis;
647
+ }
648
+ completed += message.results?.length ?? 0;
649
+ resolveIfDone();
650
+ sendNext(worker);
651
+ });
652
+ worker.on("error", rejectOnce);
653
+ worker.on("exit", (code) => {
654
+ if (!settled && code !== 0) {
655
+ rejectOnce(
656
+ new Error(`Precompute analysis worker exited with ${code}`)
657
+ );
658
+ }
659
+ });
660
+ sendNext(worker);
661
+ }
662
+ });
663
+ }
664
+ function getAnalysisWorkerCount(fileCount, requestedConcurrency) {
665
+ if (fileCount === 0) return 1;
666
+ if (requestedConcurrency === void 0 && fileCount < MIN_PARALLEL_ANALYSIS_FILES) {
667
+ return 1;
668
+ }
669
+ const available = Math.max(
670
+ 1,
671
+ os.availableParallelism?.() ?? os.cpus().length
672
+ );
673
+ const requested = getRequestedAnalysisConcurrency(
674
+ requestedConcurrency,
675
+ available
676
+ );
677
+ const usefulWorkers = Math.ceil(fileCount / ANALYSIS_BATCH_SIZE);
678
+ return Math.max(1, Math.min(fileCount, requested, usefulWorkers));
679
+ }
680
+ function getRequestedAnalysisConcurrency(requestedConcurrency, available) {
681
+ if (requestedConcurrency === void 0) {
682
+ return Math.min(available, MAX_ANALYSIS_WORKERS);
683
+ }
684
+ if (!Number.isFinite(requestedConcurrency)) return 1;
685
+ return Math.max(1, Math.floor(requestedConcurrency));
686
+ }
286
687
  async function writeRuntimeMarkdown(runtimeContentRoot, files) {
287
- await fs2.emptyDir(runtimeContentRoot);
688
+ await fs3.emptyDir(runtimeContentRoot);
288
689
  for (const file of files) {
289
- const destination = path3.join(runtimeContentRoot, file.relativePath);
290
- await fs2.ensureDir(path3.dirname(destination));
291
- 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);
292
693
  }
293
694
  }
294
- 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
+ });
295
725
  return {
296
- version: manifest.version,
297
- generatedAt: manifest.generatedAt,
298
- contentDir: manifest.contentDir,
299
- 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()
300
738
  };
301
739
  }
302
- function makeNavigation(manifest) {
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;
779
+ };
780
+ for (const slug of manifest.allSlugs) {
781
+ renderHashForSlug(slug);
782
+ }
783
+ return Object.fromEntries(memo);
784
+ }
785
+ function makePrerenderManifest(manifest, graph, config) {
303
786
  return {
304
787
  version: 1,
305
- entries: manifest.entries.filter(isListedEntry).map((entry) => ({
306
- slug: entry.slug,
307
- title: entry.menuLabel,
308
- sortKey: entry.sortKey
309
- }))
788
+ slugs: selectPrerenderSlugs(manifest, graph, config)
310
789
  };
311
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
+ }
312
862
  async function getGitDates(projectRoot, relativePath) {
313
863
  return (await getGitDatesForFiles(projectRoot, [normalizeGitPath(relativePath)])).get(normalizeGitPath(relativePath)) ?? {};
314
864
  }
@@ -349,6 +899,26 @@ async function getGitDatesForFiles(projectRoot, relativePaths) {
349
899
  function normalizeGitPath(relativePath) {
350
900
  return relativePath.replace(/\\/g, "/");
351
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
+ }
352
922
  function filterPublished(files, config) {
353
923
  return files.filter((file) => {
354
924
  if (config.filters.removeDrafts && file.frontmatter.draft === true)
@@ -400,33 +970,33 @@ function makeGraph(links, brokenLinks) {
400
970
  };
401
971
  }
402
972
  async function copyAssets(projectRoot, config, assets) {
403
- const destinationRoot = path3.join(projectRoot, ".silica/next/public/silica");
404
- await fs2.emptyDir(destinationRoot);
973
+ const destinationRoot = path4.join(projectRoot, ".silica/next/public/silica");
974
+ await fs3.emptyDir(destinationRoot);
405
975
  for (const asset of assets) {
406
- await fs2.ensureDir(
407
- path3.dirname(path3.join(destinationRoot, asset.relativePath))
976
+ await fs3.ensureDir(
977
+ path4.dirname(path4.join(destinationRoot, asset.relativePath))
408
978
  );
409
- await fs2.copyFile(
979
+ await fs3.copyFile(
410
980
  asset.absolutePath,
411
- path3.join(destinationRoot, asset.relativePath)
981
+ path4.join(destinationRoot, asset.relativePath)
412
982
  );
413
983
  }
414
- await fs2.ensureDir(path3.join(projectRoot, ".silica/next/public"));
415
- 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"), "");
416
986
  }
417
987
  async function writeSitemapAndRobots(projectRoot, config, manifest) {
418
- const publicRoot = path3.join(projectRoot, ".silica/next/public");
419
- await fs2.ensureDir(publicRoot);
988
+ const publicRoot = path4.join(projectRoot, ".silica/next/public");
989
+ await fs3.ensureDir(publicRoot);
420
990
  const baseUrl = (config.baseUrl ?? "http://localhost:3000").replace(
421
991
  /\/$/,
422
992
  ""
423
993
  );
424
- const urls = manifest.entries.filter(isListedEntry).map(
994
+ const urls = manifest.entries.filter(isListedEntry2).map(
425
995
  (entry) => ` <url><loc>${baseUrl}${slugToHref(entry.slug)}</loc></url>`
426
996
  ).join("\n");
427
- if (!await fs2.pathExists(path3.join(projectRoot, "public/sitemap.xml"))) {
428
- await fs2.writeFile(
429
- 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"),
430
1000
  `<?xml version="1.0" encoding="UTF-8"?>
431
1001
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
432
1002
  ${urls}
@@ -434,9 +1004,9 @@ ${urls}
434
1004
  `
435
1005
  );
436
1006
  }
437
- if (!await fs2.pathExists(path3.join(projectRoot, "public/robots.txt"))) {
438
- await fs2.writeFile(
439
- 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"),
440
1010
  `User-agent: *
441
1011
  Allow: /
442
1012
  Sitemap: ${baseUrl}/sitemap.xml
@@ -444,10 +1014,6 @@ Sitemap: ${baseUrl}/sitemap.xml
444
1014
  );
445
1015
  }
446
1016
  }
447
- async function writeJson(filePath, value) {
448
- await fs2.ensureDir(path3.dirname(filePath));
449
- await fs2.writeJson(filePath, value, { spaces: 2 });
450
- }
451
1017
  function getDate(value) {
452
1018
  if (value instanceof Date) return value;
453
1019
  if (typeof value === "string" || typeof value === "number") {
@@ -456,14 +1022,14 @@ function getDate(value) {
456
1022
  }
457
1023
  return void 0;
458
1024
  }
459
- function isListedEntry(entry) {
1025
+ function isListedEntry2(entry) {
460
1026
  return entry.frontmatter.listed !== false;
461
1027
  }
462
1028
  function stringifyDate(value) {
463
1029
  return value?.toISOString();
464
1030
  }
465
1031
  function titleFromFilePath(relativePath, ordering) {
466
- 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, "");
467
1033
  const title = ordering.numericPrefixes ? stripNumericPrefix(stem) : stem;
468
1034
  return /^index$/i.test(title) ? "Home" : title;
469
1035
  }
@@ -473,6 +1039,7 @@ export {
473
1039
  asFullSlug,
474
1040
  asRelativeURL,
475
1041
  asSimpleSlug,
1042
+ createWikiLinkResolutionIndex,
476
1043
  defineConfig,
477
1044
  formatPropertyLabel,
478
1045
  formatPropertyValue,