@abreen/tada 1.7.0 → 1.7.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.
@@ -5,7 +5,7 @@ import { execFileSync } from 'child_process';
5
5
  import { makeLogger } from '../log';
6
6
  import { B } from '../colors';
7
7
  import { normalizeOutputPath, toPosix } from './paths';
8
- import { hasMainMethod } from './literate-java';
8
+ import { hasMainMethod, runJavac, splitLines } from './literate-java';
9
9
  import { renderCodeSegment } from './code';
10
10
  import { computeLayout } from './trace-layout';
11
11
  import { generateStepSvg } from './trace-svg';
@@ -90,19 +90,10 @@ function ensureTracerCompiled(): string {
90
90
 
91
91
  log.debug`Compiling TraceRunner.java into ${tempDir}`;
92
92
 
93
- try {
94
- execFileSync('javac', ['-d', tempDir, sourceFile], {
95
- timeout: 30000,
96
- stdio: ['pipe', 'pipe', 'pipe'],
97
- });
98
- } catch (err: unknown) {
99
- const execErr = err as { stderr?: Buffer; message: string };
100
- const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
101
- fs.rmSync(tempDir, { recursive: true, force: true });
102
- throw new Error(`Failed to compile TraceRunner.java:\n${stderr}`, {
103
- cause: err,
104
- });
105
- }
93
+ runJavac(['-d', tempDir, sourceFile], {
94
+ tempDir,
95
+ label: 'Failed to compile TraceRunner.java',
96
+ });
106
97
 
107
98
  tracerClassDir = tempDir;
108
99
  return tempDir;
@@ -111,27 +102,17 @@ function ensureTracerCompiled(): string {
111
102
  function compileTargetFile(javaFilePath: string): string {
112
103
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-target-'));
113
104
 
114
- try {
115
- execFileSync(
116
- 'javac',
117
- [
118
- '-g',
119
- '-d',
120
- tempDir,
121
- '-sourcepath',
122
- path.dirname(javaFilePath),
123
- javaFilePath,
124
- ],
125
- { timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] },
126
- );
127
- } catch (err: unknown) {
128
- const execErr = err as { stderr?: Buffer; message: string };
129
- const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
130
- fs.rmSync(tempDir, { recursive: true, force: true });
131
- throw new Error(`Compilation failed for ${javaFilePath}:\n${stderr}`, {
132
- cause: err,
133
- });
134
- }
105
+ runJavac(
106
+ [
107
+ '-g',
108
+ '-d',
109
+ tempDir,
110
+ '-sourcepath',
111
+ path.dirname(javaFilePath),
112
+ javaFilePath,
113
+ ],
114
+ { tempDir, label: `Compilation failed for ${javaFilePath}` },
115
+ );
135
116
 
136
117
  return tempDir;
137
118
  }
@@ -224,14 +205,10 @@ export interface TraceResult {
224
205
  mtime: number;
225
206
  }
226
207
 
227
- function highlightSource(javaFile: string, source: string): string {
228
- const sourceLines = source.split('\n');
229
- if (sourceLines[sourceLines.length - 1] === '') {
230
- sourceLines.pop();
231
- }
232
- const ext = path.extname(javaFile).slice(1).toLowerCase();
233
- const lang = ext === 'java' ? 'java' : 'text';
234
- return renderCodeSegment(sourceLines, 1, lang, { linkLineNumbers: false });
208
+ function highlightSource(source: string): string {
209
+ return renderCodeSegment(splitLines(source), 1, 'java', {
210
+ linkLineNumbers: false,
211
+ });
235
212
  }
236
213
 
237
214
  function renderWidgetHtml({
@@ -322,7 +299,7 @@ export function createTraceHelpers(context: TraceContext): {
322
299
  { timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
323
300
  );
324
301
 
325
- const highlightedSource = highlightSource(javaFile, source);
302
+ const highlightedSource = highlightSource(source);
326
303
 
327
304
  const relDir = toPosix(path.relative(contentDir, pageDir));
328
305
  const traceOutputDir = path.join(distDir, relDir, '_traces', className);
@@ -367,9 +344,7 @@ export function createTraceHelpers(context: TraceContext): {
367
344
  throw new Error(`Trace target not found: ${javaFilePath}`);
368
345
  }
369
346
  const source = fs.readFileSync(javaFilePath, 'utf-8');
370
- return renderWidgetHtml({
371
- highlightedSource: highlightSource(javaFile, source),
372
- });
347
+ return renderWidgetHtml({ highlightedSource: highlightSource(source) });
373
348
  }
374
349
  const { manifestUrl, highlightedSource, totalSteps } =
375
350
  getOrRunTrace(javaFile);
@@ -1,6 +1,7 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
2
  import type Token from 'markdown-it/lib/token.mjs';
3
3
  import path from 'path';
4
+ import { isInternalLink } from './utils/link';
4
5
  import { makeLogger } from './log';
5
6
 
6
7
  const log = makeLogger(import.meta.url);
@@ -17,14 +18,6 @@ function stripQueryAndHash(href: string): string {
17
18
  return href.split('#')[0].split('?')[0];
18
19
  }
19
20
 
20
- function isExternalOrAnchor(href: string): boolean {
21
- if (!href || href.startsWith('#') || href.startsWith('//')) {
22
- return true;
23
- }
24
-
25
- return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href);
26
- }
27
-
28
21
  function normalizePathname(pathname: string): string {
29
22
  const normalized = path.posix.normalize(pathname);
30
23
  if (normalized === '.') {
@@ -142,7 +135,7 @@ export default function validateInternalLinks(
142
135
  }
143
136
 
144
137
  const href = rawHref.trim();
145
- if (!href || isExternalOrAnchor(href)) {
138
+ if (!href || !isInternalLink(href)) {
146
139
  return;
147
140
  }
148
141
 
@@ -2,12 +2,10 @@ declare const __WEBSOCKET_PORT__: number;
2
2
 
3
3
  (function () {
4
4
  const style = document.createElement('style');
5
- style.textContent = [
6
- 'body.loading-cursor,',
7
- 'body.loading-cursor * {',
8
- ' cursor: wait !important;',
9
- '}',
10
- ].join('\n');
5
+ style.textContent = `body.loading-cursor,
6
+ body.loading-cursor * {
7
+ cursor: wait !important;
8
+ }`;
11
9
  document.head.appendChild(style);
12
10
 
13
11
  const ws = new WebSocket(`ws://localhost:${__WEBSOCKET_PORT__}`);
package/build/watch.ts CHANGED
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import chokidar from 'chokidar';
4
4
  import WebSocket, { WebSocketServer } from 'ws';
5
- import type { SiteVariables, WatchState } from './types';
5
+ import type { SiteVariables, WatchState, ContentRenderResult } from './types';
6
6
  import { B } from './colors';
7
7
  import { makeLogger, printFlair } from './log';
8
8
  import { getDevSiteVariables } from './site-variables';
@@ -36,7 +36,7 @@ export async function runWatch(options: {
36
36
  httpPort?: number;
37
37
  wsPort?: number;
38
38
  }): Promise<void> {
39
- const httpPort = options.httpPort ?? null;
39
+ const httpPort = options.httpPort;
40
40
  const WEBSOCKET_PORT = options.wsPort ?? 35729;
41
41
  const DEBOUNCE_MS = 300;
42
42
  const RELOAD_CLIENT_PATH = path.resolve(
@@ -69,7 +69,6 @@ export async function runWatch(options: {
69
69
 
70
70
  let webSocketsReady = false;
71
71
  let watcherReady = false;
72
- let webServerReady = false;
73
72
  let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
74
73
  let serveStarted = false;
75
74
 
@@ -117,30 +116,23 @@ export async function runWatch(options: {
117
116
  // Dev server
118
117
 
119
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
+
120
124
  startServer({
121
- port: httpPort ?? undefined,
125
+ port: httpPort,
122
126
  distDir,
123
- onReady: _port => {
124
- webServerReady = true;
127
+ onReady: () => {
125
128
  clearTimeout(webServerTimeout);
126
129
  },
127
130
  });
128
-
129
- webServerTimeout = setTimeout(() => {
130
- if (webServerReady) {
131
- return;
132
- }
133
- log.error`Web server failed to report within 10 seconds, exiting`;
134
- process.exit(3);
135
- }, 10000);
136
131
  }
137
132
 
138
133
  // Path helpers
139
134
 
140
135
  function toContentMarkdownPath(filePath: string): string | null {
141
- if (!filePath) {
142
- return null;
143
- }
144
136
  const ext = path.extname(filePath).toLowerCase();
145
137
  if (!['.md', '.markdown'].includes(ext)) {
146
138
  return null;
@@ -156,9 +148,6 @@ export async function runWatch(options: {
156
148
  }
157
149
 
158
150
  function toPublicRelativePath(filePath: string): string | null {
159
- if (!filePath) {
160
- return null;
161
- }
162
151
  const normalizedPublicDir = path.resolve(publicDir) + path.sep;
163
152
  const normalizedFilePath = path.resolve(filePath);
164
153
  if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
@@ -185,14 +174,39 @@ export async function runWatch(options: {
185
174
  const pendingChanges = new Set<string>();
186
175
  let rebuilding = false;
187
176
 
188
- async function initialBuild(): Promise<void> {
189
- siteVariables = getDevSiteVariables();
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> {
190
207
  processedExtSet = new Set(
191
208
  getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
192
209
  );
193
- fs.mkdirSync(distDir, { recursive: true });
194
-
195
- compileTemplates(siteVariables);
196
210
  contentRenderer = new ContentRenderer(siteVariables);
197
211
  changeDetector = new ContentChangeDetector(siteVariables);
198
212
 
@@ -200,6 +214,7 @@ export async function runWatch(options: {
200
214
  pagefindRunner = new WatchPagefindRunner(siteVariables);
201
215
  }
202
216
 
217
+ compileTemplates(siteVariables);
203
218
  await contentRenderer.initHighlighter();
204
219
 
205
220
  assetFiles = [
@@ -215,15 +230,24 @@ export async function runWatch(options: {
215
230
  generateWebAppManifest(siteVariables, distDir);
216
231
  }
217
232
 
218
- publicRelPaths = copyPublicFiles(publicDir, distDir);
219
- contentAssetRelPaths = copyContentAssets(
220
- contentDir,
221
- distDir,
222
- [...processedExtSet],
223
- publicRelPaths,
224
- );
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 });
225
249
 
226
- const result = contentRenderer.processContent({ distDir, assetFiles });
250
+ const result = await buildFromConfig(true);
227
251
 
228
252
  for (const err of result.errors) {
229
253
  log.error`${err.message}`;
@@ -231,12 +255,10 @@ export async function runWatch(options: {
231
255
 
232
256
  if (result.errors.length === 0) {
233
257
  printFlair();
234
-
235
258
  if (pagefindRunner) {
236
259
  pagefindRunner.update(distDir, result.htmlAssetsByPath);
237
260
  setImmediate(() => pagefindRunner!.run());
238
261
  }
239
-
240
262
  if (!serveStarted) {
241
263
  serveStarted = true;
242
264
  serve();
@@ -293,47 +315,13 @@ export async function runWatch(options: {
293
315
  if (categories.has('config')) {
294
316
  log.event`Site config changed, restarting`;
295
317
  siteVariables = getDevSiteVariables();
296
- processedExtSet = new Set(
297
- getProcessedExtensions(
298
- Object.keys(siteVariables.codeLanguages || {}),
299
- ),
300
- );
301
- contentRenderer = new ContentRenderer(siteVariables);
302
- changeDetector = new ContentChangeDetector(siteVariables);
303
- if (isFeatureEnabled(siteVariables, 'search')) {
304
- pagefindRunner = new WatchPagefindRunner(siteVariables);
305
- }
306
- compileTemplates(siteVariables);
307
- await contentRenderer.initHighlighter();
308
-
309
- assetFiles = [
310
- ...(await bundle(siteVariables, { mode: 'development' })),
311
- ...(await bundleReloadClient()),
312
- ];
313
-
314
- copyFonts(distDir);
315
- copyKatexAssets(distDir);
316
318
 
317
- if (isFeatureEnabled(siteVariables, 'favicon')) {
318
- await generateFavicons(siteVariables, distDir);
319
- generateWebAppManifest(siteVariables, distDir);
320
- }
321
-
322
- const result = contentRenderer.processContent({ distDir, assetFiles });
319
+ const result = await buildFromConfig(false);
323
320
  for (const err of result.errors) {
324
321
  log.error`${err.message}`;
325
322
  }
326
323
  if (result.errors.length === 0) {
327
- printFlair();
328
- if (!serveStarted) {
329
- serveStarted = true;
330
- serve();
331
- }
332
- broadcast('reload');
333
- if (pagefindRunner) {
334
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
335
- setImmediate(() => pagefindRunner!.run());
336
- }
324
+ onBuildSuccess(result);
337
325
  succeeded = true;
338
326
  }
339
327
  return;
@@ -341,18 +329,8 @@ export async function runWatch(options: {
341
329
 
342
330
  // Public file changed, copy just that file
343
331
  if (categories.has('public') && !categories.has('content')) {
344
- for (const filePath of changes) {
345
- if (classifyChange(filePath) === 'public') {
346
- const absPath = path.resolve(filePath);
347
- if (fs.existsSync(absPath)) {
348
- copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
349
- const rel = toPosix(path.relative(publicDir, absPath));
350
- publicRelPaths.add(rel);
351
- }
352
- }
353
- }
354
- printFlair();
355
- broadcast('reload');
332
+ copyChangedPublicFiles(changes);
333
+ onBuildSuccess();
356
334
  succeeded = true;
357
335
  return;
358
336
  }
@@ -381,16 +359,7 @@ export async function runWatch(options: {
381
359
 
382
360
  // Copy any changed public files too
383
361
  if (categories.has('public')) {
384
- for (const filePath of changes) {
385
- if (classifyChange(filePath) === 'public') {
386
- const absPath = path.resolve(filePath);
387
- if (fs.existsSync(absPath)) {
388
- copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
389
- const rel = toPosix(path.relative(publicDir, absPath));
390
- publicRelPaths.add(rel);
391
- }
392
- }
393
- }
362
+ copyChangedPublicFiles(changes);
394
363
  }
395
364
 
396
365
  // Copy any changed non-processed content files (images, PDFs, etc.)
@@ -428,17 +397,7 @@ export async function runWatch(options: {
428
397
  }
429
398
 
430
399
  if (result.errors.length === 0) {
431
- printFlair();
432
- if (!serveStarted) {
433
- serveStarted = true;
434
- serve();
435
- }
436
- broadcast('reload');
437
-
438
- if (pagefindRunner) {
439
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
440
- setImmediate(() => pagefindRunner!.run());
441
- }
400
+ onBuildSuccess(result);
442
401
  succeeded = true;
443
402
  }
444
403
  } catch (err) {
@@ -511,11 +470,10 @@ export async function runWatch(options: {
511
470
  watcher.on('add', onFileChange);
512
471
  watcher.on('unlink', onFileChange);
513
472
 
514
- let configWatcherReady = false;
515
- let contentWatcherReady = false;
473
+ let watchersReady = 0;
516
474
 
517
475
  function onWatcherReady(): void {
518
- if (!configWatcherReady || !contentWatcherReady) {
476
+ if (++watchersReady < 2) {
519
477
  return;
520
478
  }
521
479
  watcherReady = true;
@@ -526,15 +484,8 @@ export async function runWatch(options: {
526
484
  log.info`Watching for changes...`;
527
485
  }
528
486
 
529
- configWatcher.on('ready', () => {
530
- configWatcherReady = true;
531
- onWatcherReady();
532
- });
533
-
534
- watcher.on('ready', () => {
535
- contentWatcherReady = true;
536
- onWatcherReady();
537
- });
487
+ configWatcher.on('ready', onWatcherReady);
488
+ watcher.on('ready', onWatcherReady);
538
489
  });
539
490
 
540
491
  // Return a promise that never resolves (watch mode runs until killed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  "devDependencies": {
80
80
  "@eslint/js": "^10.0.1",
81
- "@playwright/test": "1.56.1",
81
+ "@playwright/test": "^1.59.1",
82
82
  "@saintedlama/lcov-parse": "^1.0.1",
83
83
  "@types/bun": "^1.3.10",
84
84
  "@types/culori": "^4.0.1",
package/src/_base.scss CHANGED
@@ -106,6 +106,7 @@ pre {
106
106
  padding: var(--gap);
107
107
 
108
108
  code {
109
+ font-size: inherit;
109
110
  padding: 0;
110
111
  }
111
112
  }
@@ -60,6 +60,7 @@ body.literate .literate-code-output {
60
60
 
61
61
  @media (width >= 924px) {
62
62
  grid-template-columns: 1fr 1fr;
63
+ align-items: start;
63
64
 
64
65
  > pre:last-child {
65
66
  min-width: 150px;
@@ -30,10 +30,25 @@
30
30
 
31
31
  pre,
32
32
  tt,
33
- :not(pre) > code {
33
+ code {
34
34
  border: 1px solid #ccc;
35
35
  }
36
36
 
37
+ pre {
38
+ white-space: pre-wrap;
39
+ }
40
+
41
+ pre code {
42
+ border: none;
43
+ }
44
+
45
+ .code-body pre,
46
+ .code-prose-content code,
47
+ .file-header tt,
48
+ .file-header code {
49
+ border: none;
50
+ }
51
+
37
52
  // .alert strips the default padding from inline code (since it also
38
53
  // strips the background). In print we add a border, so restore the
39
54
  // padding so the border doesn't hug the text.
@@ -53,7 +53,6 @@
53
53
  --font-weight: 400;
54
54
  --line-height: 1.7;
55
55
 
56
- // Use em units to specify a size relative to normal body text
57
56
  --mono-font-size: 0.94em;
58
57
  --mono-line-height: 1.5;
59
58
  --mono-border-radius: 0.375em;