@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.
- package/README.md +68 -16
- package/bin/tada.ts +289 -24
- package/bin/validators.test.ts +4 -3
- package/bin/validators.ts +4 -2
- package/build/apply-base-path-plugin.ts +24 -13
- package/build/build-manifest.test.ts +349 -0
- package/build/build-manifest.ts +165 -0
- package/build/bundle.ts +19 -20
- package/build/code.test.ts +7 -3
- package/build/content-watch.ts +6 -6
- package/build/copy.ts +2 -2
- package/build/deflist-id-plugin.ts +1 -1
- package/build/external-links-plugin.ts +2 -2
- package/build/features.ts +1 -1
- package/build/generate-content-assets.ts +12 -7
- package/build/generate-favicon.ts +13 -6
- package/build/generate-fonts.ts +2 -2
- package/build/generate-katex-assets.ts +35 -0
- package/build/generate-manifest.ts +2 -2
- package/build/globals.test.ts +9 -11
- package/build/globals.ts +6 -7
- package/build/json-schema.test.ts +1 -1
- package/build/log.test.ts +1 -1
- package/build/log.ts +2 -2
- package/build/markdown-plugins.test.ts +118 -10
- package/build/pagefind.test.ts +2 -2
- package/build/pagefind.ts +5 -5
- package/build/pdf-text.test.ts +52 -0
- package/build/pipeline.ts +42 -15
- package/build/reachability.test.ts +1 -1
- package/build/serve.ts +3 -3
- package/build/site-variables.ts +13 -5
- package/build/templates.ts +7 -6
- package/build/toc-plugin.test.ts +2 -2
- package/build/toc-plugin.ts +1 -1
- package/build/types.ts +15 -6
- package/build/util.ts +6 -6
- package/build/utils/code.ts +5 -5
- package/build/utils/content-files.ts +3 -3
- package/build/utils/derive-theme.test.ts +1 -1
- package/build/utils/derive-theme.ts +1 -1
- package/build/utils/file-types.test.ts +1 -1
- package/build/utils/front-matter.test.ts +1 -4
- package/build/utils/front-matter.ts +2 -2
- package/build/utils/literate-java.ts +11 -10
- package/build/utils/markdown.ts +41 -9
- package/build/utils/paths.test.ts +2 -2
- package/build/utils/paths.ts +5 -1
- package/build/utils/render.ts +45 -32
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/validate-internal-links-plugin.test.ts +1 -1
- package/build/validate-internal-links-plugin.ts +1 -1
- package/build/watch-reachability-state.test.ts +2 -2
- package/build/watch-reachability-state.ts +1 -1
- package/build/watch.ts +74 -98
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +26 -7
- package/package.json +9 -7
- package/{templates → schema}/nav.schema.json +1 -2
- package/{build → schema}/site.schema.json +1 -2
- package/src/_alerts.scss +4 -0
- package/src/_base.scss +41 -2
- package/src/_layout.scss +37 -0
- package/src/anchor/index.ts +0 -1
- package/src/index.ts +3 -3
- package/src/search/index.test.ts +203 -0
- package/src/search/index.ts +3 -79
- package/src/search/pdf-utils.ts +79 -0
- package/src/search/style.scss +0 -14
- package/src/style.scss +9 -57
- package/src/timezone/index.ts +5 -6
- package/src/util.ts +1 -1
- package/templates/_top.html +1 -1
- package/types/dev.d.ts +1 -3
- package/types/site-variables.d.ts +6 -10
- package/types/untyped-modules.d.ts +7 -0
- package/config/authors.json +0 -1
- package/config/nav.json +0 -28
- /package/{content → init/content}/index.md +0 -0
- /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
- /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
- /package/{content → init/content}/lectures/01/demo.py +0 -0
- /package/{content → init/content}/lectures/01/index.md +0 -0
- /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
- /package/{content → init/content}/problem_sets/index.html +0 -0
- /package/{public → init/public}/avatars/alex.jpg +0 -0
- /package/{public → init/public}/test.txt +0 -0
- /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 `--
|
|
82
|
-
|
|
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
|
|
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
|
-
' [--
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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> [--
|
|
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 (
|
|
229
|
+
if (noInteractive && bare) {
|
|
149
230
|
console.log(message + ' using default config (bare)');
|
|
150
|
-
} else if (
|
|
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 (
|
|
161
|
-
for (const [
|
|
162
|
-
|
|
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
|
-
//
|
|
272
|
-
fs.
|
|
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.
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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':
|
package/bin/validators.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import type { SiteConfigInput } from '../build/types
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
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
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|