@abreen/tada 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +29 -33
  2. package/bin/tada.ts +356 -0
  3. package/bin/validators.test.ts +204 -0
  4. package/bin/validators.ts +83 -0
  5. package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
  6. package/build/bundle.ts +117 -0
  7. package/{webpack/code.test.js → build/code.test.ts} +6 -7
  8. package/build/colors.ts +25 -0
  9. package/build/content-watch.ts +107 -0
  10. package/build/copy.ts +118 -0
  11. package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
  12. package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
  13. package/build/features.ts +11 -0
  14. package/build/generate-content-assets.ts +315 -0
  15. package/build/generate-favicon.ts +165 -0
  16. package/build/generate-fonts.ts +31 -0
  17. package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
  18. package/build/globals.test.ts +101 -0
  19. package/{webpack/globals.js → build/globals.ts} +28 -13
  20. package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
  21. package/build/json-schema.test.ts +57 -0
  22. package/build/json-schema.ts +33 -0
  23. package/build/log.test.ts +111 -0
  24. package/build/log.ts +167 -0
  25. package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
  26. package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
  27. package/build/pagefind.ts +339 -0
  28. package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
  29. package/build/pipeline.ts +93 -0
  30. package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
  31. package/{webpack/reachability.js → build/reachability.ts} +77 -34
  32. package/build/serve.ts +112 -0
  33. package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
  34. package/{webpack → build}/site.schema.json +3 -10
  35. package/{webpack/templates.js → build/templates.ts} +35 -33
  36. package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
  37. package/build/toc-plugin.test.ts +105 -0
  38. package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
  39. package/build/types.ts +172 -0
  40. package/build/util.ts +26 -0
  41. package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
  42. package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
  43. package/build/utils/derive-theme.test.ts +111 -0
  44. package/build/utils/derive-theme.ts +85 -0
  45. package/build/utils/file-types.test.ts +61 -0
  46. package/build/utils/file-types.ts +13 -0
  47. package/build/utils/front-matter.test.ts +80 -0
  48. package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
  49. package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
  50. package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
  51. package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
  52. package/build/utils/paths.test.ts +91 -0
  53. package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
  54. package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
  55. package/build/utils/shiki-highlighter.ts +29 -0
  56. package/build/validate-internal-links-plugin.test.ts +106 -0
  57. package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
  58. package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
  59. package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
  60. package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
  61. package/build/watch.ts +573 -0
  62. package/content/index.md +9 -3
  63. package/content/markdown.md +2 -1
  64. package/content/problem_sets/index.html +14 -0
  65. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
  66. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
  67. package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
  68. package/fonts/inter/woff2/InterVariable.woff2 +0 -0
  69. package/package.json +28 -19
  70. package/src/_alerts.scss +92 -0
  71. package/src/_base.scss +106 -0
  72. package/src/{layout.scss → _layout.scss} +0 -2
  73. package/src/anchor/style.scss +1 -9
  74. package/src/code/index.ts +3 -3
  75. package/src/code.scss +1 -1
  76. package/src/critical.scss +5 -0
  77. package/src/header/_base.scss +129 -0
  78. package/src/header/style.scss +3 -131
  79. package/src/index.ts +1 -2
  80. package/src/question/style.scss +1 -1
  81. package/src/search/index.ts +36 -15
  82. package/src/search/style.scss +9 -15
  83. package/src/style.scss +6 -269
  84. package/src/toc/style.scss +5 -39
  85. package/src/util.ts +8 -5
  86. package/templates/_theme.scss +38 -14
  87. package/tsconfig.json +10 -6
  88. package/types/file-system-access.d.ts +5 -0
  89. package/types/markdown-it-plugins.d.ts +11 -0
  90. package/types/untyped-modules.d.ts +40 -0
  91. package/bin/tada.js +0 -361
  92. package/content/problem_sets/index.md +0 -6
  93. package/webpack/build-state.js +0 -97
  94. package/webpack/colors.js +0 -15
  95. package/webpack/config.base.js +0 -151
  96. package/webpack/config.dev.js +0 -23
  97. package/webpack/config.prod.js +0 -32
  98. package/webpack/content-watch-plugin.js +0 -153
  99. package/webpack/features.js +0 -5
  100. package/webpack/generate-content-assets-plugin.js +0 -308
  101. package/webpack/generate-favicon-plugin.js +0 -198
  102. package/webpack/generate-fonts-plugin.js +0 -69
  103. package/webpack/json-schema.js +0 -19
  104. package/webpack/log.js +0 -143
  105. package/webpack/pagefind-plugin.js +0 -379
  106. package/webpack/print-flair-plugin.js +0 -22
  107. package/webpack/serve.js +0 -104
  108. package/webpack/util.js +0 -49
  109. package/webpack/utils/define-plugin.js +0 -20
  110. package/webpack/utils/file-types.js +0 -26
  111. package/webpack/utils/parse-hsl.js +0 -8
  112. package/webpack/utils/shiki-highlighter.js +0 -26
  113. package/webpack/watch.js +0 -166
  114. /package/{webpack → build}/flair.json +0 -0
  115. /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
  116. /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
  117. /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
  118. /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
  119. /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
  120. /package/types/{dev.ts → dev.d.ts} +0 -0
