@abreen/tada 1.4.1 → 1.5.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.
- package/README.md +4 -2
- package/bin/tada.ts +81 -57
- package/build/apply-base-path-plugin.ts +22 -6
- package/build/code.test.ts +108 -1
- package/build/colors.test.ts +66 -0
- package/build/columns-plugin.ts +116 -0
- package/build/content-watch.ts +30 -21
- package/build/copy.ts +1 -1
- package/build/external-links-plugin.ts +1 -1
- package/build/generate-content-assets.ts +46 -4
- package/build/generate-favicon.ts +1 -1
- package/build/generate-fonts.ts +1 -1
- package/build/generate-katex-assets.ts +1 -1
- package/build/log.test.ts +7 -1
- package/build/log.ts +4 -1
- package/build/markdown-plugins.test.ts +65 -3
- package/build/pagefind.ts +1 -1
- package/build/pipeline.ts +4 -20
- package/build/serve.test.ts +59 -0
- package/build/serve.ts +29 -63
- package/build/templates.ts +2 -2
- package/build/toc-plugin.test.ts +40 -25
- package/build/toc-plugin.ts +9 -2
- package/build/{types.ts → types.d.ts} +87 -1
- package/build/util.ts +1 -1
- package/build/utils/code.ts +76 -3
- package/build/utils/content-files.test.ts +56 -0
- package/build/utils/content-files.ts +15 -11
- package/build/utils/file-types.test.ts +18 -0
- package/build/utils/file-types.ts +4 -0
- package/build/utils/include.test.ts +117 -0
- package/build/utils/include.ts +53 -0
- package/build/utils/jdi-runner/TraceRunner.java +416 -0
- package/build/utils/literate-java.ts +2 -2
- package/build/utils/markdown.ts +32 -14
- package/build/utils/paths.ts +1 -1
- package/build/utils/render.ts +97 -11
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/utils/trace-layout.test.ts +570 -0
- package/build/utils/trace-layout.ts +848 -0
- package/build/utils/trace-svg.test.ts +425 -0
- package/build/utils/trace-svg.ts +905 -0
- package/build/utils/trace.test.ts +169 -0
- package/build/utils/trace.ts +334 -0
- package/build/validate-internal-links-plugin.test.ts +30 -2
- package/build/validate-internal-links-plugin.ts +3 -3
- package/build/watch-reload-client.ts +4 -20
- package/build/watch.ts +436 -448
- package/init/content/index.md +10 -5
- package/init/content/labs/00/index.md +1 -1
- package/init/content/labs/01/SearchTreeDemo.java +31 -0
- package/init/content/labs/01/TraceDemo.java +22 -0
- package/init/content/labs/01/index.md +27 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +2 -1
- package/init/content/lectures/01/rectangle.py +45 -0
- package/init/content/lectures/02/_pr1.md +5 -0
- package/init/content/lectures/02/index.md +11 -0
- package/init/content/lectures/02/subdir/_foobar.html +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +6 -0
- package/init/content/lectures/index.md +3 -0
- package/init/content/markdown.md +30 -0
- package/package.json +20 -3
- package/src/_base.scss +13 -2
- package/src/_layout.scss +6 -5
- package/src/anchor/index.test.ts +64 -0
- package/src/code/index.test.ts +71 -0
- package/src/code/style.scss +23 -0
- package/src/code.scss +5 -3
- package/src/header/_base.scss +48 -9
- package/src/header/index.test.ts +89 -0
- package/src/header/index.ts +1 -1
- package/src/header/style.scss +1 -5
- package/src/index.ts +18 -20
- package/src/literate/style.scss +15 -7
- package/src/navigate/eligible.test.ts +101 -0
- package/src/navigate/eligible.ts +62 -0
- package/src/navigate/index.test.ts +82 -0
- package/src/navigate/index.ts +365 -0
- package/src/navigate/lifecycle.test.ts +26 -0
- package/src/navigate/lifecycle.ts +65 -0
- package/src/navigate/style.scss +61 -0
- package/src/print/index.test.ts +71 -0
- package/src/question/index.test.ts +82 -0
- package/src/question/style.scss +12 -0
- package/src/search/index.ts +38 -14
- package/src/search/style.scss +23 -14
- package/src/style.scss +25 -12
- package/src/timezone/index.test.ts +301 -1
- package/src/timezone/index.ts +68 -56
- package/src/toc/index.test.ts +237 -0
- package/src/toc/index.ts +29 -69
- package/src/toc/style.scss +17 -0
- package/src/top/index.test.ts +46 -0
- package/src/top/index.ts +2 -2
- package/src/trace/index.test.ts +528 -0
- package/src/trace/index.ts +250 -0
- package/src/trace/style.scss +210 -0
- package/src/trace/types.d.ts +47 -0
- package/src/util.test.ts +135 -0
- package/templates/_top.html +23 -22
- package/types/jsdom.d.ts +9 -0
package/build/watch.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
1
|
import fs from 'fs';
|
|
3
|
-
import { fork } from 'child_process';
|
|
4
2
|
import path from 'path';
|
|
5
3
|
import chokidar from 'chokidar';
|
|
6
4
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
@@ -31,541 +29,531 @@ import { getProcessedExtensions } from './utils/file-types';
|
|
|
31
29
|
import { ContentRenderer } from './generate-content-assets';
|
|
32
30
|
import { WatchPagefindRunner } from './pagefind';
|
|
33
31
|
import { ContentChangeDetector } from './content-watch';
|
|
32
|
+
import { startServer } from './serve';
|
|
33
|
+
|
|
34
|
+
export async function runWatch(options: {
|
|
35
|
+
httpPort?: number;
|
|
36
|
+
wsPort?: number;
|
|
37
|
+
}): Promise<void> {
|
|
38
|
+
const httpPort = options.httpPort ?? null;
|
|
39
|
+
const WEBSOCKET_PORT = options.wsPort ?? 35729;
|
|
40
|
+
const DEBOUNCE_MS = 300;
|
|
41
|
+
const RELOAD_CLIENT_PATH = path.resolve(
|
|
42
|
+
getPackageDir(),
|
|
43
|
+
'build/watch-reload-client.ts',
|
|
44
|
+
);
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
// Bundle the reload client separately to work around a Bun bundler bug where
|
|
47
|
+
// side-effect-only JS entrypoints are tree-shaken when bundled alongside SCSS
|
|
48
|
+
// entrypoints processed by a plugin.
|
|
49
|
+
async function bundleReloadClient(): Promise<string[]> {
|
|
50
|
+
const result = await Bun.build({
|
|
51
|
+
entrypoints: [RELOAD_CLIENT_PATH],
|
|
52
|
+
outdir: getDistDir(),
|
|
53
|
+
naming: '[name].bundle.[ext]',
|
|
54
|
+
sourcemap: 'inline',
|
|
55
|
+
define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
|
|
56
|
+
});
|
|
57
|
+
return result.outputs.map(output =>
|
|
58
|
+
path
|
|
59
|
+
.relative(getDistDir(), output.path)
|
|
60
|
+
.split(path.sep)
|
|
61
|
+
.join(path.posix.sep),
|
|
62
|
+
);
|
|
42
63
|
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const httpPort = getArg('--port');
|
|
47
|
-
const WEBSOCKET_PORT = getArg('--ws-port') ?? 35729;
|
|
48
|
-
const DEBOUNCE_MS = 300;
|
|
49
|
-
const RELOAD_CLIENT_PATH = path.resolve(
|
|
50
|
-
getPackageDir(),
|
|
51
|
-
'build/watch-reload-client.ts',
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
// Bundle the reload client separately to work around a Bun bundler bug where
|
|
55
|
-
// side-effect-only JS entrypoints are tree-shaken when bundled alongside SCSS
|
|
56
|
-
// entrypoints processed by a plugin.
|
|
57
|
-
async function bundleReloadClient(): Promise<string[]> {
|
|
58
|
-
const result = await Bun.build({
|
|
59
|
-
entrypoints: [RELOAD_CLIENT_PATH],
|
|
60
|
-
outdir: getDistDir(),
|
|
61
|
-
naming: '[name].bundle.[ext]',
|
|
62
|
-
sourcemap: 'inline',
|
|
63
|
-
define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
|
|
64
|
-
});
|
|
65
|
-
return result.outputs.map(output =>
|
|
66
|
-
path
|
|
67
|
-
.relative(getDistDir(), output.path)
|
|
68
|
-
.split(path.sep)
|
|
69
|
-
.join(path.posix.sep),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
64
|
|
|
73
|
-
type ChangeCategory = 'content' | 'public' | 'config';
|
|
65
|
+
type ChangeCategory = 'content' | 'public' | 'config';
|
|
74
66
|
|
|
75
|
-
const log = makeLogger(
|
|
76
|
-
const wslog = makeLogger('WebSocket');
|
|
67
|
+
const log = makeLogger(import.meta.url);
|
|
68
|
+
const wslog = makeLogger('WebSocket');
|
|
77
69
|
|
|
78
|
-
//
|
|
70
|
+
// WebSocket server
|
|
79
71
|
|
|
80
|
-
let webSocketsReady = false;
|
|
81
|
-
let watcherReady = false;
|
|
82
|
-
let webServerReady = false;
|
|
83
|
-
let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
84
|
-
let serveStarted = false;
|
|
72
|
+
let webSocketsReady = false;
|
|
73
|
+
let watcherReady = false;
|
|
74
|
+
let webServerReady = false;
|
|
75
|
+
let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
+
let serveStarted = false;
|
|
85
77
|
|
|
86
|
-
let wss: WebSocketServer | null = null;
|
|
87
|
-
try {
|
|
88
|
-
|
|
78
|
+
let wss: WebSocketServer | null = null;
|
|
79
|
+
try {
|
|
80
|
+
wss = new WebSocketServer({ port: WEBSOCKET_PORT });
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
wss.on('connection', (conn: WebSocket) => {
|
|
83
|
+
wslog.debug`WebSocket client connected`;
|
|
84
|
+
if (watcherReady && conn.readyState === WebSocket.OPEN) {
|
|
85
|
+
if (initialBuildFailed) {
|
|
86
|
+
conn.send('error');
|
|
87
|
+
}
|
|
88
|
+
conn.send('ready');
|
|
95
89
|
}
|
|
96
|
-
conn.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
wslog.debug`WebSocket client disconnected`;
|
|
90
|
+
conn.on('close', () => {
|
|
91
|
+
wslog.debug`WebSocket client disconnected`;
|
|
92
|
+
});
|
|
100
93
|
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
wss.on('error', (err: Error) => {
|
|
104
|
-
wslog.error`WebSocket server error: ${err.message}`;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
wss.on('listening', () => {
|
|
108
|
-
wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
|
|
109
|
-
webSocketsReady = true;
|
|
110
|
-
});
|
|
111
|
-
} catch (err) {
|
|
112
|
-
wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${(err as Error).message}`;
|
|
113
|
-
}
|
|
114
94
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
wslog.debug`Broadcasting "${msg}" to WebSocket clients`;
|
|
120
|
-
wss.clients.forEach((client: WebSocket) => {
|
|
121
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
122
|
-
client.send(msg);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
95
|
+
wss.on('error', (err: Error) => {
|
|
96
|
+
wslog.error`WebSocket server error: ${err.message}`;
|
|
97
|
+
});
|
|
126
98
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
child.on('close', (code: number | null) => {
|
|
135
|
-
webServerReady = false;
|
|
136
|
-
log.error`Web server exited with code ${code}`;
|
|
137
|
-
process.exit(2);
|
|
138
|
-
});
|
|
139
|
-
child.on('error', (err: Error) => {
|
|
140
|
-
webServerReady = false;
|
|
141
|
-
log.error`Web server failed: ${err.message}`;
|
|
142
|
-
});
|
|
143
|
-
child.on('message', (msg: Record<string, unknown>) => {
|
|
144
|
-
if (msg.ready) {
|
|
145
|
-
webServerReady = true;
|
|
146
|
-
clearTimeout(webServerTimeout);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
99
|
+
wss.on('listening', () => {
|
|
100
|
+
wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
|
|
101
|
+
webSocketsReady = true;
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${(err as Error).message}`;
|
|
105
|
+
}
|
|
149
106
|
|
|
150
|
-
|
|
151
|
-
if (
|
|
107
|
+
function broadcast(msg: string): void {
|
|
108
|
+
if (wss == null || !webSocketsReady) {
|
|
152
109
|
return;
|
|
153
110
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
function toContentMarkdownPath(filePath: string): string | null {
|
|
162
|
-
if (!filePath) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
166
|
-
if (!['.md', '.markdown'].includes(ext)) {
|
|
167
|
-
return null;
|
|
111
|
+
wslog.debug`Broadcasting "${msg}" to WebSocket clients`;
|
|
112
|
+
wss.clients.forEach((client: WebSocket) => {
|
|
113
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
114
|
+
client.send(msg);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
168
117
|
}
|
|
169
118
|
|
|
170
|
-
|
|
171
|
-
const normalizedFilePath = path.resolve(filePath);
|
|
172
|
-
if (!normalizedFilePath.startsWith(normalizedContentDir)) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
119
|
+
// Dev server
|
|
175
120
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
121
|
+
function serve(): void {
|
|
122
|
+
startServer({
|
|
123
|
+
port: httpPort ?? undefined,
|
|
124
|
+
distDir,
|
|
125
|
+
onReady: _port => {
|
|
126
|
+
webServerReady = true;
|
|
127
|
+
clearTimeout(webServerTimeout);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
181
130
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return null;
|
|
131
|
+
webServerTimeout = setTimeout(() => {
|
|
132
|
+
if (webServerReady) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
log.error`Web server failed to report within 10 seconds, exiting`;
|
|
136
|
+
process.exit(3);
|
|
137
|
+
}, 10000);
|
|
190
138
|
}
|
|
191
|
-
return path
|
|
192
|
-
.relative(publicDir, normalizedFilePath)
|
|
193
|
-
.split(path.sep)
|
|
194
|
-
.join(path.posix.sep);
|
|
195
|
-
}
|
|
196
139
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
siteVariables = getDevSiteVariables();
|
|
217
|
-
processedExtSet = new Set(
|
|
218
|
-
getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
|
|
219
|
-
);
|
|
220
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
140
|
+
// Path helpers
|
|
141
|
+
|
|
142
|
+
function toContentMarkdownPath(filePath: string): string | null {
|
|
143
|
+
if (!filePath) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
147
|
+
if (!['.md', '.markdown'].includes(ext)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
221
150
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
151
|
+
const normalizedContentDir = path.resolve(contentDir) + path.sep;
|
|
152
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
153
|
+
if (!normalizedFilePath.startsWith(normalizedContentDir)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
225
156
|
|
|
226
|
-
|
|
227
|
-
|
|
157
|
+
return path
|
|
158
|
+
.relative(contentDir, normalizedFilePath)
|
|
159
|
+
.split(path.sep)
|
|
160
|
+
.join(path.posix.sep);
|
|
228
161
|
}
|
|
229
162
|
|
|
230
|
-
|
|
163
|
+
function toPublicRelativePath(filePath: string): string | null {
|
|
164
|
+
if (!filePath) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const normalizedPublicDir = path.resolve(publicDir) + path.sep;
|
|
168
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
169
|
+
if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return path
|
|
173
|
+
.relative(publicDir, normalizedFilePath)
|
|
174
|
+
.split(path.sep)
|
|
175
|
+
.join(path.posix.sep);
|
|
176
|
+
}
|
|
231
177
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
178
|
+
// Watch mode
|
|
179
|
+
|
|
180
|
+
const contentDir: string = getContentDir();
|
|
181
|
+
const publicDir: string = getPublicDir();
|
|
182
|
+
const distDir: string = getDistDir();
|
|
183
|
+
|
|
184
|
+
let siteVariables: SiteVariables;
|
|
185
|
+
let processedExtSet = new Set<string>();
|
|
186
|
+
let publicRelPaths: Set<string> = new Set();
|
|
187
|
+
let contentAssetRelPaths: Set<string> = new Set();
|
|
188
|
+
let contentRenderer: ContentRenderer;
|
|
189
|
+
let changeDetector: ContentChangeDetector;
|
|
190
|
+
let pagefindRunner: WatchPagefindRunner | undefined;
|
|
191
|
+
let assetFiles: string[] = [];
|
|
192
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
193
|
+
const pendingChanges = new Set<string>();
|
|
194
|
+
let rebuilding = false;
|
|
195
|
+
|
|
196
|
+
async function initialBuild(): Promise<void> {
|
|
197
|
+
siteVariables = getDevSiteVariables();
|
|
198
|
+
processedExtSet = new Set(
|
|
199
|
+
getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
|
|
200
|
+
);
|
|
201
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
202
|
+
|
|
203
|
+
compileTemplates(siteVariables);
|
|
204
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
205
|
+
changeDetector = new ContentChangeDetector(siteVariables);
|
|
206
|
+
|
|
207
|
+
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
208
|
+
pagefindRunner = new WatchPagefindRunner(siteVariables);
|
|
209
|
+
}
|
|
236
210
|
|
|
237
|
-
|
|
238
|
-
copyKatexAssets(distDir);
|
|
211
|
+
await contentRenderer.initHighlighter();
|
|
239
212
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
213
|
+
assetFiles = [
|
|
214
|
+
...(await bundle(siteVariables, { mode: 'development' })),
|
|
215
|
+
...(await bundleReloadClient()),
|
|
216
|
+
];
|
|
244
217
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
contentDir,
|
|
248
|
-
distDir,
|
|
249
|
-
[...processedExtSet],
|
|
250
|
-
publicRelPaths,
|
|
251
|
-
);
|
|
218
|
+
copyFonts(distDir);
|
|
219
|
+
copyKatexAssets(distDir);
|
|
252
220
|
|
|
253
|
-
|
|
221
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
222
|
+
await generateFavicons(siteVariables, distDir);
|
|
223
|
+
generateWebAppManifest(siteVariables, distDir);
|
|
224
|
+
}
|
|
254
225
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
226
|
+
publicRelPaths = copyPublicFiles(publicDir, distDir);
|
|
227
|
+
contentAssetRelPaths = copyContentAssets(
|
|
228
|
+
contentDir,
|
|
229
|
+
distDir,
|
|
230
|
+
[...processedExtSet],
|
|
231
|
+
publicRelPaths,
|
|
232
|
+
);
|
|
258
233
|
|
|
259
|
-
|
|
260
|
-
printFlair();
|
|
234
|
+
const result = contentRenderer.processContent({ distDir, assetFiles });
|
|
261
235
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
setImmediate(() => pagefindRunner!.run());
|
|
236
|
+
for (const err of result.errors) {
|
|
237
|
+
log.error`${err.message}`;
|
|
265
238
|
}
|
|
266
239
|
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
240
|
+
if (result.errors.length === 0) {
|
|
241
|
+
printFlair();
|
|
242
|
+
|
|
243
|
+
if (pagefindRunner) {
|
|
244
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
245
|
+
setImmediate(() => pagefindRunner!.run());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!serveStarted) {
|
|
249
|
+
serveStarted = true;
|
|
250
|
+
serve();
|
|
251
|
+
}
|
|
270
252
|
}
|
|
271
253
|
}
|
|
272
|
-
}
|
|
273
254
|
|
|
274
|
-
function classifyChange(filePath: string): ChangeCategory | null {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
255
|
+
function classifyChange(filePath: string): ChangeCategory | null {
|
|
256
|
+
const resolved = path.resolve(filePath);
|
|
257
|
+
const resolvedContentDir = path.resolve(contentDir) + path.sep;
|
|
258
|
+
const resolvedPublicDir = path.resolve(publicDir) + path.sep;
|
|
259
|
+
const jsonDataDir = getJsonDataDir();
|
|
260
|
+
const siteConfigPath = path.resolve('site.dev.json');
|
|
280
261
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
262
|
+
if (resolved.startsWith(resolvedContentDir)) {
|
|
263
|
+
return 'content';
|
|
264
|
+
}
|
|
265
|
+
if (resolved.startsWith(resolvedPublicDir)) {
|
|
266
|
+
return 'public';
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
resolved === siteConfigPath ||
|
|
270
|
+
JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
|
|
271
|
+
) {
|
|
272
|
+
return 'config';
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
292
275
|
}
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
276
|
|
|
296
|
-
async function rebuild(): Promise<void> {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
277
|
+
async function rebuild(): Promise<void> {
|
|
278
|
+
if (rebuilding) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
rebuilding = true;
|
|
301
282
|
|
|
302
|
-
|
|
303
|
-
|
|
283
|
+
const changes = new Set(pendingChanges);
|
|
284
|
+
pendingChanges.clear();
|
|
304
285
|
|
|
305
|
-
|
|
286
|
+
broadcast('rebuilding');
|
|
306
287
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
288
|
+
// Classify changes
|
|
289
|
+
const categories = new Set<ChangeCategory>();
|
|
290
|
+
for (const filePath of changes) {
|
|
291
|
+
const category = classifyChange(filePath);
|
|
292
|
+
if (category) {
|
|
293
|
+
categories.add(category);
|
|
294
|
+
}
|
|
313
295
|
}
|
|
314
|
-
}
|
|
315
296
|
|
|
316
|
-
|
|
297
|
+
let succeeded = false;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
// Site config changed, full restart
|
|
301
|
+
if (categories.has('config')) {
|
|
302
|
+
log.event`Site config changed, restarting`;
|
|
303
|
+
siteVariables = getDevSiteVariables();
|
|
304
|
+
processedExtSet = new Set(
|
|
305
|
+
getProcessedExtensions(
|
|
306
|
+
Object.keys(siteVariables.codeLanguages || {}),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
310
|
+
changeDetector = new ContentChangeDetector(siteVariables);
|
|
311
|
+
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
312
|
+
pagefindRunner = new WatchPagefindRunner(siteVariables);
|
|
313
|
+
}
|
|
314
|
+
compileTemplates(siteVariables);
|
|
315
|
+
await contentRenderer.initHighlighter();
|
|
317
316
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
siteVariables = getDevSiteVariables();
|
|
323
|
-
processedExtSet = new Set(
|
|
324
|
-
getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
|
|
325
|
-
);
|
|
326
|
-
contentRenderer = new ContentRenderer(siteVariables);
|
|
327
|
-
changeDetector = new ContentChangeDetector(siteVariables);
|
|
328
|
-
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
329
|
-
pagefindRunner = new WatchPagefindRunner(siteVariables);
|
|
330
|
-
}
|
|
331
|
-
compileTemplates(siteVariables);
|
|
332
|
-
await contentRenderer.initHighlighter();
|
|
317
|
+
assetFiles = [
|
|
318
|
+
...(await bundle(siteVariables, { mode: 'development' })),
|
|
319
|
+
...(await bundleReloadClient()),
|
|
320
|
+
];
|
|
333
321
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
...(await bundleReloadClient()),
|
|
337
|
-
];
|
|
322
|
+
copyFonts(distDir);
|
|
323
|
+
copyKatexAssets(distDir);
|
|
338
324
|
|
|
339
|
-
|
|
340
|
-
|
|
325
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
326
|
+
await generateFavicons(siteVariables, distDir);
|
|
327
|
+
generateWebAppManifest(siteVariables, distDir);
|
|
328
|
+
}
|
|
341
329
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
330
|
+
const result = contentRenderer.processContent({ distDir, assetFiles });
|
|
331
|
+
for (const err of result.errors) {
|
|
332
|
+
log.error`${err.message}`;
|
|
333
|
+
}
|
|
334
|
+
if (result.errors.length === 0) {
|
|
335
|
+
printFlair();
|
|
336
|
+
if (!serveStarted) {
|
|
337
|
+
serveStarted = true;
|
|
338
|
+
serve();
|
|
339
|
+
}
|
|
340
|
+
broadcast('reload');
|
|
341
|
+
if (pagefindRunner) {
|
|
342
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
343
|
+
setImmediate(() => pagefindRunner!.run());
|
|
344
|
+
}
|
|
345
|
+
succeeded = true;
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
350
|
+
// Public file changed, copy just that file
|
|
351
|
+
if (categories.has('public') && !categories.has('content')) {
|
|
352
|
+
for (const filePath of changes) {
|
|
353
|
+
if (classifyChange(filePath) === 'public') {
|
|
354
|
+
const absPath = path.resolve(filePath);
|
|
355
|
+
if (fs.existsSync(absPath)) {
|
|
356
|
+
copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
|
|
357
|
+
const rel = path
|
|
358
|
+
.relative(publicDir, absPath)
|
|
359
|
+
.split(path.sep)
|
|
360
|
+
.join(path.posix.sep);
|
|
361
|
+
publicRelPaths.add(rel);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
356
364
|
}
|
|
365
|
+
printFlair();
|
|
357
366
|
broadcast('reload');
|
|
358
|
-
if (pagefindRunner) {
|
|
359
|
-
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
360
|
-
setImmediate(() => pagefindRunner!.run());
|
|
361
|
-
}
|
|
362
367
|
succeeded = true;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Content changed, incremental rebuild
|
|
372
|
+
const detection = changeDetector.detectChanges(changes);
|
|
373
|
+
|
|
374
|
+
if (detection.needsRestart) {
|
|
375
|
+
log.event`Content structure changed, full rebuild`;
|
|
376
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
377
|
+
await contentRenderer.initHighlighter();
|
|
363
378
|
}
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
379
|
|
|
367
|
-
|
|
368
|
-
if (categories.has('public') && !categories.has('content')) {
|
|
380
|
+
// Log changed content files
|
|
369
381
|
for (const filePath of changes) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
.join(path.posix.sep);
|
|
378
|
-
publicRelPaths.add(rel);
|
|
382
|
+
const markdownPath = toContentMarkdownPath(filePath);
|
|
383
|
+
if (markdownPath) {
|
|
384
|
+
log.event`${B`${markdownPath}`} changed, rebuilding`;
|
|
385
|
+
} else {
|
|
386
|
+
const pubPath = toPublicRelativePath(filePath);
|
|
387
|
+
if (pubPath) {
|
|
388
|
+
log.event`${B`public/${pubPath}`} changed`;
|
|
379
389
|
}
|
|
380
390
|
}
|
|
381
391
|
}
|
|
382
|
-
printFlair();
|
|
383
|
-
broadcast('reload');
|
|
384
|
-
succeeded = true;
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Content changed, incremental rebuild
|
|
389
|
-
const detection = changeDetector.detectChanges(changes);
|
|
390
|
-
|
|
391
|
-
if (detection.needsRestart) {
|
|
392
|
-
log.event`Content structure changed, full rebuild`;
|
|
393
|
-
contentRenderer = new ContentRenderer(siteVariables);
|
|
394
|
-
await contentRenderer.initHighlighter();
|
|
395
|
-
}
|
|
396
392
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
393
|
+
// Copy any changed public files too
|
|
394
|
+
if (categories.has('public')) {
|
|
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
|
+
}
|
|
406
407
|
}
|
|
407
408
|
}
|
|
408
|
-
}
|
|
409
409
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
// Copy any changed non-processed content files (images, PDFs, etc.)
|
|
411
|
+
if (categories.has('content')) {
|
|
412
|
+
for (const filePath of changes) {
|
|
413
|
+
if (classifyChange(filePath) !== 'content') {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
414
416
|
const absPath = path.resolve(filePath);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
+
const ext = path.extname(absPath).slice(1).toLowerCase();
|
|
418
|
+
if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
|
|
419
|
+
copyContentFile(contentDir, distDir, absPath, publicRelPaths);
|
|
417
420
|
const rel = path
|
|
418
|
-
.relative(
|
|
421
|
+
.relative(contentDir, absPath)
|
|
419
422
|
.split(path.sep)
|
|
420
423
|
.join(path.posix.sep);
|
|
421
|
-
|
|
424
|
+
contentAssetRelPaths.add(rel);
|
|
422
425
|
}
|
|
423
426
|
}
|
|
424
427
|
}
|
|
425
|
-
}
|
|
426
428
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const ext = path.extname(absPath).slice(1).toLowerCase();
|
|
435
|
-
if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
|
|
436
|
-
copyContentFile(contentDir, distDir, absPath, publicRelPaths);
|
|
437
|
-
const rel = path
|
|
438
|
-
.relative(contentDir, absPath)
|
|
439
|
-
.split(path.sep)
|
|
440
|
-
.join(path.posix.sep);
|
|
441
|
-
contentAssetRelPaths.add(rel);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
429
|
+
const watchState: WatchState | undefined = detection.needsRestart
|
|
430
|
+
? undefined
|
|
431
|
+
: {
|
|
432
|
+
changedContentFiles: detection.changedContentFiles,
|
|
433
|
+
jsonDataChanged: detection.jsonDataChanged,
|
|
434
|
+
partialsChanged: detection.partialsChanged,
|
|
435
|
+
};
|
|
445
436
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
};
|
|
437
|
+
const result = contentRenderer.processContent({
|
|
438
|
+
distDir,
|
|
439
|
+
assetFiles,
|
|
440
|
+
watchState,
|
|
441
|
+
});
|
|
452
442
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
watchState,
|
|
457
|
-
});
|
|
443
|
+
for (const err of result.errors) {
|
|
444
|
+
log.error`${err.message}`;
|
|
445
|
+
}
|
|
458
446
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
447
|
+
if (result.errors.length === 0) {
|
|
448
|
+
printFlair();
|
|
449
|
+
if (!serveStarted) {
|
|
450
|
+
serveStarted = true;
|
|
451
|
+
serve();
|
|
452
|
+
}
|
|
453
|
+
broadcast('reload');
|
|
462
454
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
455
|
+
if (pagefindRunner) {
|
|
456
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
457
|
+
setImmediate(() => pagefindRunner!.run());
|
|
458
|
+
}
|
|
459
|
+
succeeded = true;
|
|
460
|
+
}
|
|
461
|
+
} catch (err) {
|
|
462
|
+
log.error`Build failed: ${(err as Error).message}`;
|
|
463
|
+
} finally {
|
|
464
|
+
if (!succeeded) {
|
|
465
|
+
broadcast('error');
|
|
468
466
|
}
|
|
469
|
-
|
|
467
|
+
rebuilding = false;
|
|
470
468
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
469
|
+
// If more changes accumulated during rebuild, schedule another
|
|
470
|
+
if (pendingChanges.size > 0) {
|
|
471
|
+
scheduleRebuild();
|
|
474
472
|
}
|
|
475
|
-
succeeded = true;
|
|
476
|
-
}
|
|
477
|
-
} catch (err) {
|
|
478
|
-
log.error`Build failed: ${(err as Error).message}`;
|
|
479
|
-
} finally {
|
|
480
|
-
if (!succeeded) {
|
|
481
|
-
broadcast('error');
|
|
482
473
|
}
|
|
483
|
-
|
|
474
|
+
}
|
|
484
475
|
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
476
|
+
function scheduleRebuild(): void {
|
|
477
|
+
if (debounceTimer) {
|
|
478
|
+
clearTimeout(debounceTimer);
|
|
488
479
|
}
|
|
480
|
+
debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
489
481
|
}
|
|
490
|
-
}
|
|
491
482
|
|
|
492
|
-
function
|
|
493
|
-
|
|
494
|
-
|
|
483
|
+
function onFileChange(filePath: string): void {
|
|
484
|
+
pendingChanges.add(filePath);
|
|
485
|
+
scheduleRebuild();
|
|
495
486
|
}
|
|
496
|
-
debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function onFileChange(filePath: string): void {
|
|
500
|
-
pendingChanges.add(filePath);
|
|
501
|
-
scheduleRebuild();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// --- Start ---
|
|
505
487
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
488
|
+
// Start
|
|
489
|
+
|
|
490
|
+
let initialBuildFailed = false;
|
|
491
|
+
|
|
492
|
+
initialBuild()
|
|
493
|
+
.catch(err => {
|
|
494
|
+
log.error`Initial build failed: ${(err as Error).message}`;
|
|
495
|
+
initialBuildFailed = true;
|
|
496
|
+
})
|
|
497
|
+
.finally(() => {
|
|
498
|
+
const watchPaths: string[] = [contentDir, publicDir];
|
|
499
|
+
|
|
500
|
+
// Watch config files (nav.json, authors.json, site config) via the
|
|
501
|
+
// project root directory (depth 0). Chokidar v4 doesn't emit unlink
|
|
502
|
+
// for individually watched files, so we must watch the directory.
|
|
503
|
+
const jsonDataDir = getJsonDataDir();
|
|
504
|
+
const configFilePaths = new Set([
|
|
505
|
+
...JSON_DATA_FILES.map(f => path.resolve(jsonDataDir, f)),
|
|
506
|
+
path.resolve('site.dev.json'),
|
|
507
|
+
]);
|
|
508
|
+
const configWatcher = chokidar.watch(jsonDataDir, {
|
|
509
|
+
ignoreInitial: true,
|
|
510
|
+
depth: 0,
|
|
511
|
+
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
512
|
+
});
|
|
513
|
+
const onConfigFileChange = (filePath: string) => {
|
|
514
|
+
if (configFilePaths.has(path.resolve(filePath))) {
|
|
515
|
+
onFileChange(filePath);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
configWatcher.on('add', onConfigFileChange);
|
|
519
|
+
configWatcher.on('change', onConfigFileChange);
|
|
520
|
+
configWatcher.on('unlink', onConfigFileChange);
|
|
521
|
+
|
|
522
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
523
|
+
ignoreInitial: true,
|
|
524
|
+
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
watcher.on('change', onFileChange);
|
|
528
|
+
watcher.on('add', onFileChange);
|
|
529
|
+
watcher.on('unlink', onFileChange);
|
|
530
|
+
|
|
531
|
+
let configWatcherReady = false;
|
|
532
|
+
let contentWatcherReady = false;
|
|
533
|
+
|
|
534
|
+
function onWatcherReady(): void {
|
|
535
|
+
if (!configWatcherReady || !contentWatcherReady) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
watcherReady = true;
|
|
539
|
+
if (initialBuildFailed) {
|
|
540
|
+
broadcast('error');
|
|
541
|
+
}
|
|
542
|
+
broadcast('ready');
|
|
543
|
+
log.info`Watching for changes...`;
|
|
532
544
|
}
|
|
533
|
-
};
|
|
534
|
-
configWatcher.on('add', onConfigFileChange);
|
|
535
|
-
configWatcher.on('change', onConfigFileChange);
|
|
536
|
-
configWatcher.on('unlink', onConfigFileChange);
|
|
537
|
-
|
|
538
|
-
const watcher = chokidar.watch(watchPaths, {
|
|
539
|
-
ignoreInitial: true,
|
|
540
|
-
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
watcher.on('change', onFileChange);
|
|
544
|
-
watcher.on('add', onFileChange);
|
|
545
|
-
watcher.on('unlink', onFileChange);
|
|
546
545
|
|
|
547
|
-
|
|
548
|
-
|
|
546
|
+
configWatcher.on('ready', () => {
|
|
547
|
+
configWatcherReady = true;
|
|
548
|
+
onWatcherReady();
|
|
549
|
+
});
|
|
549
550
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
watcherReady = true;
|
|
555
|
-
if (initialBuildFailed) {
|
|
556
|
-
broadcast('error');
|
|
557
|
-
}
|
|
558
|
-
broadcast('ready');
|
|
559
|
-
log.info`Watching for changes...`;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
configWatcher.on('ready', () => {
|
|
563
|
-
configWatcherReady = true;
|
|
564
|
-
onWatcherReady();
|
|
551
|
+
watcher.on('ready', () => {
|
|
552
|
+
contentWatcherReady = true;
|
|
553
|
+
onWatcherReady();
|
|
554
|
+
});
|
|
565
555
|
});
|
|
566
556
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
});
|
|
571
|
-
});
|
|
557
|
+
// Return a promise that never resolves (watch mode runs until killed)
|
|
558
|
+
return new Promise(() => {});
|
|
559
|
+
}
|