@abreen/tada 1.17.1 → 1.18.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.
Files changed (111) hide show
  1. package/README.md +38 -7
  2. package/assets/material-symbols/chevron_right_24px.svg +1 -0
  3. package/bin/tada.ts +16 -7
  4. package/bin/validators.ts +33 -0
  5. package/build/branding.ts +41 -0
  6. package/build/build-manifest.ts +9 -4
  7. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  8. package/build/build-validation.ts +176 -0
  9. package/build/bundle.ts +75 -65
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/generate-favicon.ts +7 -6
  13. package/build/generate-fonts.ts +8 -15
  14. package/build/generate-katex-assets.ts +19 -17
  15. package/build/generate-web-app-manifest.ts +3 -11
  16. package/build/lodash-template.ts +14 -0
  17. package/build/material-symbols.ts +7 -4
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -244
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +5 -2
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +44 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +38 -101
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/literate-java.ts +11 -7
  34. package/build/utils/markdown-partials.ts +4 -3
  35. package/build/utils/markdown.ts +6 -17
  36. package/build/utils/paths.ts +3 -0
  37. package/build/utils/plain-text.ts +18 -0
  38. package/build/utils/render.ts +227 -165
  39. package/build/utils/shiki-highlighter.ts +11 -0
  40. package/build/utils/source-template.ts +2 -2
  41. package/build/utils/trace-core.ts +8 -83
  42. package/build/utils/trace.ts +38 -95
  43. package/build/validate-config-links.ts +60 -20
  44. package/build/watch/build-incremental.ts +46 -103
  45. package/build/watch/compiler.ts +158 -37
  46. package/build/watch/engine.ts +207 -176
  47. package/build/watch/index.ts +9 -2
  48. package/build/watch/planner.ts +88 -144
  49. package/build/watch/runtime.ts +21 -34
  50. package/build/watch/types.d.ts +32 -64
  51. package/init/content/labs/00/VowelCounter.java.md +5 -2
  52. package/init/content/labs/00/index.md +3 -2
  53. package/init/content/labs/01/index.md +3 -2
  54. package/init/content/labs/02/index.md +3 -2
  55. package/init/content/lectures/01/index.md +3 -2
  56. package/init/content/lectures/02/index.md +3 -2
  57. package/package.json +4 -4
  58. package/schema/site.schema.json +18 -2
  59. package/src/_content.scss +463 -0
  60. package/src/_fonts.scss +90 -0
  61. package/src/_layout.scss +50 -35
  62. package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
  63. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  64. package/src/code/{style.scss → _index.scss} +61 -0
  65. package/src/header/{_base.scss → _index.scss} +98 -6
  66. package/src/index.ts +1 -17
  67. package/src/navigate/{style.scss → _index.scss} +29 -3
  68. package/src/navigate/breadcrumbs.ts +129 -0
  69. package/src/navigate/eligible.ts +0 -40
  70. package/src/navigate/index.ts +38 -4
  71. package/src/navigate/lifecycle.ts +19 -2
  72. package/src/navigate/runtime.ts +57 -22
  73. package/src/page-update/index.ts +33 -20
  74. package/src/print/{style.scss → _index.scss} +3 -1
  75. package/src/search/index.ts +7 -0
  76. package/src/slides/{style.scss → _index.scss} +5 -0
  77. package/src/style.scss +24 -536
  78. package/src/toc/index.ts +2 -2
  79. package/templates/_author.html +2 -2
  80. package/templates/_download.html +1 -1
  81. package/templates/_footer.html +1 -1
  82. package/templates/_heading.html +17 -4
  83. package/templates/_nav.html +2 -2
  84. package/templates/_page-bottom.html +6 -0
  85. package/templates/_top.html +18 -14
  86. package/templates/code.html +22 -21
  87. package/templates/default.html +12 -11
  88. package/templates/literate.html +6 -5
  89. package/assets/material-symbols/south_east_20px.svg +0 -1
  90. package/build/copy.ts +0 -82
  91. package/build/generate-content-assets.ts +0 -121
  92. package/build/utils/content-files.ts +0 -170
  93. package/build/watch/assets.ts +0 -103
  94. package/build/watch/build-full.ts +0 -97
  95. package/build/watch/build-helpers.ts +0 -66
  96. package/build/watch/build-result.ts +0 -19
  97. package/build/watch/fs-commit.ts +0 -218
  98. package/build/watch/mutations.ts +0 -73
  99. package/build/watch/snapshot.ts +0 -201
  100. package/build/watch/validation.ts +0 -57
  101. package/src/code.scss +0 -60
  102. package/src/critical.scss +0 -5
  103. package/src/header/style.scss +0 -91
  104. /package/src/{literate.scss → _literate.scss} +0 -0
  105. /package/src/anchor/{style.scss → _index.scss} +0 -0
  106. /package/src/page-update/{style.scss → _index.scss} +0 -0
  107. /package/src/question/{style.scss → _index.scss} +0 -0
  108. /package/src/search/{style.scss → _index.scss} +0 -0
  109. /package/src/timezone/{style.scss → _index.scss} +0 -0
  110. /package/src/toc/{style.scss → _index.scss} +0 -0
  111. /package/src/trace/{style.scss → _index.scss} +0 -0
