@abreen/tada 1.17.1 → 1.18.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.
Files changed (101) hide show
  1. package/README.md +37 -6
  2. package/bin/tada.ts +16 -7
  3. package/bin/validators.ts +33 -0
  4. package/build/branding.ts +41 -0
  5. package/build/build-manifest.ts +9 -4
  6. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  7. package/build/build-validation.ts +176 -0
  8. package/build/bundle.ts +75 -65
  9. package/build/config-loader.ts +3 -3
  10. package/build/custom-fonts.ts +8 -5
  11. package/build/generate-favicon.ts +7 -6
  12. package/build/generate-fonts.ts +8 -15
  13. package/build/generate-katex-assets.ts +19 -17
  14. package/build/generate-web-app-manifest.ts +3 -11
  15. package/build/lodash-template.ts +14 -0
  16. package/build/output-publication.ts +238 -0
  17. package/build/pagefind.ts +49 -21
  18. package/build/pdf-text.ts +41 -1
  19. package/build/pipeline.ts +95 -101
  20. package/build/site-assets.ts +46 -0
  21. package/build/site-build.ts +267 -0
  22. package/build/source-model.ts +223 -244
  23. package/build/source-records.ts +69 -81
  24. package/build/template-globals.ts +3 -0
  25. package/build/templates.ts +5 -2
  26. package/build/toc-plugin.ts +21 -21
  27. package/build/types.d.ts +38 -38
  28. package/build/util.ts +0 -4
  29. package/build/utils/code.ts +38 -101
  30. package/build/utils/final-html.ts +171 -47
  31. package/build/utils/literate-java.ts +11 -7
  32. package/build/utils/markdown-partials.ts +4 -3
  33. package/build/utils/markdown.ts +6 -17
  34. package/build/utils/paths.ts +3 -0
  35. package/build/utils/plain-text.ts +18 -0
  36. package/build/utils/render.ts +197 -154
  37. package/build/utils/shiki-highlighter.ts +11 -0
  38. package/build/utils/source-template.ts +2 -2
  39. package/build/utils/trace-core.ts +8 -83
  40. package/build/utils/trace.ts +38 -95
  41. package/build/validate-config-links.ts +5 -5
  42. package/build/watch/build-incremental.ts +46 -103
  43. package/build/watch/compiler.ts +158 -37
  44. package/build/watch/engine.ts +207 -176
  45. package/build/watch/index.ts +9 -2
  46. package/build/watch/planner.ts +88 -144
  47. package/build/watch/runtime.ts +21 -34
  48. package/build/watch/types.d.ts +32 -64
  49. package/package.json +4 -4
  50. package/schema/site.schema.json +18 -2
  51. package/src/_content.scss +447 -0
  52. package/src/_fonts.scss +90 -0
  53. package/src/_layout.scss +50 -35
  54. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  55. package/src/code/{style.scss → _index.scss} +61 -0
  56. package/src/header/{_base.scss → _index.scss} +98 -6
  57. package/src/index.ts +1 -17
  58. package/src/navigate/{style.scss → _index.scss} +20 -0
  59. package/src/navigate/eligible.ts +0 -40
  60. package/src/navigate/index.ts +38 -4
  61. package/src/navigate/lifecycle.ts +19 -2
  62. package/src/navigate/runtime.ts +20 -4
  63. package/src/page-update/index.ts +33 -20
  64. package/src/print/{style.scss → _index.scss} +3 -1
  65. package/src/search/index.ts +7 -0
  66. package/src/slides/{style.scss → _index.scss} +5 -0
  67. package/src/style.scss +24 -536
  68. package/src/toc/index.ts +2 -2
  69. package/templates/_author.html +2 -2
  70. package/templates/_download.html +1 -1
  71. package/templates/_footer.html +1 -1
  72. package/templates/_heading.html +3 -3
  73. package/templates/_nav.html +2 -2
  74. package/templates/_page-bottom.html +6 -0
  75. package/templates/_top.html +18 -14
  76. package/templates/code.html +22 -21
  77. package/templates/default.html +12 -11
  78. package/templates/literate.html +6 -5
  79. package/build/copy.ts +0 -82
  80. package/build/generate-content-assets.ts +0 -121
  81. package/build/utils/content-files.ts +0 -170
  82. package/build/watch/assets.ts +0 -103
  83. package/build/watch/build-full.ts +0 -97
  84. package/build/watch/build-helpers.ts +0 -66
  85. package/build/watch/build-result.ts +0 -19
  86. package/build/watch/fs-commit.ts +0 -218
  87. package/build/watch/mutations.ts +0 -73
  88. package/build/watch/snapshot.ts +0 -201
  89. package/build/watch/validation.ts +0 -57
  90. package/src/code.scss +0 -60
  91. package/src/critical.scss +0 -5
  92. package/src/header/style.scss +0 -91
  93. /package/src/{literate.scss → _literate.scss} +0 -0
  94. /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
  95. /package/src/anchor/{style.scss → _index.scss} +0 -0
  96. /package/src/page-update/{style.scss → _index.scss} +0 -0
  97. /package/src/question/{style.scss → _index.scss} +0 -0
  98. /package/src/search/{style.scss → _index.scss} +0 -0
  99. /package/src/timezone/{style.scss → _index.scss} +0 -0
  100. /package/src/toc/{style.scss → _index.scss} +0 -0
  101. /package/src/trace/{style.scss → _index.scss} +0 -0
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
+ }