@abreen/tada 1.8.1 → 1.10.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 (79) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/bin/validators.ts +1 -1
  4. package/build/apply-base-path-plugin.ts +79 -17
  5. package/build/bundle.ts +9 -1
  6. package/build/content-watch.ts +3 -3
  7. package/build/copy.ts +30 -0
  8. package/build/generate-content-assets.ts +45 -59
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/templates.ts +0 -2
  12. package/build/types.d.ts +19 -1
  13. package/build/util.ts +3 -0
  14. package/build/utils/content-files.ts +98 -0
  15. package/build/utils/include.ts +4 -0
  16. package/build/utils/python-runner/trace_runner.py +272 -0
  17. package/build/utils/render.ts +47 -15
  18. package/build/utils/source-template.ts +27 -0
  19. package/build/utils/trace-core.ts +203 -0
  20. package/build/utils/trace-java.ts +132 -0
  21. package/build/utils/trace-layout.ts +31 -3
  22. package/build/utils/trace-python.ts +26 -0
  23. package/build/utils/trace.ts +174 -294
  24. package/build/validate-internal-links-plugin.ts +16 -4
  25. package/build/watch/assets.ts +111 -0
  26. package/build/watch/build-full.ts +124 -0
  27. package/build/watch/build-incremental.ts +139 -0
  28. package/build/watch/compiler-types.d.ts +19 -0
  29. package/build/watch/compiler.ts +112 -0
  30. package/build/watch/index.ts +22 -0
  31. package/build/watch/mutations.ts +75 -0
  32. package/build/watch/planner.ts +230 -0
  33. package/build/watch/records.ts +190 -0
  34. package/build/watch/runtime.ts +143 -0
  35. package/build/watch/snapshot.ts +602 -0
  36. package/build/watch/validation.ts +36 -0
  37. package/build/watch-reload-client.ts +10 -5
  38. package/init/content/labs/01/index.md +6 -0
  39. package/init/content/labs/01/trace_demo.py +16 -0
  40. package/init/content/lectures/01/Rectangle.java +2 -0
  41. package/init/content/lectures/01/index.md +6 -0
  42. package/init/content/lectures/01/rectangle.py +5 -0
  43. package/package.json +42 -23
  44. package/python/command.ts +95 -0
  45. package/python/module.ts +18 -0
  46. package/src/_alerts.scss +11 -8
  47. package/src/_base.scss +25 -29
  48. package/src/_layout.scss +17 -14
  49. package/src/_mixins.scss +10 -9
  50. package/src/anchor/style.scss +4 -2
  51. package/src/code/index.ts +9 -2
  52. package/src/code/style.scss +14 -13
  53. package/src/code.scss +14 -13
  54. package/src/header/_base.scss +21 -24
  55. package/src/header/style.scss +16 -12
  56. package/src/index.ts +3 -0
  57. package/src/literate/style.scss +4 -4
  58. package/src/navigate/index.ts +43 -279
  59. package/src/navigate/lifecycle.ts +1 -1
  60. package/src/navigate/runtime.ts +341 -0
  61. package/src/page-update/README.md +4 -0
  62. package/src/page-update/index.ts +280 -0
  63. package/src/page-update/style.scss +53 -0
  64. package/src/print/style.scss +4 -4
  65. package/src/question/style.scss +10 -6
  66. package/src/search/index.ts +9 -12
  67. package/src/search/style.scss +44 -42
  68. package/src/style.scss +63 -58
  69. package/src/timezone/style.scss +5 -5
  70. package/src/toc/style.scss +17 -16
  71. package/src/trace/style.scss +27 -27
  72. package/src/validators.ts +45 -0
  73. package/templates/_footer.html +1 -1
  74. package/templates/_theme.scss +8 -19
  75. package/watch/engine.ts +296 -0
  76. package/watch/events.ts +10 -0
  77. package/watch/fs-commit.ts +102 -0
  78. package/watch/types.d.ts +103 -0
  79. package/build/watch.ts +0 -493
