@abreen/tada 1.11.3 → 1.13.2

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 (79) hide show
  1. package/README.md +65 -64
  2. package/bin/tada.ts +61 -66
  3. package/build/build-manifest.ts +14 -3
  4. package/build/bundle.ts +0 -1
  5. package/build/config-files.ts +183 -0
  6. package/build/config-loader.ts +159 -0
  7. package/build/copy.ts +0 -83
  8. package/build/generate-content-assets.ts +75 -160
  9. package/build/globals.ts +48 -89
  10. package/build/log.ts +27 -18
  11. package/build/pagefind.ts +33 -26
  12. package/build/pipeline.ts +98 -79
  13. package/build/reachability.ts +51 -248
  14. package/build/serve.ts +35 -6
  15. package/build/site-variables.ts +20 -11
  16. package/build/source-model.ts +400 -0
  17. package/build/{watch/records.ts → source-records.ts} +111 -49
  18. package/build/template-globals.ts +91 -0
  19. package/build/templates.ts +50 -40
  20. package/build/test-helpers.ts +15 -0
  21. package/build/toc-plugin.ts +5 -1
  22. package/build/types.d.ts +9 -1
  23. package/build/util.ts +8 -0
  24. package/build/utils/code.ts +6 -16
  25. package/build/utils/content-files.ts +57 -126
  26. package/build/utils/final-html.ts +239 -0
  27. package/build/utils/markdown.ts +237 -17
  28. package/build/utils/paths.ts +2 -1
  29. package/build/utils/render.ts +108 -99
  30. package/build/validate-config-links.ts +91 -14
  31. package/build/watch/assets.ts +0 -2
  32. package/build/watch/build-full.ts +14 -15
  33. package/build/watch/build-incremental.ts +15 -8
  34. package/build/watch/compiler-types.d.ts +6 -1
  35. package/build/watch/compiler.ts +27 -15
  36. package/build/watch/index.ts +2 -7
  37. package/build/watch/planner.ts +32 -16
  38. package/build/watch/reload.ts +4 -0
  39. package/build/watch/runtime.ts +29 -45
  40. package/build/watch/snapshot.ts +50 -402
  41. package/build/watch/validation.ts +6 -3
  42. package/build/watch-reachability-state.ts +16 -23
  43. package/build/watch-reload-client.ts +11 -4
  44. package/init/content/index.md +1 -1
  45. package/init/content/labs/02/index.md +61 -0
  46. package/init/content/labs/02/selection_sort.py +23 -0
  47. package/init/content/labs/index.md +4 -2
  48. package/init/content/lectures/01/index.md +1 -1
  49. package/init/content/markdown.md +1 -1
  50. package/package.json +3 -4
  51. package/src/code/index.ts +46 -18
  52. package/src/code.scss +2 -1
  53. package/src/globals.ts +47 -0
  54. package/src/index.ts +1 -0
  55. package/src/navigate/index.ts +2 -5
  56. package/src/navigate/lifecycle.ts +3 -1
  57. package/src/navigate/runtime.ts +6 -5
  58. package/src/page-update/index.ts +20 -16
  59. package/src/search/index.ts +53 -25
  60. package/src/slides/index.ts +467 -0
  61. package/src/slides/style.scss +104 -0
  62. package/src/style.scss +8 -3
  63. package/src/timezone/index.ts +16 -20
  64. package/src/top/index.ts +2 -4
  65. package/src/trace/index.ts +91 -7
  66. package/src/trace/style.scss +1 -1
  67. package/src/util.ts +6 -2
  68. package/templates/_author.html +2 -2
  69. package/templates/_heading.html +1 -1
  70. package/templates/_nav.html +2 -2
  71. package/templates/_theme.scss +3 -3
  72. package/templates/_top.html +14 -14
  73. package/templates/default.html +8 -0
  74. package/types/dev.d.ts +1 -1
  75. package/types/site-variables.d.ts +4 -5
  76. package/watch/fs-commit.ts +63 -10
  77. package/build/apply-base-path-plugin.ts +0 -186
  78. package/build/content-watch.ts +0 -104
  79. package/build/validate-internal-links-plugin.ts +0 -207
