@abreen/tada 1.9.0 → 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 (73) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/build/apply-base-path-plugin.ts +79 -17
  4. package/build/bundle.ts +9 -1
  5. package/build/content-watch.ts +3 -3
  6. package/build/copy.ts +30 -0
  7. package/build/generate-content-assets.ts +45 -59
  8. package/build/pagefind.ts +0 -7
  9. package/build/serve.ts +4 -0
  10. package/build/types.d.ts +19 -1
  11. package/build/util.ts +3 -0
  12. package/build/utils/content-files.ts +98 -0
  13. package/build/utils/include.ts +4 -0
  14. package/build/utils/python-runner/trace_runner.py +272 -0
  15. package/build/utils/render.ts +34 -8
  16. package/build/utils/trace-core.ts +203 -0
  17. package/build/utils/trace-java.ts +132 -0
  18. package/build/utils/trace-layout.ts +31 -3
  19. package/build/utils/trace-python.ts +26 -0
  20. package/build/utils/trace.ts +174 -294
  21. package/build/validate-internal-links-plugin.ts +16 -4
  22. package/build/watch/assets.ts +111 -0
  23. package/build/watch/build-full.ts +124 -0
  24. package/build/watch/build-incremental.ts +139 -0
  25. package/build/watch/compiler-types.d.ts +19 -0
  26. package/build/watch/compiler.ts +112 -0
  27. package/build/watch/index.ts +22 -0
  28. package/build/watch/mutations.ts +75 -0
  29. package/build/watch/planner.ts +230 -0
  30. package/build/watch/records.ts +190 -0
  31. package/build/watch/runtime.ts +143 -0
  32. package/build/watch/snapshot.ts +602 -0
  33. package/build/watch/validation.ts +36 -0
  34. package/build/watch-reload-client.ts +10 -5
  35. package/init/content/labs/01/index.md +6 -0
  36. package/init/content/labs/01/trace_demo.py +16 -0
  37. package/package.json +42 -23
  38. package/python/command.ts +95 -0
  39. package/python/module.ts +18 -0
  40. package/src/_alerts.scss +11 -8
  41. package/src/_base.scss +25 -29
  42. package/src/_layout.scss +17 -14
  43. package/src/_mixins.scss +10 -9
  44. package/src/anchor/style.scss +4 -2
  45. package/src/code/index.ts +9 -2
  46. package/src/code/style.scss +14 -13
  47. package/src/code.scss +14 -13
  48. package/src/header/_base.scss +21 -24
  49. package/src/header/style.scss +16 -12
  50. package/src/index.ts +3 -0
  51. package/src/literate/style.scss +4 -4
  52. package/src/navigate/index.ts +43 -279
  53. package/src/navigate/lifecycle.ts +1 -1
  54. package/src/navigate/runtime.ts +341 -0
  55. package/src/page-update/README.md +4 -0
  56. package/src/page-update/index.ts +280 -0
  57. package/src/page-update/style.scss +53 -0
  58. package/src/print/style.scss +4 -4
  59. package/src/question/style.scss +10 -6
  60. package/src/search/index.ts +9 -12
  61. package/src/search/style.scss +44 -42
  62. package/src/style.scss +63 -58
  63. package/src/timezone/style.scss +5 -5
  64. package/src/toc/style.scss +17 -16
  65. package/src/trace/style.scss +27 -27
  66. package/src/validators.ts +45 -0
  67. package/templates/_footer.html +1 -1
  68. package/templates/_theme.scss +8 -19
  69. package/watch/engine.ts +296 -0
  70. package/watch/events.ts +10 -0
  71. package/watch/fs-commit.ts +102 -0
  72. package/watch/types.d.ts +103 -0
  73. package/build/watch.ts +0 -493
package/README.md CHANGED
@@ -9,6 +9,7 @@ A static site generator. The successor to Presto.
9
9
 
10
10
  - Modern design (light & dark following system, floating header, styled lists)