@@ -0,0 +1,230 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getJsonDataDir } from '../templates';
4
+ import type { ChangeBatch } from '../../watch/types';
5
+ import type { TadaProjectScan, TadaSnapshot } from './snapshot';
6
+
7
+ export interface TadaWatchPlan {
8
+ kind: 'full' | 'incremental';
9
+ scan: TadaProjectScan;
10
+ contentToRender: Set<string>;
11
+ publicToRender: Set<string>;
12
+ contentToRemove: Set<string>;
13
+ publicToRemove: Set<string>;
14
+ }
15
+
16
+ function readJsonFile(filePath: string): unknown {
17
+ if (!fs.existsSync(filePath)) {
18
+ return undefined;
19
+ }
20
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
21
+ }
22
+
23
+ export function diffAuthorKeys(previous: unknown, next: unknown): Set<string> {
24
+ const previousMap =
25
+ previous && typeof previous === 'object'
26
+ ? (previous as Record<string, unknown>)
27
+ : {};
28
+ const nextMap =
29
+ next && typeof next === 'object' ? (next as Record<string, unknown>) : {};
30
+ const keys = new Set([...Object.keys(previousMap), ...Object.keys(nextMap)]);
31
+ const changed = new Set<string>();
32
+
33
+ for (const key of keys) {
34
+ if (JSON.stringify(previousMap[key]) !== JSON.stringify(nextMap[key])) {
35
+ changed.add(key);
36
+ }
37
+ }
38
+
39
+ return changed;
40
+ }
41
+
42
+ function addDependents(
43
+ destination: Set<string>,
44
+ reverseMap: Map<string, Set<string>>,
45
+ key: string,
46
+ ): void {
47
+ const dependents = reverseMap.get(key);
48
+ if (!dependents) {
49
+ return;
50
+ }
51
+ for (const dependent of dependents) {
52
+ destination.add(dependent);
53
+ }
54
+ }
55
+
56
+ function sourceExists(filePath: string, scan: TadaProjectScan): boolean {
57
+ return scan.contentFiles.has(filePath) || scan.publicFiles.has(filePath);
58
+ }
59
+
60
+ export function createTadaWatchPlan({
61
+ snapshot,
62
+ batch,
63
+ scan,
64
+ }: {
65
+ snapshot: TadaSnapshot | undefined;
66
+ batch: ChangeBatch;
67
+ scan: TadaProjectScan;
68
+ }): TadaWatchPlan {
69
+ const contentToRender = new Set<string>();
70
+ const publicToRender = new Set<string>();
71
+ const contentToRemove = new Set<string>();
72
+ const publicToRemove = new Set<string>();
73
+ const jsonDataDir = getJsonDataDir();
74
+ const siteConfigPath = path.resolve('site.dev.json');
75
+ const navPath = path.resolve(jsonDataDir, 'nav.json');
76
+ const authorsPath = path.resolve(jsonDataDir, 'authors.json');
77
+
78
+ if (!snapshot) {
79
+ return {
80
+ kind: 'full',
81
+ scan,
82
+ contentToRender,
83
+ publicToRender,
84
+ contentToRemove,
85
+ publicToRemove,
86
+ };
87
+ }
88
+
89
+ for (const change of batch.changes) {
90
+ const resolvedPath = path.resolve(change.path);
91
+ if (resolvedPath === siteConfigPath || resolvedPath === navPath) {
92
+ return {
93
+ kind: 'full',
94
+ scan,
95
+ contentToRender,
96
+ publicToRender,
97
+ contentToRemove,
98
+ publicToRemove,
99
+ };
100
+ }
101
+ }
102
+
103
+ for (const change of batch.changes) {
104
+ const resolvedPath = path.resolve(change.path);
105
+ if (resolvedPath === authorsPath) {
106
+ const nextAuthorsData = readJsonFile(authorsPath);
107
+ if (snapshot.authorsData === undefined || nextAuthorsData === undefined) {
108
+ return {
109
+ kind: 'full',
110
+ scan,
111
+ contentToRender,
112
+ publicToRender,
113
+ contentToRemove,
114
+ publicToRemove,
115
+ };
116
+ }
117
+ const changedAuthorKeys = diffAuthorKeys(
118
+ snapshot.authorsData,
119
+ nextAuthorsData,
120
+ );
121
+ for (const key of changedAuthorKeys) {
122
+ addDependents(contentToRender, snapshot.reverseAuthorDeps, key);
123
+ }
124
+ continue;
125
+ }
126
+
127
+ if (scan.contentFiles.has(resolvedPath)) {
128
+ contentToRender.add(resolvedPath);
129
+ } else if (snapshot.contentRecords.has(resolvedPath)) {
130
+ contentToRemove.add(resolvedPath);
131
+ const previousRecord = snapshot.contentRecords.get(resolvedPath)!;
132
+ for (const partialPath of previousRecord.partialDeps) {
133
+ addDependents(
134
+ contentToRender,
135
+ snapshot.reversePartialDeps,
136
+ partialPath,
137
+ );
138
+ }
139
+ for (const tracePath of previousRecord.traceDeps) {
140
+ addDependents(contentToRender, snapshot.reverseTraceDeps, tracePath);
141
+ }
142
+ }
143
+
144
+ if (scan.publicFiles.has(resolvedPath)) {
145
+ publicToRender.add(resolvedPath);
146
+ } else if (snapshot.publicRecords.has(resolvedPath)) {
147
+ publicToRemove.add(resolvedPath);
148
+ }
149
+
150
+ addDependents(contentToRender, snapshot.reversePartialDeps, resolvedPath);
151
+ addDependents(contentToRender, snapshot.reverseTraceDeps, resolvedPath);
152
+ }
153
+
154
+ const changedTargets = new Set<string>();
155
+ const removedTargets = new Set<string>();
156
+ for (const target of snapshot.validTargets) {
157
+ if (!scan.validTargets.has(target)) {
158
+ changedTargets.add(target);
159
+ removedTargets.add(target);
160
+ }
161
+ }
162
+ for (const target of scan.validTargets) {
163
+ if (!snapshot.validTargets.has(target)) {
164
+ changedTargets.add(target);
165
+ }
166
+ }
167
+ for (const target of changedTargets) {
168
+ addDependents(contentToRender, snapshot.reverseInternalTargetDeps, target);
169
+ }
170
+
171
+ if (removedTargets.size > 0 && contentToRender.size === 0) {
172
+ const rootOwner = snapshot.outputOwners.get('index.html');
173
+ if (rootOwner?.kind === 'content') {
174
+ contentToRender.add(rootOwner.sourcePath);
175
+ }
176
+ }
177
+
178
+ for (const [outputPath, owner] of snapshot.outputOwners) {
179
+ const nextContentOwner = scan.contentOwners.get(outputPath);
180
+ const nextPublicOwner = scan.publicOwners.get(outputPath);
181
+ const nextKind = nextPublicOwner
182
+ ? 'public'
183
+ : nextContentOwner
184
+ ? 'content'
185
+ : null;
186
+ const nextSourcePath = nextPublicOwner || nextContentOwner;
187
+
188
+ if (!nextKind || !nextSourcePath) {
189
+ continue;
190
+ }
191
+ if (owner.kind !== nextKind || owner.sourcePath !== nextSourcePath) {
192
+ if (nextKind === 'content') {
193
+ contentToRender.add(nextSourcePath);
194
+ } else {
195
+ publicToRender.add(nextSourcePath);
196
+ }
197
+ }
198
+ }
199
+
200
+ for (const [outputPath, nextSourcePath] of scan.contentOwners) {
201
+ if (!snapshot.outputOwners.has(outputPath)) {
202
+ contentToRender.add(nextSourcePath);
203
+ }
204
+ }
205
+ for (const [outputPath, nextSourcePath] of scan.publicOwners) {
206
+ if (!snapshot.outputOwners.has(outputPath)) {
207
+ publicToRender.add(nextSourcePath);
208
+ }
209
+ }
210
+
211
+ for (const sourcePath of [...contentToRender]) {
212
+ if (!sourceExists(sourcePath, scan)) {
213
+ contentToRender.delete(sourcePath);
214
+ }
215
+ }
216
+ for (const sourcePath of [...publicToRender]) {
217
+ if (!sourceExists(sourcePath, scan)) {
218
+ publicToRender.delete(sourcePath);
219
+ }
220
+ }
221
+
222
+ return {
223
+ kind: 'incremental',
224
+ scan,
225
+ contentToRender,
226
+ publicToRender,
227
+ contentToRemove,
228
+ publicToRemove,
229
+ };
230
+ }
@@ -0,0 +1,190 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ renderCodePageAsset,
5
+ renderCopiedContentAsset,
6
+ renderLiterateJavaPageAsset,
7
+ renderPlainTextPageAsset,
8
+ toPosix,
9
+ } from '../util';
10
+ import { extensionIsMarkdown, isLiterateJava } from '../utils/file-types';
11
+ import type {
12
+ Asset,
13
+ RenderDependencyCollector,
14
+ SiteVariables,
15
+ TraceToolAvailability,
16
+ } from '../types';
17
+ import {
18
+ collectSourceOutputs,
19
+ type TadaProjectScan,
20
+ type TadaSourceRecord,
21
+ } from './snapshot';
22
+ import type { TraceCache } from './compiler-types';
23
+ import { writeAssets } from './assets';
24
+
25
+ function createDependencyCollector(): {
26
+ collector: RenderDependencyCollector;
27
+ partials: Set<string>;
28
+ traceFiles: Set<string>;
29
+ internalTargets: Set<string>;
30
+ generatedOutputPaths: Set<string>;
31
+ authorKey: string | undefined;
32
+ } {
33
+ const partials = new Set<string>();
34
+ const traceFiles = new Set<string>();
35
+ const internalTargets = new Set<string>();
36
+ const generatedOutputPaths = new Set<string>();
37
+ let authorKey: string | undefined;
38
+
39
+ return {
40
+ collector: {
41
+ partials,
42
+ traceFiles,
43
+ internalTargets,
44
+ generatedOutputPaths,
45
+ setAuthorKey(value: string) {
46
+ authorKey = value;
47
+ },
48
+ },
49
+ partials,
50
+ traceFiles,
51
+ internalTargets,
52
+ generatedOutputPaths,
53
+ get authorKey() {
54
+ return authorKey;
55
+ },
56
+ };
57
+ }
58
+
59
+ function createRawContentAsset(filePath: string, contentDir: string): Asset[] {
60
+ return [
61
+ {
62
+ assetPath: toPosix(path.relative(contentDir, filePath)),
63
+ content: fs.readFileSync(filePath),
64
+ },
65
+ ];
66
+ }
67
+
68
+ export function renderContentRecord({
69
+ filePath,
70
+ siteVariables,
71
+ scan,
72
+ assetFiles,
73
+ outputDir,
74
+ traceCache,
75
+ traceToolAvailability,
76
+ cachedTraceSourceDir,
77
+ persistOutputs = true,
78
+ }: {
79
+ filePath: string;
80
+ siteVariables: SiteVariables;
81
+ scan: TadaProjectScan;
82
+ assetFiles: string[];
83
+ outputDir: string;
84
+ traceCache: TraceCache;
85
+ traceToolAvailability: TraceToolAvailability;
86
+ cachedTraceSourceDir?: string;
87
+ persistOutputs?: boolean;
88
+ }): TadaSourceRecord {
89
+ const deps = createDependencyCollector();
90
+ const codeExtensions = Object.keys(siteVariables.codeLanguages || {}).map(
91
+ ext => ext.toLowerCase(),
92
+ );
93
+ const ext = path.extname(filePath).slice(1).toLowerCase();
94
+ const assets: Asset[] = [];
95
+ const codeEnabled = siteVariables.features.code !== false;
96
+
97
+ if (isLiterateJava(filePath)) {
98
+ assets.push(
99
+ ...renderLiterateJavaPageAsset({
100
+ filePath,
101
+ contentDir: scan.contentDir,
102
+ distDir: outputDir,
103
+ siteVariables,
104
+ assetFiles,
105
+ validInternalTargets: scan.validTargets,
106
+ literateJavaOutputPaths: scan.literateJavaOutputPaths,
107
+ dependencyCollector: deps.collector,
108
+ }),
109
+ );
110
+ } else if (scan.buildContentFiles.has(filePath)) {
111
+ if (
112
+ extensionIsMarkdown(path.extname(filePath).toLowerCase()) ||
113
+ path.extname(filePath).toLowerCase() === '.html'
114
+ ) {
115
+ assets.push(
116
+ ...renderPlainTextPageAsset({
117
+ filePath,
118
+ contentDir: scan.contentDir,
119
+ distDir: outputDir,
120
+ siteVariables,
121
+ validInternalTargets: scan.validTargets,
122
+ assetFiles,
123
+ literateJavaOutputPaths: scan.literateJavaOutputPaths,
124
+ dependencyCollector: deps.collector,
125
+ cachedTraceSourceDir,
126
+ traceCache,
127
+ traceToolAvailability,
128
+ }),
129
+ );
130
+ } else if (codeExtensions.includes(ext)) {
131
+ if (codeEnabled) {
132
+ assets.push(
133
+ ...renderCodePageAsset({
134
+ filePath,
135
+ contentDir: scan.contentDir,
136
+ distDir: outputDir,
137
+ siteVariables,
138
+ assetFiles,
139
+ dependencyCollector: deps.collector,
140
+ }),
141
+ );
142
+ }
143
+ assets.push(
144
+ ...renderCopiedContentAsset({
145
+ filePath,
146
+ contentDir: scan.contentDir,
147
+ siteVariables,
148
+ }),
149
+ );
150
+ }
151
+ } else if (!scan.processedExts.has(ext)) {
152
+ assets.push(...createRawContentAsset(filePath, scan.contentDir));
153
+ }
154
+
155
+ const outputs = collectSourceOutputs(
156
+ assets,
157
+ deps.generatedOutputPaths,
158
+ outputDir,
159
+ );
160
+ if (persistOutputs) {
161
+ writeAssets(outputDir, outputs);
162
+ }
163
+
164
+ return {
165
+ sourcePath: filePath,
166
+ kind: 'content',
167
+ outputs,
168
+ partialDeps: deps.partials,
169
+ traceDeps: deps.traceFiles,
170
+ internalTargets: deps.internalTargets,
171
+ generatedOutputPaths: deps.generatedOutputPaths,
172
+ authorKey: deps.authorKey,
173
+ };
174
+ }
175
+
176
+ export function createPublicRecord(
177
+ filePath: string,
178
+ publicDir: string,
179
+ ): TadaSourceRecord {
180
+ const relPath = toPosix(path.relative(publicDir, filePath));
181
+ return {
182
+ sourcePath: filePath,
183
+ kind: 'public',
184
+ outputs: new Map([[relPath, fs.readFileSync(filePath)]]),
185
+ partialDeps: new Set(),
186
+ traceDeps: new Set(),
187
+ internalTargets: new Set(),
188
+ generatedOutputPaths: new Set(),
189
+ };
190
+ }
@@ -0,0 +1,143 @@
1
+ import path from 'path';
2
+ import WebSocket, { WebSocketServer } from 'ws';
3
+ import { B } from '../colors';
4
+ import { makeLogger, printFlair } from '../log';
5
+ import { startServer } from '../serve';
6
+ import { WatchPagefindRunner } from '../pagefind';
7
+ import type { TadaBuildMeta } from './compiler-types';
8
+ import type { TadaSnapshot } from './snapshot';
9
+ import type { WatchLifecycleEvent } from '../../watch/types';
10
+
11
+ const log = makeLogger(import.meta.url);
12
+ const wslog = makeLogger('WebSocket');
13
+
14
+ function describeChange(kind: 'add' | 'change' | 'unlink'): string {
15
+ if (kind === 'add') {
16
+ return 'added, rebuilding';
17
+ }
18
+ if (kind === 'unlink') {
19
+ return 'removed, rebuilding';
20
+ }
21
+ return 'changed, rebuilding';
22
+ }
23
+
24
+ export class TadaWatchRuntime {
25
+ private wsPort: number;
26
+ private httpPort: number | undefined;
27
+ private distDir: string;
28
+ private wss: WebSocketServer | null;
29
+ private lastBuildFailed: boolean;
30
+ private serveStarted: boolean;
31
+ private pagefindRunner: WatchPagefindRunner | undefined;
32
+ private pagefindSiteVariablesKey: string | undefined;
33
+
34
+ constructor({
35
+ wsPort,
36
+ httpPort,
37
+ distDir,
38
+ }: {
39
+ wsPort: number;
40
+ httpPort?: number;
41
+ distDir: string;
42
+ }) {
43
+ this.wsPort = wsPort;
44
+ this.httpPort = httpPort;
45
+ this.distDir = distDir;
46
+ this.lastBuildFailed = false;
47
+ this.serveStarted = false;
48
+ this.pagefindSiteVariablesKey = undefined;
49
+ this.wss = new WebSocketServer({ port: wsPort });
50
+
51
+ this.wss.on('connection', conn => {
52
+ wslog.debug`WebSocket client connected`;
53
+ conn.on('close', () => {
54
+ wslog.debug`WebSocket client disconnected`;
55
+ });
56
+ });
57
+
58
+ this.wss.on('listening', () => {
59
+ wslog.debug`WebSocket server listening at ws://localhost:${this.wsPort}`;
60
+ });
61
+
62
+ this.wss.on('error', err => {
63
+ wslog.error`WebSocket server error: ${err.message}`;
64
+ });
65
+ }
66
+
67
+ private broadcast(message: string): void {
68
+ if (!this.wss) {
69
+ return;
70
+ }
71
+ for (const client of this.wss.clients) {
72
+ if (client.readyState === WebSocket.OPEN) {
73
+ client.send(message);
74
+ }
75
+ }
76
+ }
77
+
78
+ private ensureServerStarted(): void {
79
+ if (this.serveStarted) {
80
+ return;
81
+ }
82
+ this.serveStarted = true;
83
+ startServer({ port: this.httpPort, distDir: this.distDir });
84
+ }
85
+
86
+ async onEvent(
87
+ event: WatchLifecycleEvent<TadaSnapshot, TadaBuildMeta>,
88
+ ): Promise<void> {
89
+ switch (event.kind) {
90
+ case 'build-started':
91
+ if (!event.initial) {
92
+ for (const change of event.batch?.changes || []) {
93
+ log.event`${B`${path.basename(change.path)}`} ${describeChange(change.kind)}`;
94
+ }
95
+ this.broadcast('rebuilding');
96
+ }
97
+ return;
98
+ case 'build-succeeded': {
99
+ const recoveredFromFailure = this.lastBuildFailed;
100
+ this.lastBuildFailed = false;
101
+ printFlair();
102
+ this.ensureServerStarted();
103
+ if (!event.initial && (event.changed || recoveredFromFailure)) {
104
+ this.broadcast('reload');
105
+ }
106
+ if (event.meta && event.changed) {
107
+ if (event.meta.siteVariables.features.search !== false) {
108
+ const siteVariablesKey = JSON.stringify(event.meta.siteVariables);
109
+ if (
110
+ !this.pagefindRunner ||
111
+ this.pagefindSiteVariablesKey !== siteVariablesKey
112
+ ) {
113
+ this.pagefindRunner = new WatchPagefindRunner(
114
+ event.meta.siteVariables,
115
+ );
116
+ this.pagefindSiteVariablesKey = siteVariablesKey;
117
+ }
118
+ this.pagefindRunner.update(
119
+ this.distDir,
120
+ event.meta.htmlAssetsByPath,
121
+ );
122
+ setImmediate(() => this.pagefindRunner!.run());
123
+ } else {
124
+ this.pagefindRunner = undefined;
125
+ this.pagefindSiteVariablesKey = undefined;
126
+ }
127
+ }
128
+ return;
129
+ }
130
+ case 'build-failed':
131
+ this.lastBuildFailed = true;
132
+ for (const diagnostic of event.diagnostics) {
133
+ log.error`${diagnostic.message}`;
134
+ }
135
+ return;
136
+ case 'watching':
137
+ log.info`Watching for changes...`;
138
+ return;
139
+ case 'build-skipped':
140
+ return;
141
+ }
142
+ }
143
+ }