@abreen/tada 1.9.0 → 1.10.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 (75) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/build/apply-base-path-plugin.ts +79 -17
  4. package/build/bundle.ts +9 -1
  5. package/build/content-watch.ts +3 -3
  6. package/build/copy.ts +30 -0
  7. package/build/generate-content-assets.ts +45 -59
  8. package/build/globals.ts +1 -1
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/types.d.ts +19 -1
  12. package/build/util.ts +3 -0
  13. package/build/utils/content-files.ts +98 -0
  14. package/build/utils/include.ts +4 -0
  15. package/build/utils/python-runner/trace_runner.py +272 -0
  16. package/build/utils/render.ts +34 -8
  17. package/build/utils/trace-core.ts +203 -0
  18. package/build/utils/trace-java.ts +132 -0
  19. package/build/utils/trace-layout.ts +31 -3
  20. package/build/utils/trace-python.ts +26 -0
  21. package/build/utils/trace.ts +174 -294
  22. package/build/validate-internal-links-plugin.ts +16 -4
  23. package/build/watch/assets.ts +111 -0
  24. package/build/watch/build-full.ts +124 -0
  25. package/build/watch/build-incremental.ts +139 -0
  26. package/build/watch/compiler-types.d.ts +19 -0
  27. package/build/watch/compiler.ts +112 -0
  28. package/build/watch/index.ts +22 -0
  29. package/build/watch/mutations.ts +75 -0
  30. package/build/watch/planner.ts +230 -0
  31. package/build/watch/records.ts +190 -0
  32. package/build/watch/runtime.ts +143 -0
  33. package/build/watch/snapshot.ts +602 -0
  34. package/build/watch/validation.ts +36 -0
  35. package/build/watch-reload-client.ts +10 -5
  36. package/init/content/labs/01/index.md +6 -0
  37. package/init/content/labs/01/trace_demo.py +16 -0
  38. package/package.json +47 -24
  39. package/python/command.ts +95 -0
  40. package/python/module.ts +18 -0
  41. package/src/_alerts.scss +11 -8
  42. package/src/_base.scss +25 -29
  43. package/src/_layout.scss +17 -14
  44. package/src/_mixins.scss +10 -9
  45. package/src/anchor/style.scss +4 -2
  46. package/src/code/index.ts +9 -2
  47. package/src/code/style.scss +14 -13
  48. package/src/code.scss +14 -13
  49. package/src/header/_base.scss +21 -24
  50. package/src/header/style.scss +16 -12
  51. package/src/index.ts +3 -0
  52. package/src/literate/style.scss +4 -4
  53. package/src/navigate/index.ts +43 -279
  54. package/src/navigate/lifecycle.ts +1 -1
  55. package/src/navigate/runtime.ts +341 -0
  56. package/src/page-update/README.md +4 -0
  57. package/src/page-update/index.ts +280 -0
  58. package/src/page-update/style.scss +53 -0
  59. package/src/print/style.scss +4 -4
  60. package/src/question/style.scss +10 -6
  61. package/src/search/index.ts +9 -12
  62. package/src/search/style.scss +44 -42
  63. package/src/style.scss +63 -58
  64. package/src/timezone/index.ts +13 -3
  65. package/src/timezone/style.scss +11 -5
  66. package/src/toc/style.scss +17 -16
  67. package/src/trace/style.scss +27 -27
  68. package/src/validators.ts +45 -0
  69. package/templates/_footer.html +1 -1
  70. package/templates/_theme.scss +8 -19
  71. package/watch/engine.ts +296 -0
  72. package/watch/events.ts +10 -0
  73. package/watch/fs-commit.ts +102 -0
  74. package/watch/types.d.ts +103 -0
  75. package/build/watch.ts +0 -493
