@abreen/tada 1.1.0 → 1.3.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 (94) hide show
  1. package/README.md +69 -17
  2. package/bin/tada.ts +317 -25
  3. package/bin/validators.test.ts +4 -3
  4. package/bin/validators.ts +4 -2
  5. package/build/apply-base-path-plugin.ts +24 -13
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +19 -20
  9. package/build/code.test.ts +7 -3
  10. package/build/content-watch.ts +6 -6
  11. package/build/copy.ts +2 -2
  12. package/build/deflist-id-plugin.ts +1 -1
  13. package/build/external-links-plugin.ts +2 -2
  14. package/build/features.ts +1 -1
  15. package/build/generate-content-assets.ts +12 -7
  16. package/build/generate-favicon.ts +13 -6
  17. package/build/generate-fonts.ts +2 -2
  18. package/build/generate-katex-assets.ts +35 -0
  19. package/build/generate-manifest.ts +2 -2
  20. package/build/globals.test.ts +10 -11
  21. package/build/globals.ts +14 -7
  22. package/build/json-schema.test.ts +1 -1
  23. package/build/log.test.ts +1 -1
  24. package/build/log.ts +2 -2
  25. package/build/markdown-plugins.test.ts +116 -10
  26. package/build/pagefind.test.ts +2 -2
  27. package/build/pagefind.ts +5 -5
  28. package/build/pdf-text.test.ts +52 -0
  29. package/build/pipeline.ts +42 -15
  30. package/build/reachability.test.ts +1 -1
  31. package/build/serve.ts +20 -8
  32. package/build/site-variables.ts +13 -5
  33. package/build/templates.ts +16 -7
  34. package/build/toc-plugin.test.ts +2 -2
  35. package/build/toc-plugin.ts +1 -1
  36. package/build/types.ts +15 -6
  37. package/build/util.ts +6 -6
  38. package/build/utils/code.ts +5 -5
  39. package/build/utils/content-files.ts +3 -3
  40. package/build/utils/derive-theme.test.ts +1 -1
  41. package/build/utils/derive-theme.ts +1 -1
  42. package/build/utils/file-types.test.ts +1 -1
  43. package/build/utils/front-matter.test.ts +1 -4
  44. package/build/utils/front-matter.ts +2 -2
  45. package/build/utils/jdi-runner/LiterateRunner.java +13 -0
  46. package/build/utils/literate-java.ts +24 -12
  47. package/build/utils/markdown.ts +41 -9
  48. package/build/utils/paths.test.ts +2 -2
  49. package/build/utils/paths.ts +5 -1
  50. package/build/utils/render.ts +57 -36
  51. package/build/utils/shiki-highlighter.ts +1 -1
  52. package/build/validate-internal-links-plugin.test.ts +1 -1
  53. package/build/validate-internal-links-plugin.ts +1 -1
  54. package/build/watch-reachability-state.test.ts +2 -2
  55. package/build/watch-reachability-state.ts +1 -1
  56. package/build/watch.ts +91 -110
  57. package/init/content/labs/00/VowelCounter.java.md +129 -0
  58. package/init/content/labs/00/index.md +29 -0
  59. package/init/content/labs/index.md +9 -0
  60. package/{content → init/content}/lectures/01/Pair.java.md +1 -0
  61. package/{content → init/content}/lectures/index.md +1 -1
  62. package/{content → init/content}/markdown.md +26 -7
  63. package/package.json +19 -7
  64. package/{templates → schema}/nav.schema.json +1 -2
  65. package/{build → schema}/site.schema.json +3 -5
  66. package/src/_alerts.scss +4 -0
  67. package/src/_base.scss +90 -6
  68. package/src/_layout.scss +39 -2
  69. package/src/anchor/index.ts +0 -1
  70. package/src/index.ts +3 -3
  71. package/src/question/style.scss +8 -3
  72. package/src/search/index.test.ts +203 -0
  73. package/src/search/index.ts +3 -79
  74. package/src/search/pdf-utils.ts +79 -0
  75. package/src/search/style.scss +2 -20
  76. package/src/style.scss +20 -101
  77. package/src/timezone/index.ts +9 -25
  78. package/src/util.ts +1 -1
  79. package/templates/_top.html +2 -1
  80. package/types/dev.d.ts +1 -3
  81. package/types/site-variables.d.ts +6 -10
  82. package/types/untyped-modules.d.ts +7 -0
  83. package/build/utils/jdi-runner/LiterateRunner.class +0 -0
  84. package/config/authors.json +0 -1
  85. package/config/nav.json +0 -28
  86. /package/{content → init/content}/index.md +0 -0
  87. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  88. /package/{content → init/content}/lectures/01/demo.py +0 -0
  89. /package/{content → init/content}/lectures/01/index.md +0 -0
  90. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  91. /package/{content → init/content}/problem_sets/index.html +0 -0
  92. /package/{public → init/public}/avatars/alex.jpg +0 -0
  93. /package/{public → init/public}/test.txt +0 -0
  94. /package/{templates → schema}/authors.schema.json +0 -0
