@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.
- package/README.md +29 -33
- package/bin/tada.ts +356 -0
- package/bin/validators.test.ts +204 -0
- package/bin/validators.ts +83 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
- package/build/bundle.ts +117 -0
- package/{webpack/code.test.js → build/code.test.ts} +6 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +315 -0
- package/build/generate-favicon.ts +165 -0
- package/build/generate-fonts.ts +31 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +101 -0
- package/{webpack/globals.js → build/globals.ts} +28 -13
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +93 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
- package/{webpack → build}/site.schema.json +3 -10
- package/{webpack/templates.js → build/templates.ts} +35 -33
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +172 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +80 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +573 -0
- package/content/index.md +9 -3
- package/content/markdown.md +2 -1
- package/content/problem_sets/index.html +14 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/package.json +28 -19
- package/src/_alerts.scss +92 -0
- package/src/_base.scss +106 -0
- package/src/{layout.scss → _layout.scss} +0 -2
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +1 -2
- package/src/question/style.scss +1 -1
- package/src/search/index.ts +36 -15
- package/src/search/style.scss +9 -15
- package/src/style.scss +6 -269
- package/src/toc/style.scss +5 -39
- package/src/util.ts +8 -5
- package/templates/_theme.scss +38 -14
- package/tsconfig.json +10 -6
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/untyped-modules.d.ts +40 -0
- package/bin/tada.js +0 -361
- package/content/problem_sets/index.md +0 -6
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -151
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
- /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
|
|
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
|
-
|
|
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
|
package/content/markdown.md
CHANGED
|
@@ -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>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|