@silicajs/core 0.4.0 → 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/README.md +1 -1
- package/dist/{chunk-FOKUARB2.js → chunk-BEINUFYU.js} +21 -8
- package/dist/chunk-BEINUFYU.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +511 -72
- package/dist/index.js.map +1 -1
- package/dist/precompute-worker.js +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +7 -1
- package/dist/{theme-D361xin9.d.ts → theme-BK5ZRXFG.d.ts} +69 -3
- package/dist/theme.d.ts +1 -1
- package/package.json +3 -2
- package/dist/chunk-FOKUARB2.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
slugifySegment,
|
|
33
33
|
stripNumericPrefix,
|
|
34
34
|
tagToHref
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-BEINUFYU.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
|
|
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
|
-
|
|
182
|
-
|
|
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 =
|
|
482
|
+
const runtimeContentRoot = path4.join(projectRoot, ".silica/content");
|
|
203
483
|
const relativeGitPaths = markdownFiles.map(
|
|
204
|
-
(file) => normalizeGitPath(
|
|
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
|
|
211
|
-
await
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
|
261
|
-
await
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
688
|
+
await fs3.emptyDir(runtimeContentRoot);
|
|
415
689
|
for (const file of files) {
|
|
416
|
-
const destination =
|
|
417
|
-
await
|
|
418
|
-
await
|
|
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
|
|
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:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
785
|
+
function makePrerenderManifest(manifest, graph, config) {
|
|
430
786
|
return {
|
|
431
787
|
version: 1,
|
|
432
|
-
|
|
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 =
|
|
531
|
-
await
|
|
973
|
+
const destinationRoot = path4.join(projectRoot, ".silica/next/public/silica");
|
|
974
|
+
await fs3.emptyDir(destinationRoot);
|
|
532
975
|
for (const asset of assets) {
|
|
533
|
-
await
|
|
534
|
-
|
|
976
|
+
await fs3.ensureDir(
|
|
977
|
+
path4.dirname(path4.join(destinationRoot, asset.relativePath))
|
|
535
978
|
);
|
|
536
|
-
await
|
|
979
|
+
await fs3.copyFile(
|
|
537
980
|
asset.absolutePath,
|
|
538
|
-
|
|
981
|
+
path4.join(destinationRoot, asset.relativePath)
|
|
539
982
|
);
|
|
540
983
|
}
|
|
541
|
-
await
|
|
542
|
-
await
|
|
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 =
|
|
546
|
-
await
|
|
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(
|
|
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
|
|
555
|
-
await
|
|
556
|
-
|
|
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
|
|
565
|
-
await
|
|
566
|
-
|
|
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
|
|
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 =
|
|
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
|
}
|