@abreen/tada 1.1.0 → 1.2.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.
Files changed (88) hide show
  1. package/README.md +68 -16
  2. package/bin/tada.ts +289 -24
  3. package/bin/validators.test.ts +4 -3
  4. package/bin/validators.ts +4 -2
  5. package/build/apply-base-path-plugin.ts +24 -13
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +19 -20
  9. package/build/code.test.ts +7 -3
  10. package/build/content-watch.ts +6 -6
  11. package/build/copy.ts +2 -2
  12. package/build/deflist-id-plugin.ts +1 -1
  13. package/build/external-links-plugin.ts +2 -2
  14. package/build/features.ts +1 -1
  15. package/build/generate-content-assets.ts +12 -7
  16. package/build/generate-favicon.ts +13 -6
  17. package/build/generate-fonts.ts +2 -2
  18. package/build/generate-katex-assets.ts +35 -0
  19. package/build/generate-manifest.ts +2 -2
  20. package/build/globals.test.ts +9 -11
  21. package/build/globals.ts +6 -7
  22. package/build/json-schema.test.ts +1 -1
  23. package/build/log.test.ts +1 -1
  24. package/build/log.ts +2 -2
  25. package/build/markdown-plugins.test.ts +118 -10
  26. package/build/pagefind.test.ts +2 -2
  27. package/build/pagefind.ts +5 -5
  28. package/build/pdf-text.test.ts +52 -0
  29. package/build/pipeline.ts +42 -15
  30. package/build/reachability.test.ts +1 -1
  31. package/build/serve.ts +3 -3
  32. package/build/site-variables.ts +13 -5
  33. package/build/templates.ts +7 -6
  34. package/build/toc-plugin.test.ts +2 -2
  35. package/build/toc-plugin.ts +1 -1
  36. package/build/types.ts +15 -6
  37. package/build/util.ts +6 -6
  38. package/build/utils/code.ts +5 -5
  39. package/build/utils/content-files.ts +3 -3
  40. package/build/utils/derive-theme.test.ts +1 -1
  41. package/build/utils/derive-theme.ts +1 -1
  42. package/build/utils/file-types.test.ts +1 -1
  43. package/build/utils/front-matter.test.ts +1 -4
  44. package/build/utils/front-matter.ts +2 -2
  45. package/build/utils/literate-java.ts +11 -10
  46. package/build/utils/markdown.ts +41 -9
  47. package/build/utils/paths.test.ts +2 -2
  48. package/build/utils/paths.ts +5 -1
  49. package/build/utils/render.ts +45 -32
  50. package/build/utils/shiki-highlighter.ts +1 -1
  51. package/build/validate-internal-links-plugin.test.ts +1 -1
  52. package/build/validate-internal-links-plugin.ts +1 -1
  53. package/build/watch-reachability-state.test.ts +2 -2
  54. package/build/watch-reachability-state.ts +1 -1
  55. package/build/watch.ts +74 -98
  56. package/{content → init/content}/lectures/index.md +1 -1
  57. package/{content → init/content}/markdown.md +26 -7
  58. package/package.json +9 -7
  59. package/{templates → schema}/nav.schema.json +1 -2
  60. package/{build → schema}/site.schema.json +1 -2
  61. package/src/_alerts.scss +4 -0
  62. package/src/_base.scss +41 -2
  63. package/src/_layout.scss +37 -0
  64. package/src/anchor/index.ts +0 -1
  65. package/src/index.ts +3 -3
  66. package/src/search/index.test.ts +203 -0
  67. package/src/search/index.ts +3 -79
  68. package/src/search/pdf-utils.ts +79 -0
  69. package/src/search/style.scss +0 -14
  70. package/src/style.scss +9 -57
  71. package/src/timezone/index.ts +5 -6
  72. package/src/util.ts +1 -1
  73. package/templates/_top.html +1 -1
  74. package/types/dev.d.ts +1 -3
  75. package/types/site-variables.d.ts +6 -10
  76. package/types/untyped-modules.d.ts +7 -0
  77. package/config/authors.json +0 -1
  78. package/config/nav.json +0 -28
  79. /package/{content → init/content}/index.md +0 -0
  80. /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
  81. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  82. /package/{content → init/content}/lectures/01/demo.py +0 -0
  83. /package/{content → init/content}/lectures/01/index.md +0 -0
  84. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  85. /package/{content → init/content}/problem_sets/index.html +0 -0
  86. /package/{public → init/public}/avatars/alex.jpg +0 -0
  87. /package/{public → init/public}/test.txt +0 -0
  88. /package/{templates → schema}/authors.schema.json +0 -0
