@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
@@ -1,97 +0,0 @@
1
- import { compileTemplates, config } from '../templates';
2
- import { getDevSiteVariables } from '../site-variables';
3
- import { getDistDir } from '../util';
4
- import type { CompilerBuildResult } from './types';
5
- import { createSnapshot, type TadaSourceRecord } from './snapshot';
6
- import { scanProject } from '../source-model';
7
- import type { TraceCache, WatchTraceOptions } from './compiler-types';
8
- import {
9
- bundleWatchAssets,
10
- ensureHighlighter,
11
- makeTempBuildDir,
12
- populateStaticAssets,
13
- removeDirIfExists,
14
- } from './assets';
15
- import { validateConfig, validateProjectConfigLinks } from './validation';
16
- import { buildFailedFromError, buildSucceeded } from './build-result';
17
- import {
18
- buildFailedWithDiagnostics,
19
- renderContentRecord,
20
- renderPublicRecord,
21
- } from './build-helpers';
22
-
23
- export async function buildFull({
24
- traceCache,
25
- traceOptions,
26
- }: {
27
- traceCache: TraceCache;
28
- traceOptions: WatchTraceOptions;
29
- }): Promise<CompilerBuildResult> {
30
- const distDir = getDistDir();
31
- const outputDir = makeTempBuildDir(distDir);
32
- try {
33
- const siteVariables = getDevSiteVariables();
34
- const scan = scanProject(siteVariables);
35
- const configDiagnostics = validateConfig(scan, siteVariables);
36
- if (configDiagnostics.length > 0) {
37
- return buildFailedWithDiagnostics(outputDir, configDiagnostics);
38
- }
39
-
40
- compileTemplates(siteVariables);
41
- await ensureHighlighter(siteVariables);
42
-
43
- const assetFiles = await bundleWatchAssets(outputDir, siteVariables);
44
- await populateStaticAssets(outputDir, siteVariables);
45
-
46
- const contentRecords = new Map<string, TadaSourceRecord>();
47
- for (const filePath of scan.contentFiles) {
48
- const record = renderContentRecord({
49
- filePath,
50
- siteVariables,
51
- scan,
52
- assetFiles,
53
- outputDir,
54
- traceCache,
55
- traceOptions,
56
- cachedTraceSourceDir: distDir,
57
- });
58
- if (record) {
59
- contentRecords.set(filePath, record);
60
- }
61
- }
62
-
63
- const publicRecords = new Map<string, TadaSourceRecord>();
64
- for (const filePath of scan.publicFiles) {
65
- const record = renderPublicRecord({
66
- filePath,
67
- publicDir: scan.publicDir,
68
- outputDir,
69
- });
70
- publicRecords.set(filePath, record);
71
- }
72
-
73
- const linkDiagnostics = validateProjectConfigLinks(scan.validTargets);
74
- if (linkDiagnostics.length > 0) {
75
- return buildFailedWithDiagnostics(outputDir, linkDiagnostics);
76
- }
77
-
78
- const nextSnapshot = createSnapshot({
79
- siteVariables,
80
- assetFiles,
81
- navData: config('nav'),
82
- authorsData: config('authors'),
83
- scan,
84
- contentRecords,
85
- publicRecords,
86
- });
87
-
88
- return buildSucceeded(nextSnapshot, {
89
- kind: 'replace-root',
90
- stagedPath: outputDir,
91
- targetPath: distDir,
92
- });
93
- } catch (error) {
94
- removeDirIfExists(outputDir);
95
- return buildFailedFromError(error);
96
- }
97
- }
@@ -1,66 +0,0 @@
1
- import { createContentRecord, createPublicRecord } from '../source-records';
2
- import type { SiteVariables } from '../types';
3
- import type { TadaProjectScan, TadaSourceRecord } from './snapshot';
4
- import type { CompilerBuildResult, WatchDiagnostic } from './types';
5
- import type { TraceCache, WatchTraceOptions } from './compiler-types';
6
- import { removeDirIfExists, writeAssets } from './assets';
7
-
8
- export function buildFailedWithDiagnostics(
9
- outputDir: string,
10
- diagnostics: WatchDiagnostic[],
11
- ): CompilerBuildResult {
12
- removeDirIfExists(outputDir);
13
- return { ok: false, diagnostics };
14
- }
15
-
16
- export function renderContentRecord({
17
- filePath,
18
- siteVariables,
19
- scan,
20
- assetFiles,
21
- outputDir,
22
- traceCache,
23
- traceOptions,
24
- cachedTraceSourceDir,
25
- }: {
26
- filePath: string;
27
- siteVariables: SiteVariables;
28
- scan: TadaProjectScan;
29
- assetFiles: string[];
30
- outputDir: string;
31
- traceCache: TraceCache;
32
- traceOptions: WatchTraceOptions;
33
- cachedTraceSourceDir: string;
34
- }): TadaSourceRecord | undefined {
35
- const record = createContentRecord({
36
- filePath,
37
- siteVariables,
38
- scan,
39
- assetFiles,
40
- outputDir,
41
- traceCache,
42
- traceToolAvailability: traceOptions.toolAvailability,
43
- cachedTraceSourceDir,
44
- });
45
- if (record.outputs.size === 0) {
46
- return undefined;
47
- }
48
- writeAssets(outputDir, record.outputs);
49
- return record;
50
- }
51
-
52
- export function renderPublicRecord({
53
- filePath,
54
- publicDir,
55
- outputDir,
56
- }: {
57
- filePath: string;
58
- publicDir: string;
59
- outputDir?: string;
60
- }): TadaSourceRecord {
61
- const record = createPublicRecord(filePath, publicDir);
62
- if (outputDir) {
63
- writeAssets(outputDir, record.outputs);
64
- }
65
- return record;
66
- }
@@ -1,19 +0,0 @@
1
- import type { CommitPlan, CompilerBuildResult } from './types';
2
- import { createBuildMeta, type TadaSnapshot } from './snapshot';
3
- import { diagnosticsFromMessages } from './validation';
4
-
5
- export function buildSucceeded(
6
- snapshot: TadaSnapshot,
7
- commit: CommitPlan,
8
- ): CompilerBuildResult {
9
- return { ok: true, snapshot, commit, meta: createBuildMeta(snapshot) };
10
- }
11
-
12
- export function buildFailedFromError(error: unknown): CompilerBuildResult {
13
- return {
14
- ok: false,
15
- diagnostics: diagnosticsFromMessages([
16
- error instanceof Error ? error.message : String(error),
17
- ]),
18
- };
19
- }
@@ -1,218 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { globals, type Globals } from '../globals';
4
- import type {
5
- ApplyMutationsCommitPlan,
6
- CommitPlan,
7
- ReplaceRootCommitPlan,
8
- } from './types';
9
-
10
- type CommitPlanGlobals = Pick<Globals, 'now' | 'pid' | 'sleepSync'>;
11
-
12
- const TRANSIENT_RENAME_ERROR_CODES = new Set([
13
- 'EACCES',
14
- 'EBUSY',
15
- 'ENOTEMPTY',
16
- 'EPERM',
17
- ]);
18
- const RENAME_RETRY_DELAY_MS = 25;
19
- const RENAME_RETRY_COUNT = 8;
20
-
21
- function removeDirIfExists(dir: string): void {
22
- fs.rmSync(dir, { recursive: true, force: true });
23
- }
24
-
25
- function ensureParentDir(filePath: string): void {
26
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
- }
28
-
29
- function isTransientRenameError(
30
- error: unknown,
31
- sourcePath: string,
32
- ): error is NodeJS.ErrnoException {
33
- const code = (error as NodeJS.ErrnoException | undefined)?.code;
34
- return (
35
- typeof code === 'string' &&
36
- TRANSIENT_RENAME_ERROR_CODES.has(code) &&
37
- fs.existsSync(sourcePath)
38
- );
39
- }
40
-
41
- function renameWithRetry(
42
- sourcePath: string,
43
- targetPath: string,
44
- globals: CommitPlanGlobals,
45
- ): void {
46
- for (let attempt = 0; attempt <= RENAME_RETRY_COUNT; attempt++) {
47
- try {
48
- fs.renameSync(sourcePath, targetPath);
49
- return;
50
- } catch (error) {
51
- if (
52
- attempt === RENAME_RETRY_COUNT ||
53
- !isTransientRenameError(error, sourcePath)
54
- ) {
55
- throw error;
56
- }
57
- globals.sleepSync(RENAME_RETRY_DELAY_MS);
58
- }
59
- }
60
- }
61
-
62
- function pruneEmptyDirs(rootDir: string, filePath: string): void {
63
- let currentDir = path.dirname(filePath);
64
- const root = path.resolve(rootDir);
65
- while (currentDir !== root) {
66
- try {
67
- fs.rmdirSync(currentDir);
68
- } catch (err) {
69
- const code = (err as NodeJS.ErrnoException).code;
70
- if (code === 'ENOTEMPTY' || code === 'ENOENT') {
71
- break;
72
- }
73
- throw err;
74
- }
75
- currentDir = path.dirname(currentDir);
76
- }
77
- }
78
-
79
- interface MutationJournalEntry {
80
- targetPath: string;
81
- backupPath?: string;
82
- }
83
-
84
- function applyMutations(
85
- plan: ApplyMutationsCommitPlan,
86
- globals: CommitPlanGlobals,
87
- ): void {
88
- const transactionRoot = fs.mkdtempSync(
89
- path.join(plan.rootDir, '.watch-stage-'),
90
- );
91
- const journal: MutationJournalEntry[] = [];
92
- const createdDirs: string[] = [];
93
- try {
94
- // Index staging paths so file/directory transitions cannot collide in staging.
95
- for (const [index, mutation] of plan.mutations.entries()) {
96
- if (mutation.kind === 'write') {
97
- fs.writeFileSync(
98
- path.join(transactionRoot, `write-${index}`),
99
- mutation.content,
100
- );
101
- }
102
- }
103
-
104
- for (const [index, mutation] of plan.mutations.entries()) {
105
- const targetPath = path.resolve(plan.rootDir, mutation.path);
106
- const existing = fs.lstatSync(targetPath, { throwIfNoEntry: false });
107
- if (existing?.isDirectory()) {
108
- throw new Error(
109
- `Cannot publish file mutation over directory: ${targetPath}`,
110
- );
111
- }
112
- const backupPath = existing
113
- ? path.join(transactionRoot, `backup-${index}`)
114
- : undefined;
115
- if (backupPath) {
116
- renameWithRetry(targetPath, backupPath, globals);
117
- }
118
- journal.push({ targetPath, backupPath });
119
- if (mutation.kind === 'write') {
120
- // Track missing parents without relying on mkdir's returned path spelling.
121
- let dir = path.dirname(targetPath);
122
- while (!fs.existsSync(dir)) {
123
- createdDirs.push(dir);
124
- const parent = path.dirname(dir);
125
- if (parent === dir) {
126
- break;
127
- }
128
- dir = parent;
129
- }
130
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
131
- renameWithRetry(
132
- path.join(transactionRoot, `write-${index}`),
133
- targetPath,
134
- globals,
135
- );
136
- }
137
- }
138
- for (const mutation of plan.mutations) {
139
- if (mutation.kind === 'delete') {
140
- pruneEmptyDirs(plan.rootDir, path.resolve(plan.rootDir, mutation.path));
141
- }
142
- }
143
- } catch (publicationError) {
144
- const rollbackErrors: unknown[] = [];
145
- const removeCreatedDirs = () => {
146
- for (const dir of [...new Set(createdDirs)].sort(
147
- (a, b) => b.length - a.length,
148
- )) {
149
- try {
150
- fs.rmdirSync(dir);
151
- } catch (error) {
152
- const code = (error as NodeJS.ErrnoException).code;
153
- if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'ENOTDIR') {
154
- rollbackErrors.push(error);
155
- }
156
- }
157
- }
158
- };
159
- for (const entry of journal.toReversed()) {
160
- try {
161
- removeCreatedDirs();
162
- fs.rmSync(entry.targetPath, { force: true });
163
- if (entry.backupPath) {
164
- ensureParentDir(entry.targetPath);
165
- renameWithRetry(entry.backupPath, entry.targetPath, globals);
166
- }
167
- } catch (error) {
168
- rollbackErrors.push(error);
169
- }
170
- }
171
- removeCreatedDirs();
172
- if (rollbackErrors.length > 0) {
173
- // Keep any unrestored originals available for manual recovery.
174
- throw new AggregateError(
175
- [publicationError, ...rollbackErrors],
176
- `Failed to publish and restore output; recovery files remain in ${transactionRoot}`,
177
- { cause: publicationError },
178
- );
179
- }
180
- removeDirIfExists(transactionRoot);
181
- throw publicationError;
182
- }
183
- removeDirIfExists(transactionRoot);
184
- }
185
-
186
- function replaceRoot(
187
- plan: ReplaceRootCommitPlan,
188
- globals: CommitPlanGlobals,
189
- ): void {
190
- const { targetPath, stagedPath } = plan;
191
- const backupDir = `${targetPath}.bak-${globals.pid()}-${globals.now()}`;
192
- const targetExists = fs.existsSync(targetPath);
193
-
194
- try {
195
- if (targetExists) {
196
- renameWithRetry(targetPath, backupDir, globals);
197
- }
198
- renameWithRetry(stagedPath, targetPath, globals);
199
- if (targetExists) {
200
- removeDirIfExists(backupDir);
201
- }
202
- } catch (err) {
203
- if (!fs.existsSync(targetPath) && fs.existsSync(backupDir)) {
204
- renameWithRetry(backupDir, targetPath, globals);
205
- }
206
- throw err;
207
- }
208
- }
209
-
210
- export function applyCommitPlan(plan: CommitPlan): void {
211
- const runtimeGlobals: CommitPlanGlobals = globals;
212
- if (plan.kind === 'replace-root') {
213
- replaceRoot(plan, runtimeGlobals);
214
- return;
215
- }
216
-
217
- applyMutations(plan, runtimeGlobals);
218
- }
@@ -1,73 +0,0 @@
1
- import type { FileMutation } from './types';
2
- import type { TadaOutputOwner, TadaSnapshot } from './snapshot';
3
-
4
- function outputsEqual(
5
- left: string | Buffer | undefined,
6
- right: string | Buffer | undefined,
7
- ): boolean {
8
- if (left === undefined || right === undefined) {
9
- return left === right;
10
- }
11
- if (typeof left === 'string' && typeof right === 'string') {
12
- return left === right;
13
- }
14
- const leftBuffer = typeof left === 'string' ? Buffer.from(left) : left;
15
- const rightBuffer = typeof right === 'string' ? Buffer.from(right) : right;
16
- return Buffer.compare(leftBuffer, rightBuffer) === 0;
17
- }
18
-
19
- function getOutputContent(
20
- snapshot: TadaSnapshot,
21
- owner: TadaOutputOwner | undefined,
22
- outputPath: string,
23
- ): string | Buffer | undefined {
24
- if (!owner) {
25
- return undefined;
26
- }
27
- const records =
28
- owner.kind === 'content' ? snapshot.contentRecords : snapshot.publicRecords;
29
- return records.get(owner.sourcePath)?.outputs.get(outputPath);
30
- }
31
-
32
- export function computeMutations(
33
- previous: TadaSnapshot,
34
- next: TadaSnapshot,
35
- forceSourcePaths: Set<string> = new Set(),
36
- ): FileMutation[] {
37
- const mutations: FileMutation[] = [];
38
- const allPaths = new Set([
39
- ...previous.outputOwners.keys(),
40
- ...next.outputOwners.keys(),
41
- ]);
42
-
43
- for (const outputPath of [...allPaths].sort()) {
44
- const previousOwner = previous.outputOwners.get(outputPath);
45
- const nextOwner = next.outputOwners.get(outputPath);
46
-
47
- const previousContent = getOutputContent(
48
- previous,
49
- previousOwner,
50
- outputPath,
51
- );
52
- const nextContent = getOutputContent(next, nextOwner, outputPath);
53
-
54
- if (!nextOwner) {
55
- mutations.push({ path: outputPath, kind: 'delete' });
56
- continue;
57
- }
58
-
59
- if (
60
- !previousOwner ||
61
- !outputsEqual(previousContent, nextContent) ||
62
- forceSourcePaths.has(nextOwner.sourcePath)
63
- ) {
64
- mutations.push({
65
- path: outputPath,
66
- kind: 'write',
67
- content: nextContent!,
68
- });
69
- }
70
- }
71
-
72
- return mutations;
73
- }
@@ -1,201 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import {
4
- type TadaProjectScan,
5
- assertNoOutputPathConflicts,
6
- } from '../source-model';
7
- import type { Asset, HtmlOutputAnalysis, SiteVariables } from '../types';
8
- import type { TadaBuildMeta } from './compiler-types';
9
-
10
- export interface TadaSourceRecord {
11
- sourcePath: string;
12
- kind: 'content' | 'public';
13
- outputs: Map<string, string | Buffer>;
14
- htmlAnalysisByOutputPath?: Map<string, HtmlOutputAnalysis>;
15
- partialDeps: Set<string>;
16
- traceDeps: Set<string>;
17
- internalTargets: Set<string>;
18
- generatedOutputPaths: Set<string>;
19
- authorKey?: string;
20
- }
21
-
22
- export interface TadaOutputOwner {
23
- kind: 'content' | 'public';
24
- sourcePath: string;
25
- }
26
-
27
- export interface TadaSnapshot {
28
- siteVariables: SiteVariables;
29
- assetFiles: string[];
30
- navData: unknown;
31
- authorsData: unknown;
32
- contentRecords: Map<string, TadaSourceRecord>;
33
- publicRecords: Map<string, TadaSourceRecord>;
34
- outputOwners: Map<string, TadaOutputOwner>;
35
- reversePartialDeps: Map<string, Set<string>>;
36
- reverseTraceDeps: Map<string, Set<string>>;
37
- reverseInternalTargetDeps: Map<string, Set<string>>;
38
- reverseAuthorDeps: Map<string, Set<string>>;
39
- scan: TadaProjectScan;
40
- }
41
-
42
- function buildReverseMap(
43
- records: Iterable<TadaSourceRecord>,
44
- selector: (record: TadaSourceRecord) => Iterable<string>,
45
- ): Map<string, Set<string>> {
46
- const reverse = new Map<string, Set<string>>();
47
-
48
- for (const record of records) {
49
- for (const key of selector(record)) {
50
- if (!reverse.has(key)) {
51
- reverse.set(key, new Set());
52
- }
53
- reverse.get(key)!.add(record.sourcePath);
54
- }
55
- }
56
-
57
- return reverse;
58
- }
59
-
60
- function collectOutputOwners(snapshot: {
61
- contentRecords: Map<string, TadaSourceRecord>;
62
- publicRecords: Map<string, TadaSourceRecord>;
63
- }): Map<string, TadaOutputOwner> {
64
- const owners = new Map<string, TadaOutputOwner>();
65
-
66
- for (const record of snapshot.contentRecords.values()) {
67
- for (const outputPath of record.outputs.keys()) {
68
- owners.set(outputPath, {
69
- kind: 'content',
70
- sourcePath: record.sourcePath,
71
- });
72
- }
73
- }
74
-
75
- for (const record of snapshot.publicRecords.values()) {
76
- for (const outputPath of record.outputs.keys()) {
77
- owners.set(outputPath, { kind: 'public', sourcePath: record.sourcePath });
78
- }
79
- }
80
-
81
- return owners;
82
- }
83
-
84
- export function createSnapshot({
85
- siteVariables,
86
- assetFiles,
87
- navData,
88
- authorsData,
89
- scan,
90
- contentRecords,
91
- publicRecords,
92
- }: {
93
- siteVariables: SiteVariables;
94
- assetFiles: string[];
95
- navData: unknown;
96
- authorsData: unknown;
97
- scan: TadaProjectScan;
98
- contentRecords: Map<string, TadaSourceRecord>;
99
- publicRecords: Map<string, TadaSourceRecord>;
100
- }): TadaSnapshot {
101
- const allRecords = [...contentRecords.values()];
102
-
103
- return {
104
- siteVariables,
105
- assetFiles,
106
- navData,
107
- authorsData,
108
- contentRecords,
109
- publicRecords,
110
- outputOwners: collectOutputOwners({ contentRecords, publicRecords }),
111
- reversePartialDeps: buildReverseMap(
112
- allRecords,
113
- record => record.partialDeps,
114
- ),
115
- reverseTraceDeps: buildReverseMap(allRecords, record => record.traceDeps),
116
- reverseInternalTargetDeps: buildReverseMap(
117
- allRecords,
118
- record => record.internalTargets,
119
- ),
120
- reverseAuthorDeps: buildReverseMap(allRecords, record =>
121
- record.authorKey ? [record.authorKey] : [],
122
- ),
123
- scan,
124
- };
125
- }
126
-
127
- export function collectSourceOutputs(
128
- assets: Asset[],
129
- generatedOutputPaths: Set<string>,
130
- stageDir: string,
131
- ): Map<string, string | Buffer> {
132
- const outputs = new Map<string, string | Buffer>();
133
- for (const asset of assets) {
134
- outputs.set(asset.assetPath, asset.content);
135
- }
136
- for (const outputPath of generatedOutputPaths) {
137
- outputs.set(outputPath, fs.readFileSync(path.join(stageDir, outputPath)));
138
- }
139
- return outputs;
140
- }
141
-
142
- function cloneHtmlOutputAnalysis(
143
- analysis: HtmlOutputAnalysis,
144
- ): HtmlOutputAnalysis {
145
- return { outgoingTargets: new Set(analysis.outgoingTargets) };
146
- }
147
-
148
- export function collectSourceHtmlAnalysis(
149
- assets: Asset[],
150
- ): Map<string, HtmlOutputAnalysis> {
151
- const htmlAnalysisByOutputPath = new Map<string, HtmlOutputAnalysis>();
152
- for (const asset of assets) {
153
- if (!asset.assetPath.endsWith('.html') || !asset.htmlAnalysis) {
154
- continue;
155
- }
156
- htmlAnalysisByOutputPath.set(
157
- asset.assetPath,
158
- cloneHtmlOutputAnalysis(asset.htmlAnalysis),
159
- );
160
- }
161
- return htmlAnalysisByOutputPath;
162
- }
163
-
164
- function collectHtmlAssetsByPath(
165
- contentRecords: Map<string, TadaSourceRecord>,
166
- ): Map<string, string> {
167
- const htmlAssetsByPath = new Map<string, string>();
168
- for (const record of contentRecords.values()) {
169
- for (const [outputPath, content] of record.outputs) {
170
- if (outputPath.endsWith('.html') && typeof content === 'string') {
171
- htmlAssetsByPath.set(outputPath, content);
172
- }
173
- }
174
- }
175
- return htmlAssetsByPath;
176
- }
177
-
178
- function collectHtmlAnalysisByPath(
179
- contentRecords: Map<string, TadaSourceRecord>,
180
- ): Map<string, HtmlOutputAnalysis> {
181
- const htmlAnalysisByPath = new Map<string, HtmlOutputAnalysis>();
182
- for (const record of contentRecords.values()) {
183
- if (!record.htmlAnalysisByOutputPath) {
184
- continue;
185
- }
186
- for (const [outputPath, analysis] of record.htmlAnalysisByOutputPath) {
187
- htmlAnalysisByPath.set(outputPath, cloneHtmlOutputAnalysis(analysis));
188
- }
189
- }
190
- return htmlAnalysisByPath;
191
- }
192
-
193
- export function createBuildMeta(snapshot: TadaSnapshot): TadaBuildMeta {
194
- return {
195
- htmlAssetsByPath: collectHtmlAssetsByPath(snapshot.contentRecords),
196
- htmlAnalysisByPath: collectHtmlAnalysisByPath(snapshot.contentRecords),
197
- siteVariables: snapshot.siteVariables,
198
- };
199
- }
200
-
201
- export { assertNoOutputPathConflicts, type TadaProjectScan };