@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.
- package/README.md +37 -6
- package/bin/tada.ts +16 -7
- package/bin/validators.ts +33 -0
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +9 -4
- package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
- package/build/build-validation.ts +176 -0
- package/build/bundle.ts +75 -65
- package/build/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- package/build/generate-favicon.ts +7 -6
- package/build/generate-fonts.ts +8 -15
- package/build/generate-katex-assets.ts +19 -17
- package/build/generate-web-app-manifest.ts +3 -11
- package/build/lodash-template.ts +14 -0
- package/build/output-publication.ts +238 -0
- package/build/pagefind.ts +49 -21
- package/build/pdf-text.ts +41 -1
- package/build/pipeline.ts +95 -101
- package/build/site-assets.ts +46 -0
- package/build/site-build.ts +267 -0
- package/build/source-model.ts +223 -244
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +5 -2
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +38 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +38 -101
- package/build/utils/final-html.ts +171 -47
- package/build/utils/literate-java.ts +11 -7
- package/build/utils/markdown-partials.ts +4 -3
- package/build/utils/markdown.ts +6 -17
- package/build/utils/paths.ts +3 -0
- package/build/utils/plain-text.ts +18 -0
- package/build/utils/render.ts +197 -154
- package/build/utils/shiki-highlighter.ts +11 -0
- package/build/utils/source-template.ts +2 -2
- package/build/utils/trace-core.ts +8 -83
- package/build/utils/trace.ts +38 -95
- package/build/validate-config-links.ts +5 -5
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -176
- package/build/watch/index.ts +9 -2
- package/build/watch/planner.ts +88 -144
- package/build/watch/runtime.ts +21 -34
- package/build/watch/types.d.ts +32 -64
- package/package.json +4 -4
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +447 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/appearance-picker/{style.scss → _index.scss} +1 -1
- package/src/code/{style.scss → _index.scss} +61 -0
- package/src/header/{_base.scss → _index.scss} +98 -6
- package/src/index.ts +1 -17
- package/src/navigate/{style.scss → _index.scss} +20 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +38 -4
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +20 -4
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +7 -0
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/toc/index.ts +2 -2
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +3 -3
- package/templates/_nav.html +2 -2
- package/templates/_page-bottom.html +6 -0
- package/templates/_top.html +18 -14
- package/templates/code.html +22 -21
- package/templates/default.html +12 -11
- package/templates/literate.html +6 -5
- package/build/copy.ts +0 -82
- package/build/generate-content-assets.ts +0 -121
- package/build/utils/content-files.ts +0 -170
- package/build/watch/assets.ts +0 -103
- package/build/watch/build-full.ts +0 -97
- package/build/watch/build-helpers.ts +0 -66
- package/build/watch/build-result.ts +0 -19
- package/build/watch/fs-commit.ts +0 -218
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -57
- package/src/code.scss +0 -60
- package/src/critical.scss +0 -5
- package/src/header/style.scss +0 -91
- /package/src/{literate.scss → _literate.scss} +0 -0
- /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
- /package/src/anchor/{style.scss → _index.scss} +0 -0
- /package/src/page-update/{style.scss → _index.scss} +0 -0
- /package/src/question/{style.scss → _index.scss} +0 -0
- /package/src/search/{style.scss → _index.scss} +0 -0
- /package/src/timezone/{style.scss → _index.scss} +0 -0
- /package/src/toc/{style.scss → _index.scss} +0 -0
- /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 {
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|