@@ -2,31 +2,28 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import _ from 'lodash';
4
4
  import { stripHtml } from 'string-strip-html';
5
- import { makeLogger } from '../log.js';
6
- import { B } from '../colors.js';
7
- import createGlobals from '../globals.js';
8
- import { render, json } from '../templates.js';
5
+ import { isFeatureEnabled } from '../features';
6
+ import { makeLogger } from '../log';
7
+ import { B } from '../colors';
8
+ import createGlobals from '../globals';
9
+ import { render, json } from '../templates';
9
10
  import {
10
11
  extractJavaMethodToc,
11
12
  renderCodeSegment,
12
13
  renderCodeWithComments,
13
- } from './code.js';
14
- import { extensionIsMarkdown } from './file-types.js';
15
- import {
16
- createApplyBasePath,
17
- normalizeOutputPath,
18
- getDistDir,
19
- } from './paths.js';
20
- import { parseFrontMatterAndContent } from './front-matter.js';
21
- import { createMarkdown } from './markdown.js';
22
- import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin.js';
14
+ } from './code';
15
+ import { extensionIsMarkdown } from './file-types';
16
+ import { createApplyBasePath, normalizeOutputPath } from './paths';
17
+ import { parseFrontMatterAndContent } from './front-matter';
18
+ import { createMarkdown } from './markdown';
19
+ import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
23
20
  import {
24
21
  parseLiterateJava,
25
22
  hasMainMethod,
26
23
  deriveClassName,
27
24
  compileJavaSource,
28
25
  executeLiterateJava,
29
- } from './literate-java.js';
26
+ } from './literate-java';
30
27
  import type {
31
28
  Asset,
32
29
  SiteVariables,
@@ -34,7 +31,7 @@ import type {
34
31
  RenderCodePageOptions,
35
32
  RenderLiterateJavaOptions,
36
33
  RenderCopiedContentOptions,
37
- } from '../types.js';
34
+ } from '../types';
38
35
 
39
36
  const log = makeLogger(__filename);
40
37
 