package/README.md CHANGED
@@ -23,6 +23,7 @@ A static site generator. The successor to Presto.
23
23
  - Internal link validation at build time (broken links fail the build)
24
24
  - Internal links automatically prefixed with base path, if specified
25
25
  - Time zone chooser (automatically adjusts `<datetime>` elements)
26
+ - LaTeX math rendered at build time via [KaTeX][katex]
26
27
  - Extended Markdown syntax
27
28
  * `<<< details ... <<<` renders a collapsible box
28
29
  * `::: section ... :::` renders a special section with a fancy background
@@ -78,8 +79,13 @@ Create a new Tada site in a new directory. Prompts for:
78
79
  - **Production base URL**: e.g. `https://example.edu`
79
80
  - **Production base path**: e.g. `/cs101` (defaults to `/`)
80
81
 
81
- Pass `--default` to use the default values for all options without being
82
- prompted.
82
+ Pass `--no-interactive` to skip prompts and use default values for all options.
83
+ You can also override specific defaults with flags:
84
+
85
+ tada init mysite --no-interactive --prod-base https://example.edu --prod-base-path /cs101
86
+
87
+ Available flags: `--title`, `--symbol`, `--theme-color`, `--tint-hue`,
88
+ `--tint-amount`, `--default-time-zone`, `--prod-base`, `--prod-base-path`.
83
89
 
84
90
 
85
91
  ### `tada dev`
@@ -101,12 +107,69 @@ Start a development web server, watch for changes and rebuild automatically.
101
107
 
102
108
  ### `tada clean`
103
109
 
104
- Remove the `dist/` directory.
110
+ Remove the `dist/` directory. Pass `--prod` to also prune old production
111
+ builds (keeps the latest two versions).
105
112
 
106
113
 
107
114
  ### `tada prod`
108
115
 
109
- Build the site for production (uses `site.prod.json`).
116
+ Build the site for production (uses `site.prod.json`). Each prod build is
117
+ saved to a versioned directory under `dist-prod/` (e.g., `dist-prod/v1/`,
118
+ `dist-prod/v2/`) with a manifest file that records the SHA-256 hash of every
119
+ output file.
120
+
121
+
122
+ ### `tada diff`
123
+
124
+ Compare two production builds and list added, changed, and removed files.
125
+ With no arguments, compares the last two builds. You can also specify version
126
+ numbers explicitly:
127
+
128
+ tada diff # compare latest two builds
129
+ tada diff 1 3 # compare v1 and v3
130
+
131
+ Use `--copy <dir>` to copy only the changed and added files to a directory:
132
+
133
+ tada diff --copy upload/
134
+
135
+ The output directory will also include a `manifest.json` for the newer build.
136
+
137
+
138
+ ## Development vs. production builds
139
+
140
+ `tada dev` and `tada watch` build to the `dist/` directory using
141
+ `site.dev.json` (typically `localhost` URLs). `tada watch` includes a
142
+ development server with live reload; `tada serve` is a standalone server
143
+ for previewing a `tada dev` build. Dev builds overwrite `dist/` each time.
144
+
145
+ `tada prod` builds to a new versioned directory under `dist-prod/` each time
146
+ it runs. Previous production builds are preserved, so you can compare any
147
+ two versions with `tada diff`.
148
+
149
+
150
+ ## Deploying to S3
151
+
152
+ If you host your site in an S3 bucket, `tada diff --copy` lets you upload
153
+ only the files that changed between prod builds instead of re-uploading
154
+ everything.
155
+
156
+ First time:
157
+
158
+ ```
159
+ tada prod
160
+ # Upload the entire dist-prod/v1/ directory to your S3 bucket
161
+ ```
162
+
163
+ After making changes:
164
+
165
+ ```
166
+ tada prod
167
+ tada diff --copy upload/
168
+ # Upload just the upload/ directory to S3 (only changed files)
169
+ ```
170
+
171
+ If `tada diff` reports removed files, delete those from your S3 bucket
172
+ manually.
110
173
 