@@ -0,0 +1,103 @@
1
+ export type WatchEventKind = 'add' | 'change' | 'unlink';
2
+
3
+ export interface FileChange {
4
+ path: string;
5
+ kind: WatchEventKind;
6
+ }
7
+
8
+ export interface ChangeBatch {
9
+ changes: FileChange[];
10
+ }
11
+
12
+ export interface WatchTarget {
13
+ path: string;
14
+ chokidar?: { depth?: number };
15
+ filter?: (filePath: string) => boolean;
16
+ }
17
+
18
+ export interface WatchDiagnostic {
19
+ message: string;
20
+ }
21
+
22
+ export interface WriteFileMutation {
23
+ kind: 'write';
24
+ path: string;
25
+ content: string | Buffer;
26
+ }
27
+
28
+ export interface DeleteFileMutation {
29
+ kind: 'delete';
30
+ path: string;
31
+ }
32
+
33
+ export type FileMutation = WriteFileMutation | DeleteFileMutation;
34
+
35
+ export interface ReplaceRootCommitPlan {
36
+ kind: 'replace-root';
37
+ stagedPath: string;
38
+ targetPath: string;
39
+ }
40
+
41
+ export interface ApplyMutationsCommitPlan {
42
+ kind: 'apply-mutations';
43
+ rootDir: string;
44
+ mutations: FileMutation[];
45
+ }
46
+
47
+ export type CommitPlan = ReplaceRootCommitPlan | ApplyMutationsCommitPlan;
48
+
49
+ export type CompilerPlanResult<Plan> =
50
+ | { kind: 'skip' }
51
+ | { kind: 'build'; plan: Plan };
52
+
53
+ export type CompilerBuildResult<Snapshot, Meta> =
54
+ | {
55
+ ok: true;
56
+ snapshot: Snapshot;
57
+ commit: CommitPlan;
58
+ diagnostics?: WatchDiagnostic[];
59
+ meta?: Meta;
60
+ }
61
+ | { ok: false; diagnostics: WatchDiagnostic[]; meta?: Meta };
62
+
63
+ export interface WatchCompiler<Snapshot, Plan, Meta> {
64
+ getWatchTargets(): WatchTarget[];
65
+ buildInitial(): Promise<CompilerBuildResult<Snapshot, Meta>>;
66
+ plan(
67
+ snapshot: Snapshot | undefined,
68
+ batch: ChangeBatch,
69
+ ): Promise<CompilerPlanResult<Plan>>;
70
+ run(
71
+ plan: Plan,
72
+ snapshot: Snapshot | undefined,
73
+ ): Promise<CompilerBuildResult<Snapshot, Meta>>;
74
+ }
75
+
76
+ export type WatchLifecycleEvent<Snapshot, Meta> =
77
+ | { kind: 'watching' }
78
+ | { kind: 'build-started'; batch?: ChangeBatch; initial: boolean }
79
+ | {
80
+ kind: 'build-succeeded';
81
+ initial: boolean;
82
+ batch?: ChangeBatch;
83
+ changed: boolean;
84
+ snapshot: Snapshot;
85
+ meta?: Meta;
86
+ diagnostics: WatchDiagnostic[];
87
+ }
88
+ | {
89
+ kind: 'build-failed';
90
+ initial: boolean;
91
+ batch?: ChangeBatch;
92
+ diagnostics: WatchDiagnostic[];
93
+ meta?: Meta;
94
+ }
95
+ | { kind: 'build-skipped'; batch: ChangeBatch };
96
+
97
+ export interface WatchEngineOptions<Snapshot, Plan, Meta> {
98
+ compiler: WatchCompiler<Snapshot, Plan, Meta>;
99
+ debounceMs?: number;
100
+ onEvent?: (
101
+ event: WatchLifecycleEvent<Snapshot, Meta>,
102
+ ) => void | Promise<void>;
103
+ }
package/build/watch.ts DELETED
@@ -1,493 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import chokidar from 'chokidar';
4
- import WebSocket, { WebSocketServer } from 'ws';
5
- import type { SiteVariables, WatchState, ContentRenderResult } from './types';
6
- import { B } from './colors';
7
- import { makeLogger, printFlair } from './log';
8
- import { getDevSiteVariables } from './site-variables';
9
- import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
10
- import {
11
- getContentDir,
12
- getPublicDir,
13
- getDistDir,
14
- getPackageDir,
15
- toPosix,
16
- } from './utils/paths';
17
- import { isFeatureEnabled } from './features';
18
- import { bundle } from './bundle';
19
- import { copyFonts } from './generate-fonts';
20
- import { copyKatexAssets } from './generate-katex-assets';
21
- import { generateFavicons } from './generate-favicon';
22
- import { generateWebAppManifest } from './generate-web-app-manifest';
23
- import {
24
- copyPublicFiles,
25
- copyContentAssets,
26
- copyContentFile,
27
- copyPublicFile,
28
- } from './copy';
29
- import { getProcessedExtensions } from './utils/file-types';
30
- import { ContentRenderer } from './generate-content-assets';
31
- import { WatchPagefindRunner } from './pagefind';
32
- import { ContentChangeDetector } from './content-watch';
33
- import { startServer } from './serve';
34
-
35
- export async function runWatch(options: {
36
- httpPort?: number;
37
- wsPort?: number;
38
- }): Promise<void> {
39
- const httpPort = options.httpPort;
40
- const WEBSOCKET_PORT = options.wsPort ?? 35729;
41
- const DEBOUNCE_MS = 300;
42
- const RELOAD_CLIENT_PATH = path.resolve(
43
- getPackageDir(),
44
- 'build/watch-reload-client.ts',
45
- );
46
-
47
- // Bundle the reload client separately to work around a Bun bundler bug where
48
- // side-effect-only JS entrypoints are tree-shaken when bundled alongside SCSS
49
- // entrypoints processed by a plugin.
50
- async function bundleReloadClient(): Promise<string[]> {
51
- const result = await Bun.build({
52
- entrypoints: [RELOAD_CLIENT_PATH],
53
- outdir: getDistDir(),
54
- naming: '[name].bundle.[ext]',
55
- sourcemap: 'inline',
56
- define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
57
- });
58
- return result.outputs.map(output =>
59
- toPosix(path.relative(getDistDir(), output.path)),
60
- );
61
- }
62
-
63
- type ChangeCategory = 'content' | 'public' | 'config';
64
-
65
- const log = makeLogger(import.meta.url);
66
- const wslog = makeLogger('WebSocket');
67
-
68
- // WebSocket server
69
-
70
- let webSocketsReady = false;
71
- let watcherReady = false;
72
- let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
73
- let serveStarted = false;
74
-
75
- let wss: WebSocketServer | null = null;
76
- try {
77
- wss = new WebSocketServer({ port: WEBSOCKET_PORT });
78
-
79
- wss.on('connection', (conn: WebSocket) => {
80
- wslog.debug`WebSocket client connected`;
81
- if (watcherReady && conn.readyState === WebSocket.OPEN) {
82
- if (initialBuildFailed) {
83
- conn.send('error');
84
- }
85
- conn.send('ready');
86
- }
87
- conn.on('close', () => {
88
- wslog.debug`WebSocket client disconnected`;
89
- });
90
- });
91
-
92
- wss.on('error', (err: Error) => {
93
- wslog.error`WebSocket server error: ${err.message}`;
94
- });
95
-
96
- wss.on('listening', () => {
97
- wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
98
- webSocketsReady = true;
99
- });
100
- } catch (err) {
101
- wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${(err as Error).message}`;
102
- }
103
-
104
- function broadcast(msg: string): void {
105
- if (wss == null || !webSocketsReady) {
106
- return;
107
- }
108
- wslog.debug`Broadcasting "${msg}" to WebSocket clients`;
109
- wss.clients.forEach((client: WebSocket) => {
110
- if (client.readyState === WebSocket.OPEN) {
111
- client.send(msg);
112
- }
113
- });
114
- }
115
-
116
- // Dev server
117
-
118
- function serve(): void {
119
- webServerTimeout = setTimeout(() => {
120
- log.error`Web server failed to report within 10 seconds, exiting`;
121
- process.exit(3);
122
- }, 10000);
123
-
124
- startServer({
125
- port: httpPort,
126
- distDir,
127
- onReady: () => {
128
- clearTimeout(webServerTimeout);
129
- },
130
- });
131
- }
132
-
133
- // Path helpers
134
-
135
- function toContentMarkdownPath(filePath: string): string | null {
136
- const ext = path.extname(filePath).toLowerCase();
137
- if (!['.md', '.markdown'].includes(ext)) {
138
- return null;
139
- }
140
-
141
- const normalizedContentDir = path.resolve(contentDir) + path.sep;
142
- const normalizedFilePath = path.resolve(filePath);
143
- if (!normalizedFilePath.startsWith(normalizedContentDir)) {
144
- return null;
145
- }
146
-
147
- return toPosix(path.relative(contentDir, normalizedFilePath));
148
- }
149
-
150
- function toPublicRelativePath(filePath: string): string | null {
151
- const normalizedPublicDir = path.resolve(publicDir) + path.sep;
152
- const normalizedFilePath = path.resolve(filePath);
153
- if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
154
- return null;
155
- }
156
- return toPosix(path.relative(publicDir, normalizedFilePath));
157
- }
158
-
159
- // Watch mode
160
-
161
- const contentDir: string = getContentDir();
162
- const publicDir: string = getPublicDir();
163
- const distDir: string = getDistDir();
164
-
165
- let siteVariables: SiteVariables;
166
- let processedExtSet = new Set<string>();
167
- let publicRelPaths: Set<string> = new Set();
168
- let contentAssetRelPaths: Set<string> = new Set();
169
- let contentRenderer: ContentRenderer;
170
- let changeDetector: ContentChangeDetector;
171
- let pagefindRunner: WatchPagefindRunner | undefined;
172
- let assetFiles: string[] = [];
173
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
174
- const pendingChanges = new Set<string>();
175
- let rebuilding = false;
176
-
177
- function onBuildSuccess(result?: ContentRenderResult): void {
178
- printFlair();
179
- if (!serveStarted) {
180
- serveStarted = true;
181
- serve();
182
- }
183
- broadcast('reload');
184
- if (pagefindRunner && result) {
185
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
186
- setImmediate(() => pagefindRunner!.run());
187
- }
188
- }
189
-
190
- function copyChangedPublicFiles(changes: Set<string>): void {
191
- for (const filePath of changes) {
192
- if (classifyChange(filePath) !== 'public') {
193
- continue;
194
- }
195
- const absPath = path.resolve(filePath);
196
- if (fs.existsSync(absPath)) {
197
- copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
198
- const rel = toPosix(path.relative(publicDir, absPath));
199
- publicRelPaths.add(rel);
200
- }
201
- }
202
- }
203
-
204
- async function buildFromConfig(
205
- copyAssets: boolean,
206
- ): Promise<ContentRenderResult> {
207
- processedExtSet = new Set(
208
- getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
209
- );
210
- contentRenderer = new ContentRenderer(siteVariables);
211
- changeDetector = new ContentChangeDetector(siteVariables);
212
-
213
- if (isFeatureEnabled(siteVariables, 'search')) {
214
- pagefindRunner = new WatchPagefindRunner(siteVariables);
215
- }
216
-
217
- compileTemplates(siteVariables);
218
- await contentRenderer.initHighlighter();
219
-
220
- assetFiles = [
221
- ...(await bundle(siteVariables, { mode: 'development' })),
222
- ...(await bundleReloadClient()),
223
- ];
224
-
225
- copyFonts(distDir);
226
- copyKatexAssets(distDir);
227
-
228
- if (isFeatureEnabled(siteVariables, 'favicon')) {
229
- await generateFavicons(siteVariables, distDir);
230
- generateWebAppManifest(siteVariables, distDir);
231
- }
232
-
233
- if (copyAssets) {
234
- publicRelPaths = copyPublicFiles(publicDir, distDir);
235
- contentAssetRelPaths = copyContentAssets(
236
- contentDir,
237
- distDir,
238
- [...processedExtSet],
239
- publicRelPaths,
240
- );
241
- }
242
-
243
- return contentRenderer.processContent({ distDir, assetFiles });
244
- }
245
-
246
- async function initialBuild(): Promise<void> {
247
- siteVariables = getDevSiteVariables();
248
- fs.mkdirSync(distDir, { recursive: true });
249
-
250
- const result = await buildFromConfig(true);
251
-
252
- for (const err of result.errors) {
253
- log.error`${err.message}`;
254
- }
255
-
256
- if (result.errors.length === 0) {
257
- printFlair();
258
- if (pagefindRunner) {
259
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
260
- setImmediate(() => pagefindRunner!.run());
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 jsonDataDir = getJsonDataDir();
274
- const siteConfigPath = path.resolve('site.dev.json');
275
-
276
- if (resolved.startsWith(resolvedContentDir)) {
277
- return 'content';
278
- }
279
- if (resolved.startsWith(resolvedPublicDir)) {
280
- return 'public';
281
- }
282
- if (
283
- resolved === siteConfigPath ||
284
- JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
285
- ) {
286
- return 'config';
287
- }
288
- return null;
289
- }
290
-
291
- async function rebuild(): Promise<void> {
292
- if (rebuilding) {
293
- return;
294
- }
295
- rebuilding = true;
296
-
297
- const changes = new Set(pendingChanges);
298
- pendingChanges.clear();
299
-
300
- broadcast('rebuilding');
301
-
302
- // Classify changes
303
- const categories = new Set<ChangeCategory>();
304
- for (const filePath of changes) {
305
- const category = classifyChange(filePath);
306
- if (category) {
307
- categories.add(category);
308
- }
309
- }
310
-
311
- let succeeded = false;
312
-
313
- try {
314
- // Site config changed, full restart
315
- if (categories.has('config')) {
316
- log.event`Site config changed, restarting`;
317
- siteVariables = getDevSiteVariables();
318
-
319
- const result = await buildFromConfig(false);
320
- for (const err of result.errors) {
321
- log.error`${err.message}`;
322
- }
323
- if (result.errors.length === 0) {
324
- onBuildSuccess(result);
325
- succeeded = true;
326
- }
327
- return;
328
- }
329
-
330
- // Public file changed, copy just that file
331
- if (categories.has('public') && !categories.has('content')) {
332
- copyChangedPublicFiles(changes);
333
- onBuildSuccess();
334
- succeeded = true;
335
- return;
336
- }
337
-
338
- // Content changed, incremental rebuild
339
- const detection = changeDetector.detectChanges(changes);
340
-
341
- if (detection.needsRestart) {
342
- log.event`Content structure changed, full rebuild`;
343
- contentRenderer = new ContentRenderer(siteVariables);
344
- await contentRenderer.initHighlighter();
345
- }
346
-
347
- // Log changed content files
348
- for (const filePath of changes) {
349
- const markdownPath = toContentMarkdownPath(filePath);
350
- if (markdownPath) {
351
- log.event`${B`${markdownPath}`} changed, rebuilding`;
352
- } else {
353
- const pubPath = toPublicRelativePath(filePath);
354
- if (pubPath) {
355
- log.event`${B`public/${pubPath}`} changed`;
356
- }
357
- }
358
- }
359
-
360
- // Copy any changed public files too
361
- if (categories.has('public')) {
362
- copyChangedPublicFiles(changes);
363
- }
364
-
365
- // Copy any changed non-processed content files (images, PDFs, etc.)
366
- if (categories.has('content')) {
367
- for (const filePath of changes) {
368
- if (classifyChange(filePath) !== 'content') {
369
- continue;
370
- }
371
- const absPath = path.resolve(filePath);
372
- const ext = path.extname(absPath).slice(1).toLowerCase();
373
- if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
374
- copyContentFile(contentDir, distDir, absPath, publicRelPaths);
375
- const rel = toPosix(path.relative(contentDir, absPath));
376
- contentAssetRelPaths.add(rel);
377
- }
378
- }
379
- }
380
-
381
- const watchState: WatchState | undefined = detection.needsRestart
382
- ? undefined
383
- : {
384
- changedContentFiles: detection.changedContentFiles,
385
- jsonDataChanged: detection.jsonDataChanged,
386
- partialsChanged: detection.partialsChanged,
387
- };
388
-
389
- const result = contentRenderer.processContent({
390
- distDir,
391
- assetFiles,
392
- watchState,
393
- });
394
-
395
- for (const err of result.errors) {
396
- log.error`${err.message}`;
397
- }
398
-
399
- if (result.errors.length === 0) {
400
- onBuildSuccess(result);
401
- succeeded = true;
402
- }
403
- } catch (err) {
404
- log.error`Build failed: ${(err as Error).message}`;
405
- } finally {
406
- if (!succeeded) {
407
- broadcast('error');
408
- }
409
- rebuilding = false;
410
-
411
- // If more changes accumulated during rebuild, schedule another
412
- if (pendingChanges.size > 0) {
413
- scheduleRebuild();
414
- }
415
- }
416
- }
417
-
418
- function scheduleRebuild(): void {
419
- if (debounceTimer) {
420
- clearTimeout(debounceTimer);
421
- }
422
- debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
423
- }
424
-
425
- function onFileChange(filePath: string): void {
426
- pendingChanges.add(filePath);
427
- scheduleRebuild();
428
- }
429
-
430
- // Start
431
-
432
- let initialBuildFailed = false;
433
-
434
- initialBuild()
435
- .catch(err => {
436
- log.error`Initial build failed: ${(err as Error).message}`;
437
- initialBuildFailed = true;
438
- })
439
- .finally(() => {
440
- const watchPaths: string[] = [contentDir, publicDir];
441
-
442
- // Watch config files (nav.json, authors.json, site config) via the
443
- // project root directory (depth 0). Chokidar v4 doesn't emit unlink
444
- // for individually watched files, so we must watch the directory.
445
- const jsonDataDir = getJsonDataDir();
446
- const configFilePaths = new Set([
447
- ...JSON_DATA_FILES.map(f => path.resolve(jsonDataDir, f)),
448
- path.resolve('site.dev.json'),
449
- ]);
450
- const configWatcher = chokidar.watch(jsonDataDir, {
451
- ignoreInitial: true,
452
- depth: 0,
453
- awaitWriteFinish: { stabilityThreshold: 100 },
454
- });
455
- const onConfigFileChange = (filePath: string) => {
456
- if (configFilePaths.has(path.resolve(filePath))) {
457
- onFileChange(filePath);
458
- }
459
- };
460
- configWatcher.on('add', onConfigFileChange);
461
- configWatcher.on('change', onConfigFileChange);
462
- configWatcher.on('unlink', onConfigFileChange);
463
-
464
- const watcher = chokidar.watch(watchPaths, {
465
- ignoreInitial: true,
466
- awaitWriteFinish: { stabilityThreshold: 100 },
467
- });
468
-
469
- watcher.on('change', onFileChange);
470
- watcher.on('add', onFileChange);
471
- watcher.on('unlink', onFileChange);
472
-
473
- let watchersReady = 0;
474
-
475
- function onWatcherReady(): void {
476
- if (++watchersReady < 2) {
477
- return;
478
- }
479
- watcherReady = true;
480
- if (initialBuildFailed) {
481
- broadcast('error');
482
- }
483
- broadcast('ready');
484
- log.info`Watching for changes...`;
485
- }
486
-
487
- configWatcher.on('ready', onWatcherReady);
488
- watcher.on('ready', onWatcherReady);
489
- });
490
-
491
- // Return a promise that never resolves (watch mode runs until killed)
492
- return new Promise(() => {});
493
- }