@@ -112,6 +109,7 @@ export function injectAssetTags(
112
109
  html: string,
113
110
  assetFiles: string[],
114
111
  applyBasePath: (subPath: string) => string,
112
+ distDir: string,
115
113
  ): string {
116
114
  const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
117
115
  const cssAssets = assetFiles.filter(f => f.endsWith('.css'));
@@ -121,8 +119,6 @@ export function injectAssetTags(
121
119
  .join('');
122
120
  const criticalAssets = cssAssets.filter(f => f.includes('critical.bundle.'));
123
121
  const asyncAssets = cssAssets.filter(f => !f.includes('critical.bundle.'));
124
-
125
- const distDir = getDistDir();
126
122
  const criticalTags = criticalAssets
127
123
  .map(asset => {
128
124
  const css = fs.readFileSync(path.join(distDir, asset), 'utf-8');
@@ -131,9 +127,7 @@ export function injectAssetTags(
131
127
  .join('');
132
128
  const asyncLinkTags = asyncAssets
133
129
  .map(
134
- asset =>
135
- `<link href="${applyBasePath('/' + asset)}" rel="stylesheet" media="print" onload="this.media='all'">` +
136
- `<noscript><link href="${applyBasePath('/' + asset)}" rel="stylesheet"></noscript>`,
130
+ asset => `<link href="${applyBasePath('/' + asset)}" rel="stylesheet">`,
137
131
  )
138
132
  .join('');
139
133
 
@@ -142,6 +136,15 @@ export function injectAssetTags(
142
136
  .replace('</head>', `${scriptTags}</head>`);
143
137
  }
144
138
 
139
+ export function injectKatexStylesheet(
140
+ html: string,
141
+ applyBasePath: (subPath: string) => string,
142
+ ): string {
143
+ const href = applyBasePath('/katex/katex.min.css');
144
+ const tag = `<link href="${href}" rel="stylesheet">`;
145
+ return html.replace('<head>', `<head>${tag}`);
146
+ }
147
+
145
148
  function toContentAssetPath(contentDir: string, filePath: string): string {
146
149
  return path
147
150
  .relative(contentDir, filePath)
@@ -152,6 +155,7 @@ function toContentAssetPath(contentDir: string, filePath: string): string {
152
155
  export function renderPlainTextPageAsset({
153
156
  filePath,
154
157
  contentDir,
158
+ distDir,
155
159
  siteVariables,
156
160
  validInternalTargets,
157
161
  assetFiles,
@@ -190,11 +194,14 @@ export function renderPlainTextPageAsset({
190
194
  subPath,
191
195
  });
192
196
 
193
- const html = injectAssetTags(
194
- render(`${pageVariables.template}.html`, templateParameters) as string,
195
- assetFiles,
196
- applyBasePath,
197
- );
197
+ const templateHtml = render(
198
+ `${pageVariables.template}.html`,
199
+ templateParameters,
200
+ ) as string;
201
+ let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
202
+ if (templateHtml.includes('class="katex"')) {
203
+ html = injectKatexStylesheet(html, applyBasePath);
204
+ }
198
205
 
199
206
  return [
200
207
  {
@@ -207,6 +214,7 @@ export function renderPlainTextPageAsset({
207
214
  export function renderCodePageAsset({
208
215
  filePath,
209
216
  contentDir,
217
+ distDir,
210
218
  siteVariables,
211
219
  assetFiles,
212
220
  }: RenderCodePageOptions): Asset[] {
@@ -247,6 +255,7 @@ export function renderCodePageAsset({
247
255
  render('code.html', templateParameters) as string,
248
256
  assetFiles,
249
257
  applyBasePath,
258
+ distDir,
250
259
  );
251
260
 
252
261
  return [
@@ -288,10 +297,9 @@ function renderPlainTextContent(
288
297
  filePath,
289
298
  sourceUrlPath,
290
299
  validTargets: validInternalTargets,
291
- codeExtensions:
292
- siteVariables.features?.code === false
293
- ? []
294
- : Object.keys(siteVariables.codeLanguages!),
300
+ codeExtensions: isFeatureEnabled(siteVariables, 'code')
301
+ ? Object.keys(siteVariables.codeLanguages!)
302
+ : [],
295
303
  },
296
304
  });
297
305
 
@@ -381,6 +389,7 @@ export function stripHtmlComments(str: string): string {
381
389
  export function renderLiterateJavaPageAsset({
382
390
  filePath,
383
391
  contentDir,
392
+ distDir,
384
393
  siteVariables,
385
394
  assetFiles,
386
395
  skipExecution,
@@ -403,6 +412,9 @@ export function renderLiterateJavaPageAsset({
403
412
 
404
413
  validateFrontMatter(pageVariables, filePath);
405
414
 
415
+ const stdin =
416
+ typeof pageVariables.stdin === 'string' ? pageVariables.stdin : undefined;
417
+
406
418
  // Compile and execute the concatenated Java source
407
419
  let tempDir: string | undefined;
408
420
  let blockOutputMap: Map<number, string> | null = null;
@@ -416,6 +428,7 @@ export function renderLiterateJavaPageAsset({
416
428
  className,
417
429
  tempDir,
418
430
  codeBlocks,
431
+ stdin,
419
432
  );
420
433
  blockOutputMap = new Map(
421
434
  outputEntries.map(e => [e.blockIndex, e.output]),
@@ -434,9 +447,17 @@ export function renderLiterateJavaPageAsset({
434
447
  validatorOptions: { enabled: false },
435
448
  });
436
449
  let fenceIndex = 0;
450
+ const defaultFence = md.renderer.rules.fence!;
437
451
 
438
- md.renderer.rules.fence = (tokens, idx) => {
452
+ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
439
453
  const token = tokens[idx];
454
+ const lang = token.info.trim();
455
+
456
+ // Non-Java fences render with the default renderer (no line numbers)
457
+ if (lang !== '' && lang !== 'java') {
458
+ return defaultFence(tokens, idx, options, env, self);
459
+ }
460
+
440
461
  const code = token.content;
441
462
  const lines = code.endsWith('\n')
442
463
  ? code.slice(0, -1).split('\n')
@@ -509,11 +530,11 @@ export function renderLiterateJavaPageAsset({
509
530
  subPath,
510
531
  });
511
532
 
512
- const html = injectAssetTags(
513
- render('literate.html', templateParameters) as string,
514
- assetFiles,
515
- applyBasePath,
516
- );
533
+ const templateHtml = render('literate.html', templateParameters) as string;
534
+ let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
535
+ if (templateHtml.includes('class="katex"')) {
536
+ html = injectKatexStylesheet(html, applyBasePath);
537
+ }
517
538
 
518
539
  return [
519
540
  {
@@ -1,5 +1,5 @@
1
1
  import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki';
2
- import { makeLogger } from '../log.js';
2
+ import { makeLogger } from '../log';
3
3
 
4
4
  const log = makeLogger(__filename);
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import MarkdownIt from 'markdown-it';
3
- import validateInternalLinks from './validate-internal-links-plugin.js';
3
+ import validateInternalLinks from './validate-internal-links-plugin';
4
4
 
5
5
  function createMd(
6
6
  validTargets: string[],
@@ -1,7 +1,7 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
2
  import type Token from 'markdown-it/lib/token.mjs';
3
3
  import path from 'path';
4
- import { makeLogger } from './log.js';
4
+ import { makeLogger } from './log';
5
5
 
6
6
  const log = makeLogger(__filename);
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { collectReachableSiteAssets } from './reachability.js';
3
- import WatchReachabilityState from './watch-reachability-state.js';
2
+ import { collectReachableSiteAssets } from './reachability';
3
+ import WatchReachabilityState from './watch-reachability-state';
4
4
 
5
5
  function createReader(htmlByPath: Map<string, string>) {
6
6
  const calls: string[] = [];
@@ -1,4 +1,4 @@
1
- import { collectDirectSiteAssetLinks } from './reachability.js';
1
+ import { collectDirectSiteAssetLinks } from './reachability';
2
2
 
3
3
  type ReadHtmlForAsset = (assetPath: string) => string;
4
4
 
package/build/watch.ts CHANGED
@@ -4,37 +4,33 @@ import { fork } from 'child_process';
4
4
  import path from 'path';
5
5
  import chokidar from 'chokidar';
6
6
  import WebSocket, { WebSocketServer } from 'ws';
7
- import type { SiteVariables, WatchState } from './types.js';
8
- import { B } from './colors.js';
9
- import { makeLogger, printFlair } from './log.js';
10
- import { getDevSiteVariables } from './site-variables.js';
11
- import {
12
- compileTemplates,
13
- getHtmlTemplatesDir,
14
- getJsonDataDir,
15
- JSON_DATA_FILES,
16
- } from './templates.js';
7
+ import type { SiteVariables, WatchState } from './types';
8
+ import { B } from './colors';
9
+ import { makeLogger, printFlair } from './log';
10
+ import { getDevSiteVariables } from './site-variables';
11
+ import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
17
12
  import {
18
13
  getContentDir,
19
14
  getPublicDir,
20
15
  getDistDir,
21
16
  getPackageDir,
22
- } from './utils/paths.js';
23
- import { isFeatureEnabled } from './features.js';
24
- import { bundle } from './bundle.js';
25
- import { copyFonts } from './generate-fonts.js';
26
- import { generateFavicons } from './generate-favicon.js';
27
- import { generateManifest } from './generate-manifest.js';
17
+ } from './utils/paths';
18
+ import { isFeatureEnabled } from './features';
19
+ import { bundle } from './bundle';
20
+ import { copyFonts } from './generate-fonts';
21
+ import { copyKatexAssets } from './generate-katex-assets';
22
+ import { generateFavicons } from './generate-favicon';
23
+ import { generateManifest } from './generate-manifest';
28
24
  import {
29
25
  copyPublicFiles,
30
26
  copyContentAssets,
31
27
  copyContentFile,
32
28
  copyPublicFile,
33
- } from './copy.js';
34
- import { getProcessedExtensions } from './utils/file-types.js';
35
- import { ContentRenderer } from './generate-content-assets.js';
36
- import { WatchPagefindRunner } from './pagefind.js';
37
- import { ContentChangeDetector } from './content-watch.js';
29
+ } from './copy';
30
+ import { getProcessedExtensions } from './utils/file-types';
31
+ import { ContentRenderer } from './generate-content-assets';
32
+ import { WatchPagefindRunner } from './pagefind';
33
+ import { ContentChangeDetector } from './content-watch';
38
34
 
39
35
  function getArg(name: string): number | null {
40
36
  const idx = process.argv.indexOf(name);
@@ -74,14 +70,15 @@ async function bundleReloadClient(): Promise<string[]> {
74
70
  );
75
71
  }
76
72
 
77
- type ChangeCategory = 'content' | 'public' | 'src' | 'templates' | 'config';
73
+ type ChangeCategory = 'content' | 'public' | 'config';
78
74
 
79
75
  const log = makeLogger(__filename);
80
76
  const wslog = makeLogger('WebSocket');
81
77
 
82
- // --- WebSocket server (unchanged) ---
78
+ // --- WebSocket server ---
83
79
 
84
80
  let webSocketsReady = false;
81
+ let watcherReady = false;
85
82
  let webServerReady = false;
86
83
  let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
87
84
  let serveStarted = false;
@@ -92,6 +89,12 @@ try {
92
89
 
93
90
  wss.on('connection', (conn: WebSocket) => {
94
91
  wslog.debug`WebSocket client connected`;
92
+ if (watcherReady && conn.readyState === WebSocket.OPEN) {
93
+ if (initialBuildFailed) {
94
+ conn.send('error');
95
+ }
96
+ conn.send('ready');
97
+ }
95
98
  conn.on('close', () => {
96
99
  wslog.debug`WebSocket client disconnected`;
97
100
  });
@@ -196,12 +199,9 @@ function toPublicRelativePath(filePath: string): string | null {
196
199
  const contentDir: string = getContentDir();
197
200
  const publicDir: string = getPublicDir();
198
201
  const distDir: string = getDistDir();
199
- const packageDir: string = getPackageDir();
200
202
 
201
- let siteVariables: SiteVariables = getDevSiteVariables();
202
- let processedExtSet = new Set<string>(
203
- getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
204
- );
203
+ let siteVariables: SiteVariables;
204
+ let processedExtSet = new Set<string>();
205
205
  let publicRelPaths: Set<string> = new Set();
206
206
  let contentAssetRelPaths: Set<string> = new Set();
207
207
  let contentRenderer: ContentRenderer;
@@ -213,6 +213,10 @@ const pendingChanges = new Set<string>();
213
213
  let rebuilding = false;
214
214
 
215
215
  async function initialBuild(): Promise<void> {
216
+ siteVariables = getDevSiteVariables();
217
+ processedExtSet = new Set(
218
+ getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
219
+ );
216
220
  fs.mkdirSync(distDir, { recursive: true });
217
221
 
218
222
  compileTemplates(siteVariables);
@@ -231,6 +235,7 @@ async function initialBuild(): Promise<void> {
231
235
  ];
232
236
 
233
237
  copyFonts(distDir);
238
+ copyKatexAssets(distDir);
234
239
 
235
240
  if (isFeatureEnabled(siteVariables, 'favicon')) {
236
241
  await generateFavicons(siteVariables, distDir);
@@ -270,8 +275,6 @@ function classifyChange(filePath: string): ChangeCategory | null {
270
275
  const resolved = path.resolve(filePath);
271
276
  const resolvedContentDir = path.resolve(contentDir) + path.sep;
272
277
  const resolvedPublicDir = path.resolve(publicDir) + path.sep;
273
- const resolvedSrcDir = path.resolve(packageDir, 'src') + path.sep;
274
- const htmlTemplatesDir = path.resolve(getHtmlTemplatesDir()) + path.sep;
275
278
  const jsonDataDir = getJsonDataDir();
276
279
  const siteConfigPath = path.resolve('site.dev.json');
277
280
 
@@ -281,16 +284,10 @@ function classifyChange(filePath: string): ChangeCategory | null {
281
284
  if (resolved.startsWith(resolvedPublicDir)) {
282
285
  return 'public';
283
286
  }
284
- if (resolved.startsWith(resolvedSrcDir)) {
285
- return 'src';
286
- }
287
287
  if (
288
- resolved.startsWith(htmlTemplatesDir) ||
288
+ resolved === siteConfigPath ||
289
289
  JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
290
290
  ) {
291
- return 'templates';
292
- }
293
- if (resolved === siteConfigPath) {
294
291
  return 'config';
295
292
  }
296
293
  return null;
@@ -316,6 +313,8 @@ async function rebuild(): Promise<void> {
316
313
  }
317
314
  }
318
315
 
316
+ let succeeded = false;
317
+
319
318
  try {
320
319
  // Site config changed, full restart
321
320
  if (categories.has('config')) {
@@ -337,61 +336,36 @@ async function rebuild(): Promise<void> {
337
336
  ...(await bundleReloadClient()),
338
337
  ];
339
338
 
340
- const result = contentRenderer.processContent({ distDir, assetFiles });
341
- for (const err of result.errors) {
342
- log.error`${err.message}`;
343
- }
344
- if (result.errors.length === 0) {
345
- printFlair();
346
- broadcast('reload');
347
- if (pagefindRunner) {
348
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
349
- setImmediate(() => pagefindRunner!.run());
350
- }
339
+ copyFonts(distDir);
340
+ copyKatexAssets(distDir);
341
+
342
+ if (isFeatureEnabled(siteVariables, 'favicon')) {
343
+ await generateFavicons(siteVariables, distDir);
344
+ generateManifest(siteVariables, distDir);
351
345
  }
352
- rebuilding = false;
353
- return;
354
- }
355
346
 
356
- // Source changed, re-bundle, then re-render all content
357
- if (categories.has('src')) {
358
- assetFiles = [
359
- ...(await bundle(siteVariables, { mode: 'development' })),
360
- ...(await bundleReloadClient()),
361
- ];
362
- // Force full content re-render since asset filenames may have changed
363
347
  const result = contentRenderer.processContent({ distDir, assetFiles });
364
348
  for (const err of result.errors) {
365
349
  log.error`${err.message}`;
366
350
  }
367
351
  if (result.errors.length === 0) {
368
352
  printFlair();
353
+ if (!serveStarted) {
354
+ serveStarted = true;
355
+ serve();
356
+ }
369
357
  broadcast('reload');
370
358
  if (pagefindRunner) {
371
359
  pagefindRunner.update(distDir, result.htmlAssetsByPath);
372
360
  setImmediate(() => pagefindRunner!.run());
373
361
  }
362
+ succeeded = true;
374
363
  }
375
- rebuilding = false;
376
364
  return;
377
365
  }
378
366
 
379
- // Templates/data changed, recompile templates, re-render all content
380
- if (categories.has('templates')) {
381
- const detection = changeDetector.detectChanges(changes);
382
- if (detection.templateError) {
383
- log.error`Template error: ${detection.templateError.message}`;
384
- rebuilding = false;
385
- return;
386
- }
387
- }
388
-
389
367
  // Public file changed, copy just that file
390
- if (
391
- categories.has('public') &&
392
- !categories.has('content') &&
393
- !categories.has('templates')
394
- ) {
368
+ if (categories.has('public') && !categories.has('content')) {
395
369
  for (const filePath of changes) {
396
370
  if (classifyChange(filePath) === 'public') {
397
371
  const absPath = path.resolve(filePath);
@@ -407,17 +381,12 @@ async function rebuild(): Promise<void> {
407
381
  }
408
382
  printFlair();
409
383
  broadcast('reload');
410
- rebuilding = false;
384
+ succeeded = true;
411
385
  return;
412
386
  }
413
387
 
414
- // Content and/or templates changed, incremental rebuild
388
+ // Content changed, incremental rebuild
415
389
  const detection = changeDetector.detectChanges(changes);
416
- if (detection.templateError) {
417
- log.error`Template error: ${detection.templateError.message}`;
418
- rebuilding = false;
419
- return;
420
- }
421
390
 
422
391
  if (detection.needsRestart) {
423
392
  log.event`Content structure changed, full rebuild`;
@@ -493,16 +462,24 @@ async function rebuild(): Promise<void> {
493
462
 
494
463
  if (result.errors.length === 0) {
495
464
  printFlair();
465
+ if (!serveStarted) {
466
+ serveStarted = true;
467
+ serve();
468
+ }
496
469
  broadcast('reload');
497
470
 
498
471
  if (pagefindRunner) {
499
472
  pagefindRunner.update(distDir, result.htmlAssetsByPath);
500
473
  setImmediate(() => pagefindRunner!.run());
501
474
  }
475
+ succeeded = true;
502
476
  }
503
477
  } catch (err) {
504
478
  log.error`Build failed: ${(err as Error).message}`;
505
479
  } finally {
480
+ if (!succeeded) {
481
+ broadcast('error');
482
+ }
506
483
  rebuilding = false;
507
484
 
508
485
  // If more changes accumulated during rebuild, schedule another
@@ -526,35 +503,36 @@ function onFileChange(filePath: string): void {
526
503
 
527
504
  // --- Start ---
528
505
 
506
+ let initialBuildFailed = false;
507
+
529
508
  initialBuild()
530
- .then(() => {
531
- // Watch content, public, src, templates, data files, site config
509
+ .catch(err => {
510
+ log.error`Initial build failed: ${(err as Error).message}`;
511
+ initialBuildFailed = true;
512
+ })
513
+ .finally(() => {
532
514
  const watchPaths: string[] = [contentDir, publicDir];
533
515
 
534
- // Watch package src/ and templates/ for Tada development
535
- const srcDir = path.resolve(packageDir, 'src');
536
- const templatesDir = getHtmlTemplatesDir();
537
- if (fs.existsSync(srcDir)) {
538
- watchPaths.push(srcDir);
539
- }
540
- if (fs.existsSync(templatesDir)) {
541
- watchPaths.push(templatesDir);
542
- }
543
-
544
- // Watch data files
516
+ // Watch config files (nav.json, authors.json, site config) via the
517
+ // project root directory (depth 0). Chokidar v4 doesn't emit unlink
518
+ // for individually watched files, so we must watch the directory.
545
519
  const jsonDataDir = getJsonDataDir();
546
- for (const dataFile of JSON_DATA_FILES) {
547
- const dataPath = path.join(jsonDataDir, dataFile);
548
- if (fs.existsSync(dataPath)) {
549
- watchPaths.push(dataPath);
520
+ const configFilePaths = new Set([
521
+ ...JSON_DATA_FILES.map(f => path.resolve(jsonDataDir, f)),
522
+ path.resolve('site.dev.json'),
523
+ ]);
524
+ const configWatcher = chokidar.watch(jsonDataDir, {
525
+ ignoreInitial: true,
526
+ depth: 0,
527
+ });
528
+ const onConfigFileChange = (filePath: string) => {
529
+ if (configFilePaths.has(path.resolve(filePath))) {
530
+ onFileChange(filePath);
550
531
  }
551
- }
552
-
553
- // Watch site config
554
- const siteConfigPath = path.resolve('site.dev.json');
555
- if (fs.existsSync(siteConfigPath)) {
556
- watchPaths.push(siteConfigPath);
557
- }
532
+ };
533
+ configWatcher.on('add', onConfigFileChange);
534
+ configWatcher.on('change', onConfigFileChange);
535
+ configWatcher.on('unlink', onConfigFileChange);
558
536
 
559
537
  const watcher = chokidar.watch(watchPaths, {
560
538
  ignoreInitial: true,
@@ -565,9 +543,12 @@ initialBuild()
565
543
  watcher.on('add', onFileChange);
566
544
  watcher.on('unlink', onFileChange);
567
545
 
568
- log.info`Watching for changes...`;
569
- })
570
- .catch(err => {
571
- log.error`Initial build failed: ${(err as Error).message}`;
572
- process.exit(1);
546
+ watcher.on('ready', () => {
547
+ watcherReady = true;
548
+ if (initialBuildFailed) {
549
+ broadcast('error');
550
+ }
551
+ broadcast('ready');
552
+ log.info`Watching for changes...`;
553
+ });
573
554
  });