package/build/globals.ts CHANGED
@@ -1,91 +1,50 @@
1
- import timezones from '../src/timezone/timezones.json' with { type: 'json' };
2
- import type { SiteVariables } from './types';
3
-
4
- const MONTHS = [
5
- '',
6
- 'January',
7
- 'February',
8
- 'March',
9
- 'April',
10
- 'May',
11
- 'June',
12
- 'July',
13
- 'August',
14
- 'September',
15
- 'October',
16
- 'November',
17
- 'December',
18
- ];
19
-
20
- interface Globals {
21
- isHomePage: boolean;
22
- isoDate: (str: string | null | undefined) => string | null;
23
- readableDate: (date: string | Date | null | undefined) => string;
24
- classNames: (obj: Record<string, unknown>) => string;
25
- cx: (obj: Record<string, unknown>) => string;
26
- renderTimeZoneChooser: () => string;
27
- }
28
-
29
- export default function createGlobals(
30
- pageVariables: Record<string, unknown>,
31
- siteVariables: SiteVariables,
32
- subPath: string,
33
- ): Globals {
34
- const defaultTz = timezones.find(
35
- t => t.value === siteVariables.defaultTimeZone,
36
- )!;
37
- const options = timezones
38
- .map(tz => {
39
- const label =
40
- tz.value === 'UTC' ? tz.label : `${tz.label} (${tz.abbreviation})`;
41
- const selected = tz.value === defaultTz.value ? ' selected' : '';
42
- return `<option value="${tz.value}"${selected}>${label}</option>`;
43
- })
44
- .join('');
45
- const renderTimeZoneChooser = () =>
46
- `<span class="time-zone-label" hidden>Times shown in </span><select class="time-zone" hidden disabled aria-label="Time zone">${options}</select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`;
47
-
48
- return {
49
- isHomePage: subPath === 'index',
50
- isoDate,
51
- readableDate,
52
- classNames,
53
- cx: classNames,
54
- renderTimeZoneChooser,
55
- };
56
- }
57
-
58
- function isoDate(str: string | null | undefined): string | null {
59
- if (str == null || str == '') {
60
- return null;
61
- }
62
- const date = new Date(str);
63
- return date.toISOString().slice(0, 10);
1
+ type Sha256Hasher = {
2
+ update: (buffer: ArrayBuffer) => void;
3
+ digest: (encoding: 'hex') => string;
4
+ };
5
+
6
+ export interface Globals {
7
+ createSha256Hasher: () => Sha256Hasher;
8
+ cwd: () => string;
9
+ getEnv: (name: string) => string | undefined;
10
+ now: () => number;
11
+ pid: () => number;
12
+ readFileArrayBuffer: (filePath: string) => Promise<ArrayBuffer>;
13
+ sleepSync: (delayMs: number) => void;
14
+ stderrWrite: (chunk: string) => void;
15
+ stdoutWrite: (chunk: string) => void;
16
+ toISOString: (timestampMs: number) => string;
64
17
  }
65
18
 
66
- function readableDate(date: string | Date | null | undefined): string {
67
- if (date == null || date == '') {
68
- return '';
69
- }
70
-
71
- if (!(date instanceof Date)) {
72
- date = new Date(date);
73
- }
74
-
75
- const str = date.toISOString();
76
- const year = str.slice(0, 4);
77
- const month = Number(str.slice(5, 7));
78
- const day = Number(str.slice(8, 10));
79
-
80
- return `${MONTHS[month]} ${day}, ${year}`;
81
- }
82
-
83
- export function classNames(obj: Record<string, unknown>): string {
84
- const names: string[] = [];
85
- for (const key in obj) {
86
- if (obj[key]) {
87
- names.push(key);
88
- }
89
- }
90
- return names.join(' ');
91
- }
19
+ export const globals: Globals = {
20
+ createSha256Hasher() {
21
+ return new Bun.CryptoHasher('sha256');
22
+ },
23
+ cwd() {
24
+ return process.cwd();
25
+ },
26
+ getEnv(name) {
27
+ return process.env[name];
28
+ },
29
+ now() {
30
+ return Date.now();
31
+ },
32
+ pid() {
33
+ return process.pid;
34
+ },
35
+ readFileArrayBuffer(filePath) {
36
+ return Bun.file(filePath).arrayBuffer();
37
+ },
38
+ sleepSync(delayMs) {
39
+ Bun.sleepSync(delayMs);
40
+ },
41
+ stderrWrite(chunk) {
42
+ process.stderr.write(chunk);
43
+ },
44
+ stdoutWrite(chunk) {
45
+ process.stdout.write(chunk);
46
+ },
47
+ toISOString(timestampMs) {
48
+ return new Date(timestampMs).toISOString();
49
+ },
50
+ };
package/build/log.ts CHANGED
@@ -1,20 +1,14 @@
1
1
  import { inspect } from 'node:util';
2
2
  import path from 'path';
3
3
  import { Gi, L, Ri, P, Yi, Li } from './colors';
4
+ import { globals, type Globals } from './globals';
4
5
  import type { Logger } from './types';
5
6
  import FLAIR_STRINGS from './flair.json' with { type: 'json' };
6
7
 
7
8
  const LEVELS = ['debug', 'info', 'warn', 'error'] as const;
8
9
 
9
10
  type LogLevel = (typeof LEVELS)[number];
10
-
11
- const ENV_LOG_LEVEL = process.env.TADA_LOG_LEVEL;
12
-
13
- if (ENV_LOG_LEVEL && !LEVELS.includes(ENV_LOG_LEVEL as LogLevel)) {
14
- throw new Error(
15
- `Invalid TADA_LOG_LEVEL "${ENV_LOG_LEVEL}", must be one of: ${LEVELS.join(', ')}`,
16
- );
17
- }
11
+ type LogGlobals = Pick<Globals, 'getEnv' | 'stderrWrite' | 'stdoutWrite'>;
18
12
 
19
13
  function shouldLog(loggerLevel: string, level: string): boolean {
20
14
  return (
@@ -32,20 +26,35 @@ function validateLevel(level: string): void {
32
26
 
33
27
  function print(
34
28
  strings: string[],
29
+ globals: LogGlobals,
35
30
  stream: 'stdout' | 'stderr' = 'stdout',
36
31
  end: string = '\n',
37
32
  ): void {
38
33
  for (const s of strings) {
39
- process[stream].write(s);
34
+ if (stream === 'stderr') {
35
+ globals.stderrWrite(s);
36
+ } else {
37
+ globals.stdoutWrite(s);
38
+ }
39
+ }
40
+ if (stream === 'stderr') {
41
+ globals.stderrWrite(end);
42
+ } else {
43
+ globals.stdoutWrite(end);
40
44
  }
41
- process[stream].write(end);
42
45
  }
43
46
 
44
47
  export function makeLogger(name: string, logLevel: string = 'info'): Logger {
48
+ const envLogLevel = globals.getEnv('TADA_LOG_LEVEL');
49
+ if (envLogLevel && !LEVELS.includes(envLogLevel as LogLevel)) {
50
+ throw new Error(
51
+ `Invalid TADA_LOG_LEVEL "${envLogLevel}", must be one of: ${LEVELS.join(', ')}`,
52
+ );
53
+ }
45
54
  validateLevel(logLevel);
46
55
 
47
- if (ENV_LOG_LEVEL) {
48
- logLevel = ENV_LOG_LEVEL;
56
+ if (envLogLevel) {
57
+ logLevel = envLogLevel;
49
58
  }
50
59
 
51
60
  if (name) {
@@ -74,29 +83,29 @@ export function makeLogger(name: string, logLevel: string = 'info'): Logger {
74
83
  },
75
84
  debug(strings: TemplateStringsArray, ...args: unknown[]) {
76
85
  if (shouldLog(this.minLogLevel, 'debug')) {
77
- print(getArgs('debug', strings, args, L), 'stderr');
86
+ print(getArgs('debug', strings, args, L), globals, 'stderr');
78
87
  }
79
88
  },
80
89
  info(strings: TemplateStringsArray, ...args: unknown[]) {
81
90
  if (shouldLog(this.minLogLevel, 'info')) {
82
- print(getArgs('info', strings, args, Li));
91
+ print(getArgs('info', strings, args, Li), globals);
83
92
  }
84
93
  },
85
94
  warn(strings: TemplateStringsArray, ...args: unknown[]) {
86
95
  if (shouldLog(this.minLogLevel, 'warn')) {
87
- print(getArgs('warn', strings, args, Yi));
96
+ print(getArgs('warn', strings, args, Yi), globals);
88
97
  }
89
98
  },
90
99
  error(strings: TemplateStringsArray, ...args: unknown[]) {
91
100
  if (shouldLog(this.minLogLevel, 'error')) {
92
- print(getArgs('error', strings, args, Ri));
101
+ print(getArgs('error', strings, args, Ri), globals);
93
102
  }
94
103
  },
95
104
  event(strings: TemplateStringsArray, ...args: unknown[]) {
96
- print(getArgs('event', strings, args, Gi));
105
+ print(getArgs('event', strings, args, Gi), globals);
97
106
  },
98
107
  followup(strings: string[]) {
99
- print(strings);
108
+ print(strings, globals);
100
109
  },
101
110
  };
102
111
 
package/build/pagefind.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  normalizeOutputPath,
8
8
  } from './util';
9
9
  import { assertMutoolAvailable, extractPdfPages } from './pdf-text';
10
- import type { SiteVariables } from './types';
10
+ import type { HtmlOutputAnalysis } from './types';
11
11
 
12
12
  const log = makeLogger(import.meta.url);
13
13
  const PAGEFIND_VERBOSE = process.env.TADA_LOG_LEVEL === 'debug';
@@ -89,20 +89,26 @@ interface IndexTargets {
89
89
  }
90
90
 
91
91
  function collectIndexTargets(
92
- htmlAssetsByPath: Map<string, string>,
93
- siteVariables: SiteVariables,
92
+ htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>,
94
93
  pdfSourceByOutputPath: Map<string, string>,
95
94
  ): IndexTargets {
96
- if (htmlAssetsByPath.size === 0) {
95
+ if (htmlAnalysisByPath.size === 0) {
97
96
  return { reachableHtmlPaths: [], reachablePdfPaths: [] };
98
97
  }
99
98
 
100
- return collectReachableSiteAssets({
101
- htmlAssetsByPath,
102
- knownPdfPaths: new Set(pdfSourceByOutputPath.keys()),
103
- rootPath: 'index.html',
104
- basePath: siteVariables.basePath || '/',
105
- });
99
+ const { reachableHtmlPaths, reachableAssetTargets } =
100
+ collectReachableSiteAssets({
101
+ htmlAnalysisByPath,
102
+ knownAssetTargets: new Set(pdfSourceByOutputPath.keys()),
103
+ rootPath: 'index.html',
104
+ });
105
+
106
+ return {
107
+ reachableHtmlPaths,
108
+ reachablePdfPaths: reachableAssetTargets.filter(target =>
109
+ pdfSourceByOutputPath.has(target),
110
+ ),
111
+ };
106
112
  }
107
113
 
108
114
  interface BuildIndexOptions {
@@ -207,28 +213,25 @@ async function buildIndex({
207
213
  }
208
214
 
209
215
  interface RunPagefindOptions {
210
- siteVariables: SiteVariables;
211
216
  distPath: string;
212
217
  htmlAssetsByPath: Map<string, string>;
218
+ htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>;
213
219
  }
214
220
 
215
221
  export async function runPagefind({
216
- siteVariables,
217
222
  distPath,
218
223
  htmlAssetsByPath,
224
+ htmlAnalysisByPath,
219
225
  }: RunPagefindOptions): Promise<void> {
220
226
  const pdfSourceByOutputPath = getPdfSourceByOutputPath();
221
227
  const start = Date.now();
222
228
 
223
229
  log.debug`Finding reachable pages for search index`;
224
230
  const { reachableHtmlPaths, reachablePdfPaths } = collectIndexTargets(
225
- htmlAssetsByPath,
226
- siteVariables,
231
+ htmlAnalysisByPath,
227
232
  pdfSourceByOutputPath,
228
233
  );
229
234
 
230
- const snapshotReadyAt = Date.now();
231
-
232
235
  let noun = reachableHtmlPaths.length === 1 ? 'page' : 'pages';
233
236
  let message = `Building search index for ${reachableHtmlPaths.length} ${noun}`;
234
237
  if (reachablePdfPaths.length > 0) {
@@ -246,27 +249,32 @@ export async function runPagefind({
246
249
  });
247
250
 
248
251
  const finishedAt = Date.now();
249
- log.debug`Search index built in ${finishedAt - snapshotReadyAt}ms (${finishedAt - start}ms total)`;
252
+ log.debug`Search index built in ${finishedAt - start}ms`;
250
253
  }
251
254
 
252
255
  export class WatchPagefindRunner {
253
- private siteVariables: SiteVariables;
254
256
  private watchRunInProgress: boolean;
255
257
  private watchRunQueued: boolean;
256
258
  private distPath: string | null;
257
259
  private htmlCacheByAssetPath: Map<string, string>;
260
+ private htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>;
258
261
 
259
- constructor(siteVariables: SiteVariables) {
260
- this.siteVariables = siteVariables;
262
+ constructor() {
261
263
  this.watchRunInProgress = false;
262
264
  this.watchRunQueued = false;
263
265
  this.distPath = null;
264
266
  this.htmlCacheByAssetPath = new Map();
267
+ this.htmlAnalysisByPath = new Map();
265
268
  }
266
269
 
267
- update(distPath: string, htmlAssetsByPath: Map<string, string>): void {
270
+ update(
271
+ distPath: string,
272
+ htmlAssetsByPath: Map<string, string>,
273
+ htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>,
274
+ ): void {
268
275
  this.distPath = distPath;
269
276
  this.htmlCacheByAssetPath = htmlAssetsByPath;
277
+ this.htmlAnalysisByPath = htmlAnalysisByPath;
270
278
  }
271
279
 
272
280
  run(): void {
@@ -280,6 +288,7 @@ export class WatchPagefindRunner {
280
288
  this.watchRunQueued = false;
281
289
  const distPath = this.distPath!;
282
290
  const htmlAssetsByPath = new Map(this.htmlCacheByAssetPath);
291
+ const htmlAnalysisByPath = new Map(this.htmlAnalysisByPath);
283
292
  const pdfSourceByOutputPath = getPdfSourceByOutputPath();
284
293
  const start = Date.now();
285
294
 
@@ -289,8 +298,7 @@ export class WatchPagefindRunner {
289
298
  let reachablePdfPaths: string[];
290
299
  try {
291
300
  ({ reachableHtmlPaths, reachablePdfPaths } = collectIndexTargets(
292
- htmlAssetsByPath,
293
- this.siteVariables,
301
+ htmlAnalysisByPath,
294
302
  pdfSourceByOutputPath,
295
303
  ));
296
304
  } catch (err) {
@@ -302,7 +310,6 @@ export class WatchPagefindRunner {
302
310
  return;
303
311
  }
304
312
 
305
- const snapshotReadyAt = Date.now();
306
313
  log.debug`Building search index in background`;
307
314
  buildIndex({
308
315
  distPath,
@@ -313,11 +320,11 @@ export class WatchPagefindRunner {
313
320
  })
314
321
  .then(() => {
315
322
  const finishedAt = Date.now();
316
- log.debug`Search index ready after ${finishedAt - snapshotReadyAt}ms (${finishedAt - start}ms total)`;
323
+ log.info`Search index ready after ${finishedAt - start}ms`;
317
324
  })
318
325
  .catch(err => {
319
326
  const failedAt = Date.now();
320
- log.warn`Search index failed after ${failedAt - snapshotReadyAt}ms of indexing (${failedAt - start}ms total): ${err.message}`;
327
+ log.warn`Search index failed after ${failedAt - start}ms: ${err.message}`;
321
328
  })
322
329
  .finally(() => {
323
330
  this.watchRunInProgress = false;
package/build/pipeline.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import { getDevSiteVariables, getProdSiteVariables } from './site-variables';
3
- import { getExtensionToShikiLanguage } from './site-variables';
4
3
  import { compileTemplates } from './templates';
5
- import {
6
- getDistDir,
7
- getProdDistDir,
8
- getContentDir,
9
- getPublicDir,
10
- } from './utils/paths';
4
+ import { getDistDir, getProdDistDir } from './utils/paths';
11
5
  import { isFeatureEnabled } from './features';
12
6
  import { bundle } from './bundle';
13
7
  import { copyFonts } from './generate-fonts';
@@ -15,90 +9,115 @@ import { copyKatexAssets } from './generate-katex-assets';
15
9
  import { generateFavicons } from './generate-favicon';
16
10
  import { generateWebAppManifest } from './generate-web-app-manifest';
17
11
  import { copyPublicFiles, copyContentAssets } from './copy';
18
- import { getProcessedExtensions } from './utils/file-types';
19
12
  import { ContentRenderer } from './generate-content-assets';
20
13
  import { runPagefind } from './pagefind';
21
14
  import { makeLogger, printFlair } from './log';
22
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';
23
19
  import path from 'path';
24
20
 
25
21
  const log = makeLogger(import.meta.url);
26
22
 
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-`),
28
+ );
29
+ }
30
+
31
+ function removeDirIfExists(dir: string): void {
32
+ fs.rmSync(dir, { recursive: true, force: true });
33
+ }
34
+
27
35
  export async function runPipeline(
28
36
  mode: 'development' | 'production',
29
37
  ): Promise<void> {
30
38
  const isDev = mode === 'development';
31
39
  const siteVariables = isDev ? getDevSiteVariables() : getProdSiteVariables();
32
- let distDir: string;
33
- let prodVersion: number;
34
-
35
- if (isDev) {
36
- distDir = getDistDir();
37
- } else {
38
- const prodBase = getProdDistDir();
39
- prodVersion = getNextVersion(prodBase);
40
- distDir = path.join(prodBase, `v${prodVersion}`);
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
+
47
+ try {
48
+ const configDiagnostics = validateConfig(scan);
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
+ }
110
+
111
+ applyCommitPlan({
112
+ kind: 'replace-root',
113
+ stagedPath: distDir,
114
+ targetPath: finalOutputDir,
115
+ });
116
+ published = true;
117
+ printFlair();
118
+ } finally {
119
+ if (!published) {
120
+ removeDirIfExists(distDir);
121
+ }
41
122
  }
42
-
43
- const contentDir = getContentDir();
44
- const publicDir = getPublicDir();
45
-
46
- // Ensure dist/ exists
47
- fs.mkdirSync(distDir, { recursive: true });
48
-
49
- // Phase 1: Setup
50
- compileTemplates(siteVariables);
51
- const contentRenderer = new ContentRenderer(siteVariables);
52
- await contentRenderer.initHighlighter();
53
-
54
- // Phase 2: Bundle + assets (parallel)
55
- const parallelTasks: (Promise<unknown> | void)[] = [
56
- bundle(siteVariables, { mode, distDir }),
57
- copyFonts(distDir),
58
- copyKatexAssets(distDir),
59
- ];
60
-
61
- if (isFeatureEnabled(siteVariables, 'favicon')) {
62
- parallelTasks.push(generateFavicons(siteVariables, distDir));
63
- }
64
-
65
- const results = await Promise.all(parallelTasks);
66
- const assetFiles = results[0] as string[]; // bundle output filenames
67
-
68
- if (isFeatureEnabled(siteVariables, 'favicon')) {
69
- generateWebAppManifest(siteVariables, distDir);
70
- }
71
-
72
- const publicRelPaths = copyPublicFiles(publicDir, distDir);
73
- const processedExtensions = getProcessedExtensions(
74
- Object.keys(getExtensionToShikiLanguage(siteVariables)),
75
- );
76
- copyContentAssets(contentDir, distDir, processedExtensions, publicRelPaths);
77
-
78
- // Phase 3: Content rendering
79
- const { errors, htmlAssetsByPath } = contentRenderer.processContent({
80
- distDir,
81
- assetFiles,
82
- });
83
-
84
- for (const err of errors) {
85
- log.error`${err.message}`;
86
- }
87
- if (errors.length > 0) {
88
- process.exit(1);
89
- }
90
-
91
- // Phase 4: Post-processing
92
- if (isFeatureEnabled(siteVariables, 'search')) {
93
- await runPagefind({ siteVariables, distPath: distDir, htmlAssetsByPath });
94
- }
95
-
96
- // Phase 5: Build manifest (production only)
97
- if (!isDev) {
98
- const manifestPath = path.join(distDir, 'tada.manifest.json');
99
- await generateBuildManifest(distDir, manifestPath, prodVersion!);
100
- log.info`Built dist-prod/v${prodVersion!}/`;
101
- }
102
-
103
- printFlair();
104
123
  }