package/build/pdf-text.ts CHANGED
@@ -137,7 +137,7 @@ export function buildPdfPageRecords(pageTexts: string[]): PdfExtractResult {
137
137
  return { pages, hasExtractedText: pages.length > 0 };
138
138
  }
139
139
 
140
- export async function extractPdfPages(
140
+ async function extractPdfPagesUncached(
141
141
  pdfPath: string,
142
142
  ): Promise<PdfExtractResult> {
143
143
  await assertMutoolAvailable();
@@ -181,3 +181,43 @@ export async function extractPdfPages(
181
181
  }
182
182
  }
183
183
  }
184
+
185
+ interface FileSignature {
186
+ size: number;
187
+ mtimeMs: number;
188
+ }
189
+
190
+ /**
191
+ * Wraps a PDF text extractor so each PDF is extracted again only after its size
192
+ * or modification time changes. Concurrent requests for one file share a
193
+ * single extraction, and failed extractions are not cached.
194
+ */
195
+ export function createCachedPdfExtractor(
196
+ extract: (pdfPath: string) => Promise<PdfExtractResult>,
197
+ stat: (pdfPath: string) => FileSignature,
198
+ ): (pdfPath: string) => Promise<PdfExtractResult> {
199
+ const cache = new Map<
200
+ string,
201
+ FileSignature & { result: Promise<PdfExtractResult> }
202
+ >();
203
+ return pdfPath => {
204
+ const { size, mtimeMs } = stat(pdfPath);
205
+ const cached = cache.get(pdfPath);
206
+ if (cached && cached.size === size && cached.mtimeMs === mtimeMs) {
207
+ return cached.result;
208
+ }
209
+ const entry = { size, mtimeMs, result: extract(pdfPath) };
210
+ cache.set(pdfPath, entry);
211
+ entry.result.catch(() => {
212
+ if (cache.get(pdfPath) === entry) {
213
+ cache.delete(pdfPath);
214
+ }
215
+ });
216
+ return entry.result;
217
+ };
218
+ }
219
+
220
+ export const extractPdfPages = createCachedPdfExtractor(
221
+ extractPdfPagesUncached,
222
+ pdfPath => fs.statSync(pdfPath),
223
+ );
package/build/pipeline.ts CHANGED
@@ -1,123 +1,117 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
2
3
  import { getDevSiteVariables, getProdSiteVariables } from './site-variables';
3
- import { compileTemplates } from './templates';
4
4
  import { getDistDir, getProdDistDir } from './utils/paths';
5
5
  import { isFeatureEnabled } from './features';
6
- import { bundle } from './bundle';
7
- import { copyFonts } from './generate-fonts';
8
- import { copyKatexAssets } from './generate-katex-assets';
9
- import { generateFavicons } from './generate-favicon';
10
- import { generateWebAppManifest } from './generate-web-app-manifest';
11
- import { copyPublicFiles, copyContentAssets } from './copy';
12
- import { ContentRenderer } from './generate-content-assets';
13
- import { runPagefind } from './pagefind';
6
+ import { getKeptOutputPrefixes, runPagefind } from './pagefind';
14
7
  import { makeLogger, printFlair } from './log';