11
11
  - Fast client-side navigation
12
+ - Detects a new version of the current page and allows the user to reload
12
13
  - Clickable/linkable landmarks (headings, deflists, alert boxes)
13
14
  - Dynamic table of contents
14
15
  * Floats on the side of the screen when window is large enough
@@ -21,8 +22,11 @@ A static site generator. The successor to Presto.
21
22
  * Dynamic table of contents for each method/function
22
23
  * Converts new Markdown comment syntax ([added in Java 23][jep467]) to HTML
23
24
  * Indexed by Pagefind (classes, interfaces, methods, and fields)
25
+ - Interactive execution traces via `renderTrace()`
26
+ * Supports Java and Python source files
27
+ * Renders step-by-step source highlighting, output, and memory diagrams
24
28
  - PDF files are copied into `dist/`
25
- - Text of each PDF page is extracted using [mutool][mutool] and indexed
29
+ - Text of each PDF page is extracted using `mutool` (if present) and indexed
26
30
  - External link handling (special visual treatment for external links)
27
31
  - Internal link validation at build time (broken links fail the build)
28
32
  - Internal links automatically prefixed with base path, if specified
@@ -38,16 +42,31 @@ A static site generator. The successor to Presto.
38
42
  - Automatically generated favicon
39
43
  * Text, color, font and font weight taken from config file
40
44
 
45
+ ## Prerequisites
46
+
47
+ - [Bun](https://bun.com/)
48
+ - [MuPDF](https://mupdf.com/) (optional)
49
+ - macOS (using [Homebrew][homebrew]): `brew install mupdf-tools`
50
+ - Fedora Linux: `sudo dnf install mupdf`
51
+ - Ubuntu Linux: `sudo apt-get install mupdf-tools`
52
+ - Windows: `winget install mutool`
53
+
54
+ > [!NOTE]
55
+ > You may skip MuPDF if you don't need search results to include links to PDF
56
+ > pages. You can also turn off `features.search` in the config to disable
57
+ > search entirely.
58
+
59
+ If you plan to use `renderTrace()` to generate traces of Java or Python code,
60
+ the appropriate compiler/interpreter must be installed.
41
61
 
42
62
  ## Installation
43
63
 
44
- Install [Bun](https://bun.sh/), then install Tada globally:
64
+ Install Tada globally:
45
65
 
46
66
  ```
47
- bun add -g @abreen/tada
67
+ bun install --global @abreen/tada
48
68
  ```
49
69
 
50
-
51
70
  ## Quick start
52
71
 
53
72
  Create a new site:
@@ -69,7 +88,6 @@ tada serve
69
88
 
70
89
  Visit [http://localhost:8080/index.html](http://localhost:8080/index.html).
71
90
 
72
-
73
91
  ## CLI commands
74
92
 
75
93
  ### `tada init <dirname>`
@@ -92,30 +110,25 @@ You can also override specific defaults with flags:
92
110
  Available flags: `--title`, `--symbol`, `--theme-color`, `--tint-hue`,
93
111
  `--tint-amount`, `--default-time-zone`, `--prod-base`, `--prod-base-path`.
94
112
 
95
-
96
113
  ### `tada dev`
97
114
 
98
115
  Build the site for local development (using `site.dev.json`)
99
116
  into the `dist/` directory.
100
117
 
101
-
102
118
  ### `tada serve`
103
119
 
104
120
  Start a development web server at `http://localhost:8080` which serves the
105
121
  files in the `dist/` directory.
106
122
 
107
-
108
123
  ### `tada watch`
109
124
 
110
125
  Start a development web server, watch for changes and rebuild automatically.
111
126
 
112
-
113
127
  ### `tada clean`
114
128
 
115
129
  Remove the `dist/` directory. Pass `--prod` to also prune old production
116
130
  builds (keeps the latest two versions).
117
131
 
118
-
119
132
  ### `tada prod`
120
133
 
121
134
  Build the site for production (uses `site.prod.json`). Each prod build is
@@ -123,7 +136,6 @@ saved to a versioned directory under `dist-prod/` (e.g., `dist-prod/v1/`,
123
136
  `dist-prod/v2/`) with a manifest file that records the SHA-256 hash of every
124
137
  output file.
125
138
 
126
-
127
139
  ### `tada diff`
128
140
 
129
141
  Compare two production builds and list added, changed, and removed files.
@@ -139,7 +151,6 @@ Use `--copy <dir>` to copy only the changed and added files to a directory:
139
151
 
140
152
  The output directory will also include a `manifest.json` for the newer build.
141
153
 
142
-
143
154
  ## Development vs. production builds
144
155
 
145
156
  `tada dev` and `tada watch` build to the `dist/` directory using
@@ -151,7 +162,6 @@ for previewing a `tada dev` build. Dev builds overwrite `dist/` each time.
151
162
  it runs. Previous production builds are preserved, so you can compare any
152
163
  two versions with `tada diff`.
153
164
 
154
-
155
165
  ## Deploying to S3
156
166
 
157
167
  If you host your site in an S3 bucket, `tada diff --copy` lets you upload
@@ -176,20 +186,6 @@ tada diff --copy upload/
176
186
  If `tada diff` reports removed files, delete those from your S3 bucket
177
187
  manually.
178
188
 
179
-
180
- ## Prerequisites
181
-
182
- - [Bun](https://bun.sh/)
183
- - [MuPDF](https://mupdf.com/) (optional, for PDF text extraction in search)
184
- - On macOS: `brew install mupdf-tools`
185
- - On Fedora: `dnf install mupdf`
186
- - On Ubuntu: `apt-get install mupdf-tools`
187
-
188
- > You may skip MuPDF if you don't need search results to include links to PDF
189
- > pages. You can also turn off `features.search` in the config to disable
190
- > search entirely.
191
-
192
-
193
189
  ## Configuration
194
190
 
195
191
  Build-time site config lives in:
@@ -241,7 +237,6 @@ Example site configuration JSON file:
241
237
  | `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
242
238
  | `vars` | Arbitrary key/value variables exposed to templates/content as `vars.*` (e.g., `<%= vars.staffEmail %>`) |
243
239
 
244
-
245
240
  #### `nav.json`
246
241
 
247
242
  Defines the site navigation structure. The file contains an array of section
@@ -275,7 +270,6 @@ but three or more sections are supported.
275
270
  ]
276
271
  ```
277
272
 
278
-
279
273
  #### `authors.json`
280
274
 
281
275
  Maps author handles (used in front matter `author` fields) to display names
@@ -293,7 +287,6 @@ with `name`, `avatar`, and optionally `url`.
293
287
  }
294
288
  ```
295
289
 
296
-
297
290
  ## Content
298
291
 
299
292
  Site content lives in the `content/` directory. Markdown is converted to HTML.
@@ -304,7 +297,6 @@ Any other kinds of files are copied into `dist/` in the same locations.
304
297
  All files in `public/` are copied directly into `dist/` with zero processing.
305
298
  Files in `public/` are **not** included in the search index.
306
299
 
307
-
308
300
  ### Front matter fields
309
301
 
310
302
  Each file in `content/` should start with "front matter" (a YAML-formatted
@@ -323,7 +315,6 @@ list of variables parsed using the [`front-matter`][front-matter] library).
323
315
  You may also add arbitrary fields in a page's front matter, and access them
324
316
  using Lodash syntax (see below).
325
317
 
326
-
327
318
  ### Variable substitution
328
319
 
329
320
  Plain text content (e.g., HTML and Markdown) are processed using [Lodash
@@ -335,11 +326,10 @@ templates][lodash].
335
326
  under `vars` (e.g., `<%= vars.staffEmail %>`)
336
327
 
337
328
 
338
-
339
329
  [inter]: https://fonts.google.com/specimen/Inter
340
330
  [lodash]: https://lodash.info/doc/template
341
331
  [front-matter]: https://www.npmjs.com/package/front-matter
342
332
  [pagefind]: https://pagefind.app/
343
- [mutool]: https://mupdf.readthedocs.io/en/latest/tools/mutool.html
344
333
  [jep467]: https://openjdk.org/jeps/467
345
334
  [katex]: https://katex.org/
335
+ [homebrew]: https://brew.sh/
package/bin/tada.ts CHANGED
@@ -621,7 +621,7 @@ try {
621
621
  case 'watch': {
622
622
  requireSiteConfig('dev');
623
623
  const { runWatch } = await import(
624
- path.join(packageDir, 'build/watch.ts')
624
+ path.join(packageDir, 'build/watch/index.ts')
625
625
  );
626
626
  await runWatch({
627
627
  httpPort: parsePortArg('--port'),
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import type MarkdownIt from 'markdown-it';
3
3
  import type Token from 'markdown-it/lib/token.mjs';
4
+ import { JSDOM } from 'jsdom';
4
5
  import { createApplyBasePath } from './utils/paths';
5
6
  import { isFeatureEnabled } from './features';
6
7
  import { isInternalLink } from './utils/link';
@@ -9,6 +10,47 @@ import type { SiteVariables } from './types';
9
10
 
10
11
  const log = makeLogger(import.meta.url);
11
12
 
13
+ function serializeStartTag(element: Element): string {
14
+ const attrs = Array.from(element.attributes).map(attr =>
15
+ attr.value === '' ? attr.name : `${attr.name}="${attr.value}"`,
16
+ );
17
+ return `<${element.tagName.toLowerCase()}${attrs.length > 0 ? ` ${attrs.join(' ')}` : ''}>`;
18
+ }
19
+
20
+ function isStandaloneOpeningAnchorTag(html: string): boolean {
21
+ if (!/^<a\b/i.test(html)) {
22
+ return false;
23
+ }
24
+
25
+ let i = 2;
26
+ let quote: '"' | "'" | null = null;
27
+
28
+ while (i < html.length) {
29
+ const char = html[i];
30
+ if (quote) {
31
+ if (char === quote) {
32
+ quote = null;
33
+ }
34
+ i += 1;
35
+ continue;
36
+ }
37
+
38
+ if (char === '"' || char === "'") {
39
+ quote = char;
40
+ i += 1;
41
+ continue;
42
+ }
43
+
44
+ if (char === '>') {
45
+ return html.slice(i + 1).trim() === '';
46
+ }
47
+
48
+ i += 1;
49
+ }
50
+
51
+ return false;
52
+ }
53
+
12
54
  interface ApplyBasePathOptions {
13
55
  literateJavaOutputPaths?: Set<string>;
14
56
  sourceUrlPath?: string;
@@ -91,25 +133,45 @@ export default function applyBasePathPlugin(
91
133
  token.attrSet('src', afterApply);
92
134
  }
93
135
  } else if (token.type === 'html_block' || token.type === 'html_inline') {
94
- token.content = token.content
95
- .replace(
96
- /(<a\b[^>]*\bhref=")(\/)([^"]*")/g,
97
- (match, prefix, slash, rest) => {
98
- const href = slash + rest.slice(0, -1);
136
+ if (isStandaloneOpeningAnchorTag(token.content)) {
137
+ const dom = new JSDOM(`<body>${token.content}__tada__</a></body>`);
138
+ const anchor = dom.window.document.body.querySelector('a[href]');
139
+ const href = anchor?.getAttribute('href');
140
+ if (anchor && href && href.startsWith('/')) {
141
+ const afterApply = applyBasePath(href);
142
+ log.debug`Applying base path to raw HTML anchor: ${href} -> ${afterApply}`;
143
+ anchor.setAttribute('href', afterApply);
144
+ token.content = serializeStartTag(anchor);
145
+ }
146
+ } else {
147
+ const dom = new JSDOM(`<body>${token.content}</body>`);
148
+ const { document } = dom.window;
149
+ let changed = false;
150
+
151
+ for (const anchor of document.body.querySelectorAll('a[href]')) {
152
+ const href = anchor.getAttribute('href');
153
+ if (href && href.startsWith('/')) {
99
154
  const afterApply = applyBasePath(href);
100
- log.debug`Applying base path to a tag href: ${href} -> ${afterApply}`;
101
- return prefix + afterApply + '"';
102
- },
103
- )
104
- .replace(
105
- /(<img\b[^>]*\bsrc=")(\/)([^"]*")/g,
106
- (match, prefix, slash, rest) => {
107
- const src = slash + rest.slice(0, -1);
155
+ log.debug`Applying base path to raw HTML anchor: ${href} -> ${afterApply}`;
156
+ anchor.setAttribute('href', afterApply);
157
+ changed = true;
158
+ }
159
+ }
160
+
161
+ for (const image of document.body.querySelectorAll('img[src]')) {
162
+ const src = image.getAttribute('src');
163
+ if (src && src.startsWith('/')) {
108
164
  const afterApply = applyBasePath(src);
109
- log.debug`Applying base path to img tag src: ${src} -> ${afterApply}`;
110
- return prefix + afterApply + '"';
111
- },
112
- );
165
+ log.debug`Applying base path to raw HTML image: ${src} -> ${afterApply}`;
166
+ image.setAttribute('src', afterApply);
167
+ changed = true;
168
+ }
169
+ }
170
+
171
+ if (changed) {
172
+ token.content = document.body.innerHTML;
173
+ }
174
+ }
113
175
  }
114
176
 
115
177
  token.children?.forEach(checkAndApplyBasePath);
package/build/bundle.ts CHANGED
@@ -14,6 +14,14 @@ import type { PluginBuilder } from 'bun';
14
14
  import type { SiteVariables } from './types';
15
15
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
16
16
 
17
+ function formatCssNumber(value: number, precision = 4): string {
18
+ if (Number.isInteger(value)) {
19
+ return String(value);
20
+ }
21
+
22
+ return value.toFixed(precision).replace(/\.?0+$/, '');
23
+ }
24
+
17
25
  function renderThemeScss(siteVariables: SiteVariables): string {
18
26
  const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
19
27
  const template = fs.readFileSync(templatePath, 'utf-8');
@@ -27,7 +35,7 @@ function renderThemeScss(siteVariables: SiteVariables): string {
27
35
  const iconColorHoverDark = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 60%)`;
28
36
  const iconColorTranslucentDark = `hsl(${tintHue}deg ${(85 * tintAmount) / 100}% 90%)`;
29
37
 
30
- const linkHue = deriveLinkHue(tintHue);
38
+ const linkHue = formatCssNumber(deriveLinkHue(tintHue));
31
39
  const linkColor = `hsl(${linkHue}deg 44.4% 49.4%)`;
32
40
  const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
33
41
  const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
@@ -85,10 +85,10 @@ export class ContentChangeDetector {
85
85
  }
86
86
  }
87
87
 
88
- const changedJavaFiles = [...changedContentFiles].filter(f =>
89
- f.endsWith('.java'),
88
+ const changedTraceFiles = [...changedContentFiles].filter(
89
+ f => f.endsWith('.java') || f.endsWith('.py'),
90
90
  );
91
- for (const filePath of changedJavaFiles) {
91
+ for (const filePath of changedTraceFiles) {
92
92
  log.event`${B`${path.basename(filePath)}`} changed, re-running traces`;
93
93
  }
94
94
 
package/build/copy.ts CHANGED
@@ -102,6 +102,36 @@ function copySingleFile(
102
102
  log.info`Copying ${sourceLabel} file ${B`${rel}`}`;
103
103
  }
104
104
 
105
+ export function deleteOutputPath(distDir: string, relPath: string): void {
106
+ const root = path.resolve(distDir);
107
+ const target = path.resolve(distDir, relPath);
108
+ const relative = path.relative(root, target);
109
+ if (
110
+ relative.startsWith('..') ||
111
+ path.isAbsolute(relative) ||
112
+ relative.length === 0
113
+ ) {
114
+ throw new Error(`refusing to delete path outside dist/: ${relPath}`);
115
+ }
116
+
117
+ fs.rmSync(target, { force: true });
118
+ log.info`Removing output ${B`${toPosix(relPath)}`}`;
119
+
120
+ let currentDir = path.dirname(target);
121
+ while (currentDir !== root) {
122
+ try {
123
+ fs.rmdirSync(currentDir);
124
+ } catch (err) {
125
+ const code = (err as NodeJS.ErrnoException).code;
126
+ if (code === 'ENOTEMPTY' || code === 'ENOENT') {
127
+ break;
128
+ }
129
+ throw err;
130
+ }
131
+ currentDir = path.dirname(currentDir);
132
+ }
133
+ }
134
+
105
135
  export function copyPublicFile(
106
136
  publicDir: string,
107
137
  distDir: string,
@@ -15,13 +15,14 @@ import {
15
15
  renderPlainTextPageAsset,
16
16
  } from './util';
17
17
  import { isLiterateJava } from './utils/file-types';
18
- import { checkJavac } from './utils/literate-java';
18
+ import { checkTraceToolAvailability, isTraceSourceFile } from './utils/trace';
19
19
  import { toPosix } from './utils/paths';
20
20
  import type {
21
21
  SiteVariables,
22
22
  Asset,
23
23
  ContentRenderOptions,
24
24
  ContentRenderResult,
25
+ TraceToolAvailability,
25
26
  WatchState,
26
27
  } from './types';
27
28
 
@@ -33,6 +34,7 @@ export class ContentRenderer {
33
34
  private sourceFileCache: Map<string, Asset[]>;
34
35
  private lastBuildFiles: Set<string>;
35
36
  private javacAvailable: boolean | undefined;
37
+ private traceToolAvailability: TraceToolAvailability | undefined;
36
38
  private traceCache: Map<
37
39
  string,
38
40
  {
@@ -78,12 +80,10 @@ export class ContentRenderer {
78
80
  return new Set(buildContentFiles);
79
81
  }
80
82
 
81
- // If any .java file changed, the trace cache will be stale, so rebuild
83
+ // If any trace source changed, the trace cache will be stale, so rebuild
82
84
  // all content pages to pick up the new trace output.
83
- const javaFileChanged = [...changedContentFiles].some(f =>
84
- f.endsWith('.java'),
85
- );
86
- if (javaFileChanged) {
85
+ const traceFileChanged = [...changedContentFiles].some(isTraceSourceFile);
86
+ if (traceFileChanged) {
87
87
  return new Set(buildContentFiles);
88
88
  }
89
89
 
@@ -103,28 +103,6 @@ export class ContentRenderer {
103
103
  return Object.prototype.hasOwnProperty.call(codeExtensions, ext.slice(1));
104
104
  }
105
105
 
106
- getDirtyCopiedSourceFiles(
107
- buildContentFiles: string[],
108
- { changedContentFiles }: WatchState = {},
109
- ): string[] {
110
- if (this.lastBuildFiles.size === 0 || !changedContentFiles) {
111
- return buildContentFiles.filter(filePath =>
112
- this.isCopiedAssetSource(filePath),
113
- );
114
- }
115
-
116
- const buildFileSet = new Set(buildContentFiles);
117
- const dirtyCopiedSourceFiles: string[] = [];
118
- for (const filePath of changedContentFiles) {
119
- if (!buildFileSet.has(filePath) || !this.isCopiedAssetSource(filePath)) {
120
- continue;
121
- }
122
- dirtyCopiedSourceFiles.push(filePath);
123
- }
124
-
125
- return dirtyCopiedSourceFiles;
126
- }
127
-
128
106
  renderSourceAssets(
129
107
  filePath: string,
130
108
  contentDir: string,
@@ -163,7 +141,7 @@ export class ContentRenderer {
163
141
  assetFiles,
164
142
  literateJavaOutputPaths,
165
143
  traceCache: this.traceCache,
166
- javacAvailable: this.javacAvailable,
144
+ traceToolAvailability: this.traceToolAvailability,
167
145
  }),
168
146
  );
169
147
  return assets;
@@ -190,27 +168,23 @@ export class ContentRenderer {
190
168
  return assets;
191
169
  }
192
170
 
193
- writeUncachedAssets(
194
- distDir: string,
195
- copiedSourceFiles: string[],
196
- contentDir: string,
197
- ): void {
198
- for (const filePath of copiedSourceFiles) {
199
- for (const asset of renderCopiedContentAsset({
200
- filePath,
201
- contentDir,
202
- siteVariables: this.siteVariables,
203
- })) {
204
- const outPath = path.join(distDir, asset.assetPath);
205
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
206
- fs.writeFileSync(outPath, asset.content);
207
- }
171
+ renderCopiedSourceAssets(filePath: string, contentDir: string): Asset[] {
172
+ if (!this.isCopiedAssetSource(filePath)) {
173
+ return [];
208
174
  }
175
+
176
+ return renderCopiedContentAsset({
177
+ filePath,
178
+ contentDir,
179
+ siteVariables: this.siteVariables,
180
+ });
209
181
  }
210
182
 
211
183
  updateSourceCache(
212
184
  filePath: string,
213
185
  assets: Asset[],
186
+ changedOutputRelPaths: Set<string>,
187
+ removedOutputRelPaths: Set<string>,
214
188
  changedHtmlAssetPaths: Set<string>,
215
189
  removedHtmlAssetPaths: Set<string>,
216
190
  ): void {
@@ -221,12 +195,14 @@ export class ContentRenderer {
221
195
  if (nextAssetPaths.has(asset.assetPath)) {
222
196
  continue;
223
197
  }
198
+ removedOutputRelPaths.add(asset.assetPath);
224
199
  if (asset.assetPath.endsWith('.html')) {
225
200
  removedHtmlAssetPaths.add(asset.assetPath);
226
201
  }
227
202
  }
228
203
 
229
204
  for (const asset of assets) {
205
+ changedOutputRelPaths.add(asset.assetPath);
230
206
  if (asset.assetPath.endsWith('.html')) {
231
207
  changedHtmlAssetPaths.add(asset.assetPath);
232
208
  }
@@ -248,6 +224,7 @@ export class ContentRenderer {
248
224
 
249
225
  pruneRemovedSources(
250
226
  buildFileSet: Set<string>,
227
+ removedOutputRelPaths: Set<string>,
251
228
  removedHtmlAssetPaths: Set<string>,
252
229
  ): void {
253
230
  for (const filePath of [...this.lastBuildFiles]) {
@@ -257,6 +234,7 @@ export class ContentRenderer {
257
234
 
258
235
  const previousAssets = this.getCachedAssets(filePath);
259
236
  for (const asset of previousAssets) {
237
+ removedOutputRelPaths.add(asset.assetPath);
260
238
  if (asset.assetPath.endsWith('.html')) {
261
239
  removedHtmlAssetPaths.add(asset.assetPath);
262
240
  }
@@ -280,10 +258,8 @@ export class ContentRenderer {
280
258
  buildContentFiles,
281
259
  watchState,
282
260
  );
283
- const dirtyCopiedSourceFiles = this.getDirtyCopiedSourceFiles(
284
- buildContentFiles,
285
- watchState,
286
- );
261
+ const changedOutputRelPaths = new Set<string>();
262
+ const removedOutputRelPaths = new Set<string>();
287
263
  const changedHtmlAssetPaths = new Set<string>();
288
264
  const removedHtmlAssetPaths = new Set<string>();
289
265
  const validInternalTargets = getValidInternalTargets(
@@ -312,7 +288,11 @@ export class ContentRenderer {
312
288
  errors.push(new Error(msg));
313
289
  }
314
290
 
315
- this.pruneRemovedSources(buildFileSet, removedHtmlAssetPaths);
291
+ this.pruneRemovedSources(
292
+ buildFileSet,
293
+ removedOutputRelPaths,
294
+ removedHtmlAssetPaths,
295
+ );
316
296
 
317
297
  if (dirtySourceFiles.size > 0) {
318
298
  const noun = dirtySourceFiles.size === 1 ? 'file' : 'files';
@@ -320,7 +300,8 @@ export class ContentRenderer {
320
300
  }
321
301
 
322
302
  if (this.javacAvailable === undefined && dirtySourceFiles.size > 0) {
323
- this.javacAvailable = checkJavac();
303
+ this.traceToolAvailability = checkTraceToolAvailability();
304
+ this.javacAvailable = this.traceToolAvailability.java;
324
305
  if (!this.javacAvailable) {
325
306
  log.warn`javac was not found; literate Java pages will not include execution output`;
326
307
  }
@@ -329,14 +310,17 @@ export class ContentRenderer {
329
310
  for (const filePath of dirtySourceFiles) {
330
311
  let assets: Asset[];
331
312
  try {
332
- assets = this.renderSourceAssets(
333
- filePath,
334
- contentDir,
335
- distDir,
336
- validInternalTargets,
337
- assetFiles,
338
- literateJavaOutputPaths,
339
- );
313
+ assets = [
314
+ ...this.renderSourceAssets(
315
+ filePath,
316
+ contentDir,
317
+ distDir,
318
+ validInternalTargets,
319
+ assetFiles,
320
+ literateJavaOutputPaths,
321
+ ),
322
+ ...this.renderCopiedSourceAssets(filePath, contentDir),
323
+ ];
340
324
  } catch (err) {
341
325
  errors.push(err as Error);
342
326
  continue;
@@ -344,13 +328,14 @@ export class ContentRenderer {
344
328
  this.updateSourceCache(
345
329
  filePath,
346
330
  assets,
331
+ changedOutputRelPaths,
332
+ removedOutputRelPaths,
347
333
  changedHtmlAssetPaths,
348
334
  removedHtmlAssetPaths,
349
335
  );
350
336
  }
351
337
 
352
338
  this.writeCachedAssets(distDir, buildContentFiles);
353
- this.writeUncachedAssets(distDir, dirtyCopiedSourceFiles, contentDir);
354
339
  this.lastBuildFiles = buildFileSet;
355
340
 
356
341
  // Collect HTML asset content for Pagefind
@@ -368,6 +353,7 @@ export class ContentRenderer {
368
353
  errors,
369
354
  changedHtmlAssetPaths,
370
355
  removedHtmlAssetPaths,
356
+ removedOutputRelPaths,
371
357
  htmlAssetsByPath,
372
358
  buildContentFiles,
373
359
  };
package/build/pagefind.ts CHANGED
@@ -245,13 +245,6 @@ export async function runPagefind({
245
245
  pdfSourceByOutputPath,
246
246
  });
247
247
 
248
- try {
249
- const pagefind = await getPagefind();
250
- await pagefind.close();
251
- } catch {
252
- // Best-effort cleanup
253
- }
254
-
255
248
  const finishedAt = Date.now();
256
249
  log.debug`Search index built in ${finishedAt - snapshotReadyAt}ms (${finishedAt - start}ms total)`;
257
250
  }
package/build/serve.ts CHANGED
@@ -62,6 +62,10 @@ export function startServer(options: StartServerOptions): void {
62
62
  return new Response(null, { status: 304, headers });
63
63
  }
64
64
 
65
+ if (req.method === 'HEAD') {
66
+ return new Response(null, { headers });
67
+ }
68
+
65
69
  return new Response(Bun.file(result.filePath), { headers });
66
70
  }
67
71