111
174
 
112
175
  ## Prerequisites
@@ -224,18 +287,6 @@ with `name`, `avatar`, and optionally `url`.
224
287
  ```
225
288
 
226
289
 
227
- ### Log level
228
-
229
- Set the `TADA_LOG_LEVEL` environment variable to control build log verbosity.
230
- Valid levels (from most to least verbose): `debug`, `info`, `warn`, `error`.
231
- The default level is `info`. To see *all* logs generated by Tada, set the env
232
- var to `debug`:
233
-
234
- ```
235
- TADA_LOG_LEVEL=debug tada dev
236
- ```
237
-
238
-
239
290
  ## Content
240
291
 
241
292
  Site content lives in the `content/` directory. Markdown is converted to HTML.
@@ -284,3 +335,4 @@ templates][lodash].
284
335
  [pagefind]: https://pagefind.app/
285
336
  [mutool]: https://mupdf.readthedocs.io/en/latest/tools/mutool.html
286
337
  [jep467]: https://openjdk.org/jeps/467
338
+ [katex]: https://katex.org/
package/bin/tada.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { execSync } from 'child_process';
5
+ import { parseArgs } from 'util';
5
6
  import packageJson from '../package.json' with { type: 'json' };
6
7
  import {
7
8
  validateSymbol,
@@ -10,7 +11,14 @@ import {
10
11
  validateUrl,
11
12
  validateBasePath,
12
13
  createSiteConfig,
13
- } from './validators.js';
14
+ } from './validators';
15
+ import {
16
+ loadManifest,
17
+ diffManifests,
18
+ copyChangedFiles,
19
+ getVersions,
20
+ pruneOldVersions,
21
+ } from '../build/build-manifest';
14
22
 
15
23
  const { version } = packageJson;
16
24
 
@@ -33,6 +41,7 @@ const COMMANDS = {
33
41
  watch: 'Watch for changes and rebuild',
34
42
  serve: 'Start a local development server',
35
43
  clean: null,
44
+ diff: null,
36
45
  };
37
46
 
38
47
  interface InitQuestion {
@@ -97,13 +106,30 @@ function printUsage() {
97
106
  for (const [cmd, desc] of Object.entries(COMMANDS)) {
98
107
  if (cmd === 'init') {
99
108
  console.log(' init <dirname> Initialize a new site');
100
- console.log(' [--default] (Use defaults for all options)');
101
109
  console.log(
102
- ' [--bare] (Create a minimal site with one page)',
110
+ ' [--no-interactive] (Skip prompts; use defaults or flags)',
111
+ );
112
+ console.log(
113
+ ' [--bare] (Create a minimal site with one page)',
103
114
  );
115
+ console.log(' [--title, --symbol, --theme-color, --tint-hue,');
116
+ console.log(' --tint-amount, --default-time-zone, --prod-base,');
117
+ console.log(' --prod-base-path]');
104
118
  continue;
105
119
  } else if (cmd === 'clean') {
106
120
  console.log(' clean Remove the dist/ directory');
121
+ console.log(
122
+ ' [--prod] Prune old prod builds, keeping the two latest',
123
+ );
124
+ continue;
125
+ } else if (cmd === 'diff') {
126
+ console.log(
127
+ ' diff Show changed files since last prod build',
128
+ );
129
+ console.log(' [N M] Compare version N vs version M');
130
+ console.log(
131
+ ' [--copy <dir>] Copy changed files to a directory',
132
+ );
107
133
  continue;
108
134
  }
109
135
  console.log(` ${cmd.padEnd(18)} ${desc}`);
@@ -127,14 +153,69 @@ function copyDirRecursive(src: string, dest: string): void {
127
153
  }
128
154
  }
129
155
 
156
+ const FLAG_TO_KEY: Record<string, string> = {
157
+ title: 'title',
158
+ symbol: 'symbol',
159
+ 'theme-color': 'themeColor',
160
+ 'tint-hue': 'tintHue',
161
+ 'tint-amount': 'tintAmount',
162
+ 'default-time-zone': 'defaultTimeZone',
163
+ 'prod-base': 'prodBase',
164
+ 'prod-base-path': 'prodBasePath',
165
+ };
166
+
130
167
  async function initCommand(args: string[]): Promise<void> {
131
- const useDefaults = args.includes('--default');
132
- const bare = args.includes('--bare');
133
- const dirname = args.filter(a => !a.startsWith('--'))[0];
168
+ let values: Record<string, string | boolean | undefined>;
169
+ let positionals: string[];
170
+
171
+ try {
172
+ ({ values, positionals } = parseArgs({
173
+ args,
174
+ options: {
175
+ 'no-interactive': { type: 'boolean', default: false },
176
+ bare: { type: 'boolean', default: false },
177
+ title: { type: 'string', default: INIT_QUESTIONS.title.defaultValue },
178
+ symbol: { type: 'string', default: INIT_QUESTIONS.symbol.defaultValue },
179
+ 'theme-color': {
180
+ type: 'string',
181
+ default: INIT_QUESTIONS.themeColor.defaultValue,
182
+ },
183
+ 'tint-hue': {
184
+ type: 'string',
185
+ default: INIT_QUESTIONS.tintHue.defaultValue,
186
+ },
187
+ 'tint-amount': {
188
+ type: 'string',
189
+ default: INIT_QUESTIONS.tintAmount.defaultValue,
190
+ },
191
+ 'default-time-zone': {
192
+ type: 'string',
193
+ default: INIT_QUESTIONS.defaultTimeZone.defaultValue,
194
+ },
195
+ 'prod-base': {
196
+ type: 'string',
197
+ default: INIT_QUESTIONS.prodBase.defaultValue,
198
+ },
199
+ 'prod-base-path': {
200
+ type: 'string',
201
+ default: INIT_QUESTIONS.prodBasePath.defaultValue,
202
+ },
203
+ },
204
+ strict: true,
205
+ allowPositionals: true,
206
+ }));
207
+ } catch (e: unknown) {
208
+ console.error(`Error: ${(e as Error).message}`);
209
+ process.exit(1);
210
+ }
211
+
212
+ const noInteractive = values['no-interactive'] as boolean;
213
+ const bare = values.bare as boolean;
214
+ const dirname = positionals[0];
134
215
 
135
216
  if (!dirname) {
136
217
  console.error('Error: Provide a name for the new directory');
137
- console.log('Usage: tada init <dirname> [--default] [--bare]');
218
+ console.log('Usage: tada init <dirname> [--no-interactive] [--bare]');
138
219
  process.exit(1);
139
220
  }
140
221
 
@@ -145,9 +226,9 @@ async function initCommand(args: string[]): Promise<void> {
145
226
  }
146
227
 
147
228
  const message = `Creating a new Tada site in ${projectDir}`;
148
- if (useDefaults && bare) {
229
+ if (noInteractive && bare) {
149
230
  console.log(message + ' using default config (bare)');
150
- } else if (useDefaults) {
231
+ } else if (noInteractive) {
151
232
  console.log(message + ' using default config');
152
233
  } else if (bare) {
153
234
  console.log(message + ' (bare)');
@@ -157,9 +238,15 @@ async function initCommand(args: string[]): Promise<void> {
157
238
 
158
239
  const config: Record<string, string> = {};
159
240
 
160
- if (useDefaults) {
161
- for (const [key, { defaultValue }] of Object.entries(INIT_QUESTIONS)) {
162
- config[key] = defaultValue;
241
+ if (noInteractive) {
242
+ for (const [flag, key] of Object.entries(FLAG_TO_KEY)) {
243
+ const value = values[flag] as string;
244
+ const error = INIT_QUESTIONS[key].validate(value);
245
+ if (error) {
246
+ console.error(`Error: --${flag}: ${error}`);
247
+ process.exit(1);
248
+ }
249
+ config[key] = value;
163
250
  }
164
251
  } else {
165
252
  const questions = Object.entries(INIT_QUESTIONS);
@@ -223,6 +310,7 @@ async function initCommand(args: string[]): Promise<void> {
223
310
  base: 'http://localhost:8080',
224
311
  basePath: '/',
225
312
  internalDomains: ['localhost'],
313
+ features: { search: true, code: true, favicon: false },
226
314
  });
227
315
 
228
316
  const prodConfig = createSiteConfig({
@@ -235,6 +323,7 @@ async function initCommand(args: string[]): Promise<void> {
235
323
  base: prodBase,
236
324
  basePath: prodBasePath,
237
325
  internalDomains: [prodDomain],
326
+ features: { search: true, code: true, favicon: true },
238
327
  });
239
328
 
240
329
  fs.writeFileSync(
@@ -268,23 +357,58 @@ async function initCommand(args: string[]): Promise<void> {
268
357
  ) + '\n',
269
358
  );
270
359
  } else {
271
- // Copy nav and authors data files to the project root
272
- fs.copyFileSync(
273
- path.join(packageDir, 'config/nav.json'),
360
+ // Create nav and authors data files in the project root
361
+ fs.writeFileSync(
274
362
  path.join(projectDir, 'nav.json'),
363
+ JSON.stringify(
364
+ [
365
+ {
366
+ title: 'Navigation',
367
+ links: [{ text: 'Home', internal: '/index.html' }],
368
+ },
369
+ {
370
+ title: 'Topics',
371
+ links: [
372
+ { text: 'Lectures', internal: '/lectures/index.html' },
373
+ {
374
+ text: 'Problem Sets',
375
+ internal: '/problem_sets/index.html',
376
+ disabled: true,
377
+ },
378
+ ],
379
+ },
380
+ {
381
+ title: 'Links',
382
+ links: [
383
+ { text: 'Zoom', external: 'https://zoom.com' },
384
+ {
385
+ text: 'Canvas',
386
+ external: 'https://www.instructure.com/',
387
+ disabled: true,
388
+ },
389
+ ],
390
+ },
391
+ ],
392
+ null,
393
+ 2,
394
+ ) + '\n',
275
395
  );
276
- fs.copyFileSync(
277
- path.join(packageDir, 'config/authors.json'),
396
+ fs.writeFileSync(
278
397
  path.join(projectDir, 'authors.json'),
398
+ JSON.stringify(
399
+ { alex: { name: 'Alex Breen', avatar: '/avatars/alex.jpg' } },
400
+ null,
401
+ 2,
402
+ ) + '\n',
279
403
  );
280
404
 
281
405
  // Copy content/ and public/ from the package
282
406
  copyDirRecursive(
283
- path.join(packageDir, 'content'),
407
+ path.join(packageDir, 'init/content'),
284
408
  path.join(projectDir, 'content'),
285
409
  );
286
410
  copyDirRecursive(
287
- path.join(packageDir, 'public'),
411
+ path.join(packageDir, 'init/public'),
288
412
  path.join(projectDir, 'public'),
289
413
  );
290
414
  }
@@ -296,6 +420,147 @@ async function initCommand(args: string[]): Promise<void> {
296
420
  console.log(` tada serve`);
297
421
  }
298
422
 
423
+ function cleanCommand(args: string[]): void {
424
+ if (args.includes('--prod')) {
425
+ const prodBase = path.resolve(process.cwd(), 'dist-prod');
426
+ const removed = pruneOldVersions(prodBase);
427
+ if (removed.length === 0) {
428
+ console.log('Nothing to clean.');
429
+ return;
430
+ }
431
+ for (const v of removed) {
432
+ console.log(`Removed v${v}`);
433
+ }
434
+ return;
435
+ }
436
+
437
+ const distDir = path.resolve(process.cwd(), 'dist');
438
+ if (!fs.existsSync(distDir)) {
439
+ console.log('Nothing to clean.');
440
+ return;
441
+ }
442
+ fs.rmSync(distDir, { recursive: true });
443
+ console.log('Cleaned dist/');
444
+ }
445
+
446
+ function diffCommand(args: string[]): void {
447
+ const projectDir = process.cwd();
448
+ const prodBase = path.resolve(projectDir, 'dist-prod');
449
+ const versions = getVersions(prodBase);
450
+
451
+ if (versions.length === 0) {
452
+ console.error('Error: No prod builds found. Run "tada prod" first.');
453
+ process.exit(1);
454
+ }
455
+
456
+ const copyIdx = args.indexOf('--copy');
457
+ const versionArgs = copyIdx === -1 ? args : args.slice(0, copyIdx);
458
+ const numericArgs = versionArgs
459
+ .map(a => parseInt(a, 10))
460
+ .filter(n => !isNaN(n));
461
+
462
+ let oldVer: number;
463
+ let newVer: number;
464
+
465
+ if (numericArgs.length === 2) {
466
+ [oldVer, newVer] = numericArgs.sort((a, b) => a - b);
467
+ } else if (numericArgs.length === 0) {
468
+ if (versions.length < 2) {
469
+ console.error(
470
+ 'Error: Need at least two prod builds to diff. Run "tada prod" again.',
471
+ );
472
+ process.exit(1);
473
+ }
474
+ oldVer = versions[versions.length - 2];
475
+ newVer = versions[versions.length - 1];
476
+ } else {
477
+ console.error('Error: Provide zero or two version numbers.');
478
+ console.log('Usage: tada diff [N M] [--copy <dir>]');
479
+ process.exit(1);
480
+ }
481
+
482
+ const oldManifestPath = path.join(prodBase, `v${oldVer}.manifest.json`);
483
+ const newManifestPath = path.join(prodBase, `v${newVer}.manifest.json`);
484
+
485
+ const oldManifest = loadManifest(oldManifestPath);
486
+ if (!oldManifest) {
487
+ console.error(`Error: No manifest for v${oldVer}.`);
488
+ process.exit(1);
489
+ }
490
+ const newManifest = loadManifest(newManifestPath);
491
+ if (!newManifest) {
492
+ console.error(`Error: No manifest for v${newVer}.`);
493
+ process.exit(1);
494
+ }
495
+
496
+ const diff = diffManifests(oldManifest, newManifest);
497
+ const totalChanges =
498
+ diff.added.length + diff.changed.length + diff.removed.length;
499
+
500
+ const fmtDate = (iso: string) => new Date(iso).toLocaleString();
501
+ console.log(`v${oldVer} ${fmtDate(oldManifest.buildTime)}`);
502
+ console.log(`v${newVer} ${fmtDate(newManifest.buildTime)}`);
503
+
504
+ if (totalChanges === 0) {
505
+ console.log('\nNo changes between builds.');
506
+ return;
507
+ }
508
+
509
+ if (diff.added.length > 0) {
510
+ console.log(`\nAdded (${diff.added.length}):`);
511
+ for (const f of diff.added) {
512
+ console.log(` + ${f}`);
513
+ }
514
+ }
515
+ if (diff.changed.length > 0) {
516
+ console.log(`\nChanged (${diff.changed.length}):`);
517
+ for (const f of diff.changed) {
518
+ console.log(` ~ ${f}`);
519
+ }
520
+ }
521
+ if (diff.removed.length > 0) {
522
+ console.log(`\nRemoved (${diff.removed.length}):`);
523
+ for (const f of diff.removed) {
524
+ console.log(` - ${f}`);
525
+ }
526
+ }
527
+
528
+ const noun = (n: number) => (n === 1 ? 'file' : 'files');
529
+ console.log(`\nTotal: ${totalChanges} ${noun(totalChanges)} differ`);
530
+
531
+ if (copyIdx !== -1) {
532
+ const outDirArg = args[copyIdx + 1];
533
+ if (!outDirArg) {
534
+ console.error('Error: --copy requires a directory argument');
535
+ process.exit(1);
536
+ }
537
+
538
+ const newDistDir = path.join(prodBase, `v${newVer}`);
539
+ const resolvedOutDir = path.resolve(projectDir, outDirArg);
540
+ copyChangedFiles(diff, newDistDir, resolvedOutDir);
541
+
542
+ // Always include pagefind/ (excluded from manifest but needed for search)
543
+ const pagefindSrc = path.join(newDistDir, 'pagefind');
544
+ if (fs.existsSync(pagefindSrc)) {
545
+ copyDirRecursive(pagefindSrc, path.join(resolvedOutDir, 'pagefind'));
546
+ }
547
+
548
+ const manifestSrc = path.join(prodBase, `v${newVer}.manifest.json`);
549
+ fs.copyFileSync(manifestSrc, path.join(resolvedOutDir, 'manifest.json'));
550
+
551
+ const copiedCount = diff.added.length + diff.changed.length;
552
+ console.log(
553
+ `\nCopied ${copiedCount} ${noun(copiedCount)} + manifest.json to ${outDirArg}`,
554
+ );
555
+
556
+ if (diff.removed.length > 0) {
557
+ console.log(
558
+ `\nNote: ${diff.removed.length} ${noun(diff.removed.length)} were removed from the build.`,
559
+ );
560
+ }
561
+ }
562
+ }
563
+
299
564
  /*
300
565
  * Start of script
301
566
  */
@@ -335,11 +600,11 @@ switch (command) {
335
600
  }
336
601
 
337
602
  case 'clean':
338
- fs.rmSync(path.resolve(process.cwd(), 'dist'), {
339
- recursive: true,
340
- force: true,
341
- });
342
- console.log('Cleaned dist/');
603
+ cleanCommand(process.argv.slice(3));
604
+ break;
605
+
606
+ case 'diff':
607
+ diffCommand(process.argv.slice(3));
343
608
  break;
344
609
 
345
610
  case '--version':
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import type { SiteConfigInput } from '../build/types.js';
2
+ import type { SiteConfigInput } from '../build/types';
3
3
  import {
4
4
  validateSymbol,
5
5
  validateColor,
@@ -7,7 +7,7 @@ import {
7
7
  validateUrl,
8
8
  validateBasePath,
9
9
  createSiteConfig,
10
- } from './validators.js';
10
+ } from './validators';
11
11
 
12
12
  describe('validateSymbol', () => {
13
13
  test('accepts a single uppercase letter', () => {
@@ -172,6 +172,7 @@ describe('createSiteConfig', () => {
172
172
  base: 'https://example.edu',
173
173
  basePath: '/',
174
174
  internalDomains: ['example.edu'],
175
+ features: { search: true, code: true, favicon: false },
175
176
  };
176
177
 
177
178
  test('returns an object with all expected keys', () => {
@@ -184,7 +185,7 @@ describe('createSiteConfig', () => {
184
185
  basePath: '/',
185
186
  internalDomains: ['example.edu'],
186
187
  defaultTimeZone: 'America/New_York',
187
- features: { search: true, code: true, favicon: true },
188
+ features: { search: true, code: true, favicon: false },
188
189
  codeLanguages: { java: 'java', py: 'python' },
189
190
  vars: {},
190
191
  });
package/bin/validators.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SiteConfigInput, SiteVariables } from '../build/types.js';
1
+ import type { SiteConfigInput, SiteVariables } from '../build/types';
2
2
 
3
3
  export function validateSymbol(value: string): string | null {
4
4
  if (!value) {
@@ -65,11 +65,13 @@ export function createSiteConfig({
65
65
  base,
66
66
  basePath,
67
67
  internalDomains,
68
+ features,
68
69
  }: SiteConfigInput): SiteVariables {
69
70
  return {
70
71
  title,
72
+ titlePostfix: ` - ${title}`,
71
73
  symbol,
72
- features: { search: true, code: true, favicon: true },
74
+ features,
73
75
  base,
74
76
  basePath,
75
77
  internalDomains,
@@ -1,8 +1,9 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
2
  import type Token from 'markdown-it/lib/token.mjs';
3
- import { createApplyBasePath } from './utils/paths.js';
4
- import { makeLogger } from './log.js';
5
- import type { SiteVariables } from './types.js';
3
+ import { createApplyBasePath } from './utils/paths';
4
+ import { isFeatureEnabled } from './features';
5
+ import { makeLogger } from './log';
6
+ import type { SiteVariables } from './types';
6
7
 
7
8
  const log = makeLogger(__filename);
8
9
 
@@ -11,7 +12,7 @@ export default function applyBasePathPlugin(
11
12
  siteVariables: SiteVariables,
12
13
  ): void {
13
14
  const applyBasePath = createApplyBasePath(siteVariables);
14
- const rewriteCodeLinks = siteVariables.features?.code !== false;
15
+ const rewriteCodeLinks = isFeatureEnabled(siteVariables, 'code');
15
16
 
16
17
  function rewriteInternalHref(href: string): string {
17
18
  const match = href.match(/^([^?#]*)(.*)$/);
@@ -67,15 +68,25 @@ export default function applyBasePathPlugin(
67
68
  token.attrSet('src', afterApply);
68
69
  }
69
70
  } else if (token.type === 'html_block' || token.type === 'html_inline') {
70
- token.content = token.content.replace(
71
- /(<img\b[^>]*\bsrc=")(\/)([^"]*")/g,
72
- (match, prefix, slash, rest) => {
73
- const src = slash + rest.slice(0, -1);
74
- const afterApply = applyBasePath(src);
75
- log.debug`Applying base path to img tag src: ${src} -> ${afterApply}`;
76
- return prefix + afterApply + '"';
77
- },
78
- );
71
+ token.content = token.content
72
+ .replace(
73
+ /(<a\b[^>]*\bhref=")(\/)([^"]*")/g,
74
+ (match, prefix, slash, rest) => {
75
+ const href = slash + rest.slice(0, -1);
76
+ const afterApply = applyBasePath(href);
77
+ log.debug`Applying base path to a tag href: ${href} -> ${afterApply}`;
78
+ return prefix + afterApply + '"';
79
+ },
80
+ )
81
+ .replace(
82
+ /(<img\b[^>]*\bsrc=")(\/)([^"]*")/g,
83
+ (match, prefix, slash, rest) => {
84
+ const src = slash + rest.slice(0, -1);
85
+ const afterApply = applyBasePath(src);
86
+ log.debug`Applying base path to img tag src: ${src} -> ${afterApply}`;
87
+ return prefix + afterApply + '"';
88
+ },
89
+ );
79
90
  }
80
91
 
81
92
  token.children?.map(checkAndApplyBasePath);