15
- import { generateBuildManifest, getNextVersion } from './build-manifest';
16
- import { scanProject } from './source-model';
17
- import { validateConfig } from './watch/validation';
18
- import { applyCommitPlan } from './watch/fs-commit';
19
- import path from 'path';
8
+ import {
9
+ generateBuildManifest,
10
+ getNextVersion,
11
+ MANIFEST_FILE_NAME,
12
+ } from './build-manifest';
13
+ import { buildSite, createBuildMeta, type TadaSnapshot } from './site-build';
14
+ import {
15
+ BuildFailedError,
16
+ diagnosticFromError,
17
+ printDiagnostics,
18
+ } from './build-validation';
19
+ import { applyMutations, planFullWrite } from './output-publication';
20
+ import type { BuildDiagnostic } from './build-types';
21
+ import type { SiteVariables } from './types';
20
22
 
21
23
  const log = makeLogger(import.meta.url);
22
24
 
23
- function makeTempOutputDir(targetDir: string): string {
24
- const parentDir = path.dirname(targetDir);
25
- fs.mkdirSync(parentDir, { recursive: true });
26
- return fs.mkdtempSync(
27
- path.join(parentDir, `${path.basename(targetDir)}-build-`),
25
+ function fail(diagnostics: BuildDiagnostic[]): never {
26
+ printDiagnostics(diagnostics);
27
+ const noun = diagnostics.length === 1 ? 'error' : 'errors';
28
+ throw new BuildFailedError(`${diagnostics.length} ${noun}`);
29
+ }
30
+
31
+ async function indexSearch(
32
+ outputDir: string,
33
+ snapshot: TadaSnapshot,
34
+ ): Promise<void> {
35
+ if (isFeatureEnabled(snapshot.siteVariables, 'search')) {
36
+ const meta = createBuildMeta(snapshot);
37
+ await runPagefind({ distPath: outputDir, ...meta });
38
+ }
39
+ }
40
+
41
+ /** Updates `dist/` in place to hold exactly this build's outputs. */
42
+ async function publishDev(snapshot: TadaSnapshot): Promise<void> {
43
+ const distDir = getDistDir();
44
+ applyMutations(
45
+ distDir,
46
+ planFullWrite(
47
+ distDir,
48
+ snapshot.outputs,
49
+ getKeptOutputPrefixes(snapshot.siteVariables),
50
+ ),
28
51
  );
52
+ await indexSearch(distDir, snapshot);
29
53
  }
30
54
 
31
- function removeDirIfExists(dir: string): void {
32
- fs.rmSync(dir, { recursive: true, force: true });
55
+ /** Writes a new `dist-prod/v{N}/`; it counts as a version once its manifest is written. */
56
+ async function publishProd(snapshot: TadaSnapshot): Promise<void> {
57
+ const prodBase = getProdDistDir();
58
+ const version = getNextVersion(prodBase);
59
+ const versionDir = path.join(prodBase, `v${version}`);
60
+ // A directory without a manifest is left over from a failed build.
61
+ fs.rmSync(versionDir, { recursive: true, force: true });
62
+ try {
63
+ applyMutations(versionDir, planFullWrite(versionDir, snapshot.outputs));
64
+ await indexSearch(versionDir, snapshot);
65
+ await generateBuildManifest(
66
+ versionDir,
67
+ path.join(versionDir, MANIFEST_FILE_NAME),
68
+ version,
69
+ );
70
+ } catch (error) {
71
+ try {
72
+ fs.rmSync(versionDir, { recursive: true, force: true });
73
+ } catch {
74
+ // Without a manifest, the leftover directory is ignored and replaced.
75
+ }
76
+ throw error;
77
+ }
78
+ log.info`Built dist-prod/v${version}/`;
33
79
  }
34
80
 
35
81
  export async function runPipeline(
36
82
  mode: 'development' | 'production',
37
83
  ): Promise<void> {
38
84
  const isDev = mode === 'development';
39
- const siteVariables = isDev ? getDevSiteVariables() : getProdSiteVariables();
40
- const scan = scanProject(siteVariables);
41
- const publishDir = isDev
42
- ? getDistDir()
43
- : path.join(getProdDistDir(), 'v-next');
44
- const distDir = makeTempOutputDir(publishDir);
45
- let published = false;
46
-
85
+ let siteVariables: SiteVariables;
47
86
  try {
48
- const configDiagnostics = validateConfig(scan, siteVariables);
49
- if (configDiagnostics.length > 0) {
50
- throw new Error(configDiagnostics[0].message);
51
- }
52
-
53
- compileTemplates(siteVariables);
54
- const contentRenderer = new ContentRenderer(siteVariables);
55
- await contentRenderer.initHighlighter();
56
-
57
- const parallelTasks: (Promise<unknown> | void)[] = [
58
- bundle(siteVariables, { mode, distDir }),
59
- copyFonts(distDir),
60
- copyKatexAssets(distDir),
61
- ];
62
-
63
- if (isFeatureEnabled(siteVariables, 'favicon')) {
64
- parallelTasks.push(generateFavicons(siteVariables, distDir));
65
- }
66
-
67
- const results = await Promise.all(parallelTasks);
68
- const assetFiles = results[0] as string[]; // bundle output filenames
69
-
70
- if (isFeatureEnabled(siteVariables, 'favicon')) {
71
- generateWebAppManifest(siteVariables, distDir);
72
- }
73
-
74
- const publicRelPaths = copyPublicFiles(scan.publicDir, distDir);
75
- copyContentAssets(
76
- scan.contentDir,
77
- distDir,
78
- [...scan.processedExts],
79
- publicRelPaths,
80
- );
81
-
82
- const { errors, htmlAssetsByPath, htmlAnalysisByPath } =
83
- contentRenderer.processContent({ distDir, assetFiles, scan });
84
-
85
- for (const err of errors) {
86
- log.error`${err.message}`;
87
- }
88
- if (errors.length > 0) {
89
- throw new Error(errors[0].message);
90
- }
91
-
92
- if (isFeatureEnabled(siteVariables, 'search')) {
93
- await runPagefind({
94
- distPath: distDir,
95
- htmlAssetsByPath,
96
- htmlAnalysisByPath,
97
- });
98
- }
99
-
100
- let finalOutputDir = getDistDir();
101
-
102
- if (!isDev) {
103
- const prodBase = getProdDistDir();
104
- const prodVersion = getNextVersion(prodBase);
105
- finalOutputDir = path.join(prodBase, `v${prodVersion}`);
106
- const manifestPath = path.join(distDir, 'tada.manifest.json');
107
- await generateBuildManifest(distDir, manifestPath, prodVersion);
108
- log.info`Built dist-prod/v${prodVersion}/`;
109
- }
87
+ siteVariables = isDev ? getDevSiteVariables() : getProdSiteVariables();
88
+ } catch (error) {
89
+ fail([diagnosticFromError(error)]);
90
+ }
110
91
 
111
- applyCommitPlan({
112
- kind: 'replace-root',
113
- stagedPath: distDir,
114
- targetPath: finalOutputDir,
92
+ let result: Awaited<ReturnType<typeof buildSite>>;
93
+ try {
94
+ result = await buildSite({
95
+ siteVariables,
96
+ mode,
97
+ isWatchMode: false,
98
+ traceCache: new Map(),
115
99
  });
116
- published = true;
117
- printFlair();
118
- } finally {
119
- if (!published) {
120
- removeDirIfExists(distDir);
121
- }
100
+ } catch (error) {
101
+ fail([diagnosticFromError(error)]);
102
+ }
103
+ if (!result.ok) {
104
+ fail(result.diagnostics);
105
+ }
106
+
107
+ try {
108
+ await (isDev ? publishDev(result.snapshot) : publishProd(result.snapshot));
109
+ } catch (error) {
110
+ fail([
111
+ diagnosticFromError(
112
+ new Error(`Failed to write output: ${(error as Error).message}`),
113
+ ),
114
+ ]);
122
115
  }
116
+ printFlair();
123
117
  }
@@ -0,0 +1,46 @@
1
+ import { bundle, bundleReloadClient } from './bundle';
2
+ import { isFeatureEnabled } from './features';
3
+ import { generateFavicons } from './generate-favicon';
4
+ import { getFontOutputs } from './generate-fonts';
5
+ import { getKatexOutputs } from './generate-katex-assets';
6
+ import { createWebAppManifest } from './generate-web-app-manifest';
7
+ import type { OutputContent, SiteVariables } from './types';
8
+
9
+ /** Files Tada generates itself, as opposed to files rendered from sources */
10
+ interface SiteAssets {
11
+ /** CSS and JavaScript bundles that every page links to */
12
+ assetFiles: string[];
13
+ /** Every generated file, keyed by output path */
14
+ outputs: Map<string, OutputContent>;
15
+ }
16
+
17
+ export async function generateSiteAssets(
18
+ siteVariables: SiteVariables,
19
+ {
20
+ mode,
21
+ isWatchMode,
22
+ }: { mode: 'development' | 'production'; isWatchMode: boolean },
23
+ ): Promise<SiteAssets> {
24
+ const generateFaviconFiles =
25
+ isFeatureEnabled(siteVariables, 'favicon') && !siteVariables.favicon;
26
+ const [bundles, reloadClient, favicons] = await Promise.all([
27
+ bundle(siteVariables, { mode }),
28
+ isWatchMode ? bundleReloadClient() : new Map<string, string>(),
29
+ generateFaviconFiles
30
+ ? generateFavicons(siteVariables)
31
+ : new Map<string, string | Uint8Array>(),
32
+ ]);
33
+ const bundleFiles = new Map([...bundles, ...reloadClient]);
34
+
35
+ const outputs = new Map<string, OutputContent>([
36
+ ...bundleFiles,
37
+ ...getFontOutputs(),
38
+ ...getKatexOutputs(),
39
+ ...favicons,
40
+ ]);
41
+ if (generateFaviconFiles) {
42
+ outputs.set('manifest.json', createWebAppManifest(siteVariables));
43
+ }
44
+
45
+ return { assetFiles: [...bundleFiles.keys()], outputs };
46
+ }
@@ -0,0 +1,267 @@
1
+ import path from 'path';
2
+ import { compileTemplates, config } from './templates';
3
+ import { getProjectDir, toPosix } from './utils/paths';
4
+ import { makeLogger } from './log';
5
+ import { getRuntimeBundledShikiLanguages } from './site-variables';
6
+ import { initHighlighter } from './utils/shiki-highlighter';
7
+ import { generateSiteAssets } from './site-assets';
8
+ import { getPdfSources } from './pagefind';
9
+ import { checkTraceToolAvailability } from './utils/trace';
10
+ import { scanProject, sourcePaths, type TadaProjectScan } from './source-model';
11
+ import {
12
+ createContentRecord,
13
+ createPublicRecord,
14
+ type TadaSourceRecord,
15
+ } from './source-records';
16
+ import {
17
+ diagnosticFromError,
18
+ findOutputConflicts,
19
+ validateConfig,
20
+ validateProjectConfigLinks,
21
+ } from './build-validation';
22
+ import type { OutputFile } from './output-publication';
23
+ import type {
24
+ OutputContent,
25
+ SiteVariables,
26
+ TraceToolAvailability,
27
+ } from './types';
28
+ import type { BuildDiagnostic, TadaBuildMeta, TraceCache } from './build-types';
29
+
30
+ const log = makeLogger(import.meta.url);
31
+
32
+ /** The `sourcePath` recorded for files Tada generates itself */
33
+ export const GENERATED_SOURCE = 'tada';
34
+
35
+ /** Everything one successful build produced, kept by watch mode */
36
+ export interface TadaSnapshot {
37
+ siteVariables: SiteVariables;
38
+ assetFiles: string[];
39
+ generatedOutputs: ReadonlyMap<string, OutputContent>;
40
+ authorsData: unknown;
41
+ records: ReadonlyMap<string, TadaSourceRecord>;
42
+ outputs: ReadonlyMap<string, OutputFile>;
43
+ fileDependents: ReadonlyMap<string, ReadonlySet<string>>;
44
+ targetDependents: ReadonlyMap<string, ReadonlySet<string>>;
45
+ authorDependents: ReadonlyMap<string, ReadonlySet<string>>;
46
+ scan: TadaProjectScan;
47
+ }
48
+
49
+ type SnapshotInputs = Pick<
50
+ TadaSnapshot,
51
+ 'siteVariables' | 'assetFiles' | 'authorsData' | 'records' | 'scan'
52
+ > & { generatedOutputs?: ReadonlyMap<string, OutputContent> };
53
+
54
+ export type SiteBuildResult =
55
+ | { ok: true; snapshot: TadaSnapshot }
56
+ | { ok: false; diagnostics: BuildDiagnostic[] };
57
+
58
+ export interface RenderContext {
59
+ siteVariables: SiteVariables;
60
+ scan: TadaProjectScan;
61
+ assetFiles: string[];
62
+ isWatchMode: boolean;
63
+ traceCache: TraceCache;
64
+ traceToolAvailability: TraceToolAvailability;
65
+ }
66
+
67
+ export function createSnapshot(inputs: SnapshotInputs): TadaSnapshot {
68
+ const generatedOutputs = inputs.generatedOutputs ?? new Map();
69
+ const outputs = new Map<string, OutputFile>();
70
+ for (const [outputPath, content] of generatedOutputs) {
71
+ outputs.set(outputPath, { sourcePath: GENERATED_SOURCE, content });
72
+ }
73
+ const fileDependents = new Map<string, Set<string>>();
74
+ const targetDependents = new Map<string, Set<string>>();
75
+ const authorDependents = new Map<string, Set<string>>();
76
+ for (const record of inputs.records.values()) {
77
+ for (const [outputPath, content] of record.outputs) {
78
+ outputs.set(outputPath, { sourcePath: record.sourcePath, content });
79
+ }
80
+ for (const [index, keys] of [
81
+ [fileDependents, [...record.partialDeps, ...record.traceDeps]],
82
+ [targetDependents, record.internalTargets],
83
+ [authorDependents, record.authorKey ? [record.authorKey] : []],
84
+ ] as const) {
85
+ for (const key of keys) {
86
+ if (!index.has(key)) {
87
+ index.set(key, new Set());
88
+ }
89
+ index.get(key)!.add(record.sourcePath);
90
+ }
91
+ }
92
+ }
93
+ return {
94
+ ...inputs,
95
+ generatedOutputs,
96
+ outputs,
97
+ fileDependents,
98
+ targetDependents,
99
+ authorDependents,
100
+ };
101
+ }
102
+
103
+ export function createBuildMeta(snapshot: TadaSnapshot): TadaBuildMeta {
104
+ const htmlAssetsByPath = new Map<string, string>();
105
+ const htmlAnalysisByPath: TadaBuildMeta['htmlAnalysisByPath'] = new Map();
106
+ for (const record of snapshot.records.values()) {
107
+ if (record.kind !== 'content') {
108
+ continue;
109
+ }
110
+ for (const [outputPath, content] of record.outputs) {
111
+ if (outputPath.endsWith('.html') && typeof content === 'string') {
112
+ htmlAssetsByPath.set(outputPath, content);
113
+ }
114
+ }
115
+ for (const [outputPath, analysis] of record.htmlAnalysisByOutputPath ??
116
+ []) {
117
+ htmlAnalysisByPath.set(outputPath, analysis);
118
+ }
119
+ }
120
+ return {
121
+ htmlAssetsByPath,
122
+ htmlAnalysisByPath,
123
+ pdfSourceByOutputPath: getPdfSources(snapshot.scan),
124
+ siteVariables: snapshot.siteVariables,
125
+ };
126
+ }
127
+
128
+ export async function ensureHighlighter(
129
+ siteVariables: SiteVariables,
130
+ ): Promise<void> {
131
+ await initHighlighter(getRuntimeBundledShikiLanguages(siteVariables));
132
+ }
133
+
134
+ /**
135
+ * Renders each source into a record. A source that fails to render becomes a
136
+ * diagnostic, and rendering continues so every page error is reported.
137
+ */
138
+ export function renderSources(
139
+ filePaths: Iterable<string>,
140
+ context: RenderContext,
141
+ ): { records: Map<string, TadaSourceRecord>; diagnostics: BuildDiagnostic[] } {
142
+ const records = new Map<string, TadaSourceRecord>();
143
+ const diagnostics: BuildDiagnostic[] = [];
144
+ for (const filePath of filePaths) {
145
+ try {
146
+ const record =
147
+ context.scan.sources.get(filePath)?.kind === 'public'
148
+ ? createPublicRecord(filePath, context.scan.publicDir)
149
+ : createContentRecord({
150
+ filePath,
151
+ siteVariables: context.siteVariables,
152
+ scan: context.scan,
153
+ assetFiles: context.assetFiles,
154
+ isWatchMode: context.isWatchMode,
155
+ traceCache: context.traceCache,
156
+ traceToolAvailability: context.traceToolAvailability,
157
+ skipLiterateJavaExecution: !context.traceToolAvailability.java,
158
+ });
159
+ if (record.outputs.size > 0) {
160
+ records.set(filePath, record);
161
+ }
162
+ } catch (error) {
163
+ const { message } = diagnosticFromError(error);
164
+ // Every page error names its file.
165
+ const relPath = toPosix(path.relative(getProjectDir(), filePath));
166
+ diagnostics.push({
167
+ message:
168
+ message.includes(filePath) || message.includes(relPath)
169
+ ? message
170
+ : `${filePath}: ${message}`,
171
+ });
172
+ }
173
+ }
174
+ return { records, diagnostics };
175
+ }
176
+
177
+ /** Validates rendered records and assembles the snapshot of a build. */
178
+ export function finishBuild({
179
+ siteVariables,
180
+ scan,
181
+ assetFiles,
182
+ generatedOutputs,
183
+ records,
184
+ diagnostics,
185
+ }: {
186
+ siteVariables: SiteVariables;
187
+ scan: TadaProjectScan;
188
+ assetFiles: string[];
189
+ generatedOutputs: ReadonlyMap<string, OutputContent>;
190
+ records: ReadonlyMap<string, TadaSourceRecord>;
191
+ diagnostics: BuildDiagnostic[];
192
+ }): SiteBuildResult {
193
+ const allDiagnostics = [
194
+ ...diagnostics,
195
+ ...validateProjectConfigLinks(scan.validTargets),
196
+ ...findOutputConflicts(scan, siteVariables, generatedOutputs, records),
197
+ ];
198
+ if (allDiagnostics.length > 0) {
199
+ return { ok: false, diagnostics: allDiagnostics };
200
+ }
201
+ return {
202
+ ok: true,
203
+ snapshot: createSnapshot({
204
+ siteVariables,
205
+ assetFiles,
206
+ generatedOutputs,
207
+ authorsData: config('authors'),
208
+ scan,
209
+ records,
210
+ }),
211
+ };
212
+ }
213
+
214
+ /** Builds every source and generated file in memory. Nothing is written. */
215
+ export async function buildSite({
216
+ siteVariables,
217
+ mode,
218
+ isWatchMode,
219
+ traceCache,
220
+ traceToolAvailability,
221
+ scan = scanProject(siteVariables),
222
+ }: {
223
+ siteVariables: SiteVariables;
224
+ mode: 'development' | 'production';
225
+ isWatchMode: boolean;
226
+ traceCache: TraceCache;
227
+ /** Probed only if omitted and the site has content pages */
228
+ traceToolAvailability?: TraceToolAvailability;
229
+ /** A scan the caller already made with the same site variables */
230
+ scan?: TadaProjectScan;
231
+ }): Promise<SiteBuildResult> {
232
+ const configDiagnostics = validateConfig(scan, siteVariables);
233
+ if (configDiagnostics.length > 0) {
234
+ return { ok: false, diagnostics: configDiagnostics };
235
+ }
236
+
237
+ compileTemplates(siteVariables);
238
+ await ensureHighlighter(siteVariables);
239
+ const assets = await generateSiteAssets(siteVariables, { mode, isWatchMode });
240
+
241
+ const pageCount = [...sourcePaths(scan, 'content', true)].length;
242
+ if (pageCount > 0) {
243
+ log.info`Processing ${pageCount} content ${pageCount === 1 ? 'file' : 'files'}`;
244
+ }
245
+ if (!traceToolAvailability) {
246
+ traceToolAvailability = pageCount > 0 ? checkTraceToolAvailability() : {};
247
+ if (pageCount > 0 && !traceToolAvailability.java) {
248
+ log.warn`javac was not found; literate Java pages will not include execution output`;
249
+ }
250
+ }
251
+ const { records, diagnostics } = renderSources(scan.sources.keys(), {
252
+ siteVariables,
253
+ scan,
254
+ assetFiles: assets.assetFiles,
255
+ isWatchMode,
256
+ traceCache,
257
+ traceToolAvailability,
258
+ });
259
+ return finishBuild({
260
+ siteVariables,
261
+ scan,
262
+ assetFiles: assets.assetFiles,
263
+ generatedOutputs: assets.outputs,
264
+ records,
265
+ diagnostics,
266
+ });
267
+ }