package/build/watch.ts ADDED
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env bun
2
+ import fs from 'fs';
3
+ import { fork } from 'child_process';
4
+ import path from 'path';
5
+ import chokidar from 'chokidar';
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';
17
+ import {
18
+ getContentDir,
19
+ getPublicDir,
20
+ getDistDir,
21
+ 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';
28
+ import {
29
+ copyPublicFiles,
30
+ copyContentAssets,
31
+ copyContentFile,
32
+ 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';
38
+
39
+ function getArg(name: string): number | null {
40
+ const idx = process.argv.indexOf(name);
41
+ if (idx !== -1 && process.argv[idx + 1]) {
42
+ const val = parseInt(process.argv[idx + 1], 10);
43
+ if (val > 0 && val < 65536) {
44
+ return val;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ const httpPort = getArg('--port');
51
+ const WEBSOCKET_PORT = getArg('--ws-port') ?? 35729;
52
+ const DEBOUNCE_MS = 300;
53
+ const RELOAD_CLIENT_PATH = path.resolve(
54
+ getPackageDir(),
55
+ 'build/watch-reload-client.ts',
56
+ );
57
+
58
+ // Bundle the reload client separately to work around a Bun bundler bug where
59
+ // side-effect-only JS entrypoints are tree-shaken when bundled alongside SCSS
60
+ // entrypoints processed by a plugin.
61
+ async function bundleReloadClient(): Promise<string[]> {
62
+ const result = await Bun.build({
63
+ entrypoints: [RELOAD_CLIENT_PATH],
64
+ outdir: getDistDir(),
65
+ naming: '[name].bundle.[ext]',
66
+ sourcemap: 'inline',
67
+ define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
68
+ });
69
+ return result.outputs.map(output =>
70
+ path
71
+ .relative(getDistDir(), output.path)
72
+ .split(path.sep)
73
+ .join(path.posix.sep),
74
+ );
75
+ }
76
+
77
+ type ChangeCategory = 'content' | 'public' | 'src' | 'templates' | 'config';
78
+
79
+ const log = makeLogger(__filename);
80
+ const wslog = makeLogger('WebSocket');
81
+
82
+ // --- WebSocket server (unchanged) ---
83
+
84
+ let webSocketsReady = false;
85
+ let webServerReady = false;
86
+ let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
87
+ let serveStarted = false;
88
+
89
+ let wss: WebSocketServer | null = null;
90
+ try {
91
+ wss = new WebSocketServer({ port: WEBSOCKET_PORT });
92
+
93
+ wss.on('connection', (conn: WebSocket) => {
94
+ wslog.debug`WebSocket client connected`;
95
+ conn.on('close', () => {
96
+ wslog.debug`WebSocket client disconnected`;
97
+ });
98
+ });
99
+
100
+ wss.on('error', (err: Error) => {
101
+ wslog.error`WebSocket server error: ${err.message}`;
102
+ });
103
+
104
+ wss.on('listening', () => {
105
+ wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
106
+ webSocketsReady = true;
107
+ });
108
+ } catch (err) {
109
+ wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${(err as Error).message}`;
110
+ }
111
+
112
+ function broadcast(msg: string): void {
113
+ if (wss == null || !webSocketsReady) {
114
+ return;
115
+ }
116
+ wslog.debug`Broadcasting "${msg}" to WebSocket clients`;
117
+ wss.clients.forEach((client: WebSocket) => {
118
+ if (client.readyState === WebSocket.OPEN) {
119
+ client.send(msg);
120
+ }
121
+ });
122
+ }
123
+
124
+ // --- Dev server ---
125
+
126
+ function serve(): void {
127
+ const serveArgs = httpPort != null ? ['--port', String(httpPort)] : [];
128
+ const child = fork(path.join(__dirname, 'serve.js'), serveArgs, {
129
+ stdio: 'inherit',
130
+ });
131
+ child.on('close', (code: number | null) => {
132
+ webServerReady = false;
133
+ log.error`Web server exited with code ${code}`;
134
+ process.exit(2);
135
+ });
136
+ child.on('error', (err: Error) => {
137
+ webServerReady = false;
138
+ log.error`Web server failed: ${err.message}`;
139
+ });
140
+ child.on('message', (msg: Record<string, unknown>) => {
141
+ if (msg.ready) {
142
+ webServerReady = true;
143
+ clearTimeout(webServerTimeout);
144
+ }
145
+ });
146
+
147
+ webServerTimeout = setTimeout(() => {
148
+ if (webServerReady) {
149
+ return;
150
+ }
151
+ log.error`Web server failed to report within 10 seconds, exiting`;
152
+ process.exit(3);
153
+ }, 10000);
154
+ }
155
+
156
+ // --- Path helpers ---
157
+
158
+ function toContentMarkdownPath(filePath: string): string | null {
159
+ if (!filePath) {
160
+ return null;
161
+ }
162
+ const ext = path.extname(filePath).toLowerCase();
163
+ if (!['.md', '.markdown'].includes(ext)) {
164
+ return null;
165
+ }
166
+
167
+ const normalizedContentDir = path.resolve(contentDir) + path.sep;
168
+ const normalizedFilePath = path.resolve(filePath);
169
+ if (!normalizedFilePath.startsWith(normalizedContentDir)) {
170
+ return null;
171
+ }
172
+
173
+ return path
174
+ .relative(contentDir, normalizedFilePath)
175
+ .split(path.sep)
176
+ .join(path.posix.sep);
177
+ }
178
+
179
+ function toPublicRelativePath(filePath: string): string | null {
180
+ if (!filePath) {
181
+ return null;
182
+ }
183
+ const normalizedPublicDir = path.resolve(publicDir) + path.sep;
184
+ const normalizedFilePath = path.resolve(filePath);
185
+ if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
186
+ return null;
187
+ }
188
+ return path
189
+ .relative(publicDir, normalizedFilePath)
190
+ .split(path.sep)
191
+ .join(path.posix.sep);
192
+ }
193
+
194
+ // --- Watch mode ---
195
+
196
+ const contentDir: string = getContentDir();
197
+ const publicDir: string = getPublicDir();
198
+ const distDir: string = getDistDir();
199
+ const packageDir: string = getPackageDir();
200
+
201
+ let siteVariables: SiteVariables = getDevSiteVariables();
202
+ let processedExtSet = new Set<string>(
203
+ getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
204
+ );
205
+ let publicRelPaths: Set<string> = new Set();
206
+ let contentAssetRelPaths: Set<string> = new Set();
207
+ let contentRenderer: ContentRenderer;
208
+ let changeDetector: ContentChangeDetector;
209
+ let pagefindRunner: WatchPagefindRunner | undefined;
210
+ let assetFiles: string[] = [];
211
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
212
+ const pendingChanges = new Set<string>();
213
+ let rebuilding = false;
214
+
215
+ async function initialBuild(): Promise<void> {
216
+ fs.mkdirSync(distDir, { recursive: true });
217
+
218
+ compileTemplates(siteVariables);
219
+ contentRenderer = new ContentRenderer(siteVariables);
220
+ changeDetector = new ContentChangeDetector(siteVariables);
221
+
222
+ if (isFeatureEnabled(siteVariables, 'search')) {
223
+ pagefindRunner = new WatchPagefindRunner(siteVariables);
224
+ }
225
+
226
+ await contentRenderer.initHighlighter();
227
+
228
+ assetFiles = [
229
+ ...(await bundle(siteVariables, { mode: 'development' })),
230
+ ...(await bundleReloadClient()),
231
+ ];
232
+
233
+ copyFonts(distDir);
234
+
235
+ if (isFeatureEnabled(siteVariables, 'favicon')) {
236
+ await generateFavicons(siteVariables, distDir);
237
+ generateManifest(siteVariables, distDir);
238
+ }
239
+
240
+ publicRelPaths = copyPublicFiles(publicDir, distDir);
241
+ contentAssetRelPaths = copyContentAssets(
242
+ contentDir,
243
+ distDir,
244
+ [...processedExtSet],
245
+ publicRelPaths,
246
+ );
247
+
248
+ const result = contentRenderer.processContent({ distDir, assetFiles });
249
+
250
+ for (const err of result.errors) {
251
+ log.error`${err.message}`;
252
+ }
253
+
254
+ if (result.errors.length === 0) {
255
+ printFlair();
256
+
257
+ if (pagefindRunner) {
258
+ pagefindRunner.update(distDir, result.htmlAssetsByPath);
259
+ setImmediate(() => pagefindRunner!.run());
260
+ }
261
+
262
+ if (!serveStarted) {
263
+ serveStarted = true;
264
+ serve();
265
+ }
266
+ }
267
+ }
268
+
269
+ function classifyChange(filePath: string): ChangeCategory | null {
270
+ const resolved = path.resolve(filePath);
271
+ const resolvedContentDir = path.resolve(contentDir) + path.sep;
272
+ 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
+ const jsonDataDir = getJsonDataDir();
276
+ const siteConfigPath = path.resolve('site.dev.json');
277
+
278
+ if (resolved.startsWith(resolvedContentDir)) {
279
+ return 'content';
280
+ }
281
+ if (resolved.startsWith(resolvedPublicDir)) {
282
+ return 'public';
283
+ }
284
+ if (resolved.startsWith(resolvedSrcDir)) {
285
+ return 'src';
286
+ }
287
+ if (
288
+ resolved.startsWith(htmlTemplatesDir) ||
289
+ JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
290
+ ) {
291
+ return 'templates';
292
+ }
293
+ if (resolved === siteConfigPath) {
294
+ return 'config';
295
+ }
296
+ return null;
297
+ }
298
+
299
+ async function rebuild(): Promise<void> {
300
+ if (rebuilding) {
301
+ return;
302
+ }
303
+ rebuilding = true;
304
+
305
+ const changes = new Set(pendingChanges);
306
+ pendingChanges.clear();
307
+
308
+ broadcast('rebuilding');
309
+
310
+ // Classify changes
311
+ const categories = new Set<ChangeCategory>();
312
+ for (const filePath of changes) {
313
+ const category = classifyChange(filePath);
314
+ if (category) {
315
+ categories.add(category);
316
+ }
317
+ }
318
+
319
+ try {
320
+ // Site config changed, full restart
321
+ if (categories.has('config')) {
322
+ log.event`Site config changed, restarting`;
323
+ siteVariables = getDevSiteVariables();
324
+ processedExtSet = new Set(
325
+ getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
326
+ );
327
+ contentRenderer = new ContentRenderer(siteVariables);
328
+ changeDetector = new ContentChangeDetector(siteVariables);
329
+ if (isFeatureEnabled(siteVariables, 'search')) {
330
+ pagefindRunner = new WatchPagefindRunner(siteVariables);
331
+ }
332
+ compileTemplates(siteVariables);
333
+ await contentRenderer.initHighlighter();
334
+
335
+ assetFiles = [
336
+ ...(await bundle(siteVariables, { mode: 'development' })),
337
+ ...(await bundleReloadClient()),
338
+ ];
339
+
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
+ }
351
+ }
352
+ rebuilding = false;
353
+ return;
354
+ }
355
+
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
+ const result = contentRenderer.processContent({ distDir, assetFiles });
364
+ for (const err of result.errors) {
365
+ log.error`${err.message}`;
366
+ }
367
+ if (result.errors.length === 0) {
368
+ printFlair();
369
+ broadcast('reload');
370
+ if (pagefindRunner) {
371
+ pagefindRunner.update(distDir, result.htmlAssetsByPath);
372
+ setImmediate(() => pagefindRunner!.run());
373
+ }
374
+ }
375
+ rebuilding = false;
376
+ return;
377
+ }
378
+
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
+ // Public file changed, copy just that file
390
+ if (
391
+ categories.has('public') &&
392
+ !categories.has('content') &&
393
+ !categories.has('templates')
394
+ ) {
395
+ for (const filePath of changes) {
396
+ if (classifyChange(filePath) === 'public') {
397
+ const absPath = path.resolve(filePath);
398
+ if (fs.existsSync(absPath)) {
399
+ copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
400
+ const rel = path
401
+ .relative(publicDir, absPath)
402
+ .split(path.sep)
403
+ .join(path.posix.sep);
404
+ publicRelPaths.add(rel);
405
+ }
406
+ }
407
+ }
408
+ printFlair();
409
+ broadcast('reload');
410
+ rebuilding = false;
411
+ return;
412
+ }
413
+
414
+ // Content and/or templates changed, incremental rebuild
415
+ const detection = changeDetector.detectChanges(changes);
416
+ if (detection.templateError) {
417
+ log.error`Template error: ${detection.templateError.message}`;
418
+ rebuilding = false;
419
+ return;
420
+ }
421
+
422
+ if (detection.needsRestart) {
423
+ log.event`Content structure changed, full rebuild`;
424
+ contentRenderer = new ContentRenderer(siteVariables);
425
+ await contentRenderer.initHighlighter();
426
+ }
427
+
428
+ // Log changed content files
429
+ for (const filePath of changes) {
430
+ const markdownPath = toContentMarkdownPath(filePath);
431
+ if (markdownPath) {
432
+ log.event`${B`${markdownPath}`} changed, rebuilding`;
433
+ } else {
434
+ const pubPath = toPublicRelativePath(filePath);
435
+ if (pubPath) {
436
+ log.event`${B`public/${pubPath}`} changed`;
437
+ }
438
+ }
439
+ }
440
+
441
+ // Copy any changed public files too
442
+ if (categories.has('public')) {
443
+ for (const filePath of changes) {
444
+ if (classifyChange(filePath) === 'public') {
445
+ const absPath = path.resolve(filePath);
446
+ if (fs.existsSync(absPath)) {
447
+ copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
448
+ const rel = path
449
+ .relative(publicDir, absPath)
450
+ .split(path.sep)
451
+ .join(path.posix.sep);
452
+ publicRelPaths.add(rel);
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ // Copy any changed non-processed content files (images, PDFs, etc.)
459
+ if (categories.has('content')) {
460
+ for (const filePath of changes) {
461
+ if (classifyChange(filePath) !== 'content') {
462
+ continue;
463
+ }
464
+ const absPath = path.resolve(filePath);
465
+ const ext = path.extname(absPath).slice(1).toLowerCase();
466
+ if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
467
+ copyContentFile(contentDir, distDir, absPath, publicRelPaths);
468
+ const rel = path
469
+ .relative(contentDir, absPath)
470
+ .split(path.sep)
471
+ .join(path.posix.sep);
472
+ contentAssetRelPaths.add(rel);
473
+ }
474
+ }
475
+ }
476
+
477
+ const watchState: WatchState | undefined = detection.needsRestart
478
+ ? undefined
479
+ : {
480
+ changedContentFiles: detection.changedContentFiles,
481
+ templatesChanged: detection.templatesChanged,
482
+ };
483
+
484
+ const result = contentRenderer.processContent({
485
+ distDir,
486
+ assetFiles,
487
+ watchState,
488
+ });
489
+
490
+ for (const err of result.errors) {
491
+ log.error`${err.message}`;
492
+ }
493
+
494
+ if (result.errors.length === 0) {
495
+ printFlair();
496
+ broadcast('reload');
497
+
498
+ if (pagefindRunner) {
499
+ pagefindRunner.update(distDir, result.htmlAssetsByPath);
500
+ setImmediate(() => pagefindRunner!.run());
501
+ }
502
+ }
503
+ } catch (err) {
504
+ log.error`Build failed: ${(err as Error).message}`;
505
+ } finally {
506
+ rebuilding = false;
507
+
508
+ // If more changes accumulated during rebuild, schedule another
509
+ if (pendingChanges.size > 0) {
510
+ scheduleRebuild();
511
+ }
512
+ }
513
+ }
514
+
515
+ function scheduleRebuild(): void {
516
+ if (debounceTimer) {
517
+ clearTimeout(debounceTimer);
518
+ }
519
+ debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
520
+ }
521
+
522
+ function onFileChange(filePath: string): void {
523
+ pendingChanges.add(filePath);
524
+ scheduleRebuild();
525
+ }
526
+
527
+ // --- Start ---
528
+
529
+ initialBuild()
530
+ .then(() => {
531
+ // Watch content, public, src, templates, data files, site config
532
+ const watchPaths: string[] = [contentDir, publicDir];
533
+
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
545
+ 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);
550
+ }
551
+ }
552
+
553
+ // Watch site config
554
+ const siteConfigPath = path.resolve('site.dev.json');
555
+ if (fs.existsSync(siteConfigPath)) {
556
+ watchPaths.push(siteConfigPath);
557
+ }
558
+
559
+ const watcher = chokidar.watch(watchPaths, {
560
+ ignoreInitial: true,
561
+ awaitWriteFinish: { stabilityThreshold: 100 },
562
+ });
563
+
564
+ watcher.on('change', onFileChange);
565
+ watcher.on('add', onFileChange);
566
+ watcher.on('unlink', onFileChange);
567
+
568
+ log.info`Watching for changes...`;
569
+ })
570
+ .catch(err => {
571
+ log.error`Initial build failed: ${(err as Error).message}`;
572
+ process.exit(1);
573
+ });
package/content/index.md CHANGED
@@ -1,10 +1,13 @@
1
1
  title: Home
2
2
  description: The home page for <%= site.title %>.
3
3
  author: alex
4
+ published: 2026-03-20
5
+ toolName: Tada
4
6
 
5
7
  ## Welcome
6
8
 
7
- This is an example site built with [Tada](https://github.com/abreen/tada).
9
+ This is an example site built with <%= page.toolName %>.
10
+
8
11
 
9
12
  ## Getting started
10
13
 
@@ -14,6 +17,9 @@ contains the site configuration, and `templates/` contains the HTML layouts.
14
17
  See the [Markdown examples page](/markdown.html) for syntax examples.
15
18
 
16
19
  !!! note
17
- Consult the [readme](https://github.com/abreen/tada) for full
18
- documentation on configuration, templates, and content authoring.
20
+ For more documentation, see the [GitHub page][github].
19
21
  !!!
22
+
23
+
24
+
25
+ [github]: https://github.com/abreen/Tada
@@ -1,7 +1,8 @@
1
1
  title: Markdown Examples
2
2
  description: Examples of Markdown syntax supported by Tada.
3
- toc: true
4
3
  author: alex
4
+ published: 2026-03-20
5
+ toc: true
5
6
 
6
7
  Markdown and HTML files in the `content/` directory
7
8
  must contain ["front matter"][front-matter] (YAML-formatted metadata).
@@ -0,0 +1,14 @@
1
+ title: Problem Sets
2
+ author: alex
3
+ description: An example page for <%= site.title %> which is not generated.
4
+ skip: true
5
+
6
+ <p>
7
+ For convenience or backwards compatibility, you can write standard HTML
8
+ files. The front matter section should still be specified for HTML files.
9
+ </p>
10
+
11
+ <p>
12
+ This page is not generated during the build because <code>skip</code>
13
+ is set to <code>true</code>.
14
+ </p>