@abreen/tada 1.1.0 → 1.3.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.
- package/README.md +69 -17
- package/bin/tada.ts +317 -25
- 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 +10 -11
- package/build/globals.ts +14 -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 +116 -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 +20 -8
- package/build/site-variables.ts +13 -5
- package/build/templates.ts +16 -7
- 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/jdi-runner/LiterateRunner.java +13 -0
- package/build/utils/literate-java.ts +24 -12
- 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 +57 -36
- 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 +91 -110
- package/init/content/labs/00/VowelCounter.java.md +129 -0
- package/init/content/labs/00/index.md +29 -0
- package/init/content/labs/index.md +9 -0
- package/{content → init/content}/lectures/01/Pair.java.md +1 -0
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +26 -7
- package/package.json +19 -7
- package/{templates → schema}/nav.schema.json +1 -2
- package/{build → schema}/site.schema.json +3 -5
- package/src/_alerts.scss +4 -0
- package/src/_base.scss +90 -6
- package/src/_layout.scss +39 -2
- package/src/anchor/index.ts +0 -1
- package/src/index.ts +3 -3
- package/src/question/style.scss +8 -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 +2 -20
- package/src/style.scss +20 -101
- package/src/timezone/index.ts +9 -25
- package/src/util.ts +1 -1
- package/templates/_top.html +2 -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/build/utils/jdi-runner/LiterateRunner.class +0 -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/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,11 +23,12 @@ 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
|
|
29
30
|
* `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
|
|
30
|
-
* `???
|
|
31
|
+
* `??? question ... ???` renders a Q & A section; answers hidden until click
|
|
31
32
|
* Special heading subtitles with `## Heading # A subtitle here`
|
|
32
33
|
- Automatically generated favicon
|
|
33
34
|
* Text, color, font and font weight taken from config file
|
|
@@ -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,45 @@ 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)',
|
|
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]');
|
|
118
|
+
continue;
|
|
119
|
+
} else if (cmd === 'watch') {
|
|
120
|
+
console.log(` ${cmd.padEnd(18)} ${desc}`);
|
|
121
|
+
console.log(
|
|
122
|
+
' [--port <n>] HTTP server port (default: 8080)',
|
|
123
|
+
);
|
|
124
|
+
console.log(
|
|
125
|
+
' [--ws-port <n>] WebSocket port for live reload (default: 35729)',
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
} else if (cmd === 'serve') {
|
|
129
|
+
console.log(` ${cmd.padEnd(18)} ${desc}`);
|
|
130
|
+
console.log(
|
|
131
|
+
' [--port <n>] HTTP server port (default: 8080)',
|
|
103
132
|
);
|
|
104
133
|
continue;
|
|
105
134
|
} else if (cmd === 'clean') {
|
|
106
135
|
console.log(' clean Remove the dist/ directory');
|
|
136
|
+
console.log(
|
|
137
|
+
' [--prod] Prune old prod builds, keeping the two latest',
|
|
138
|
+
);
|
|
139
|
+
continue;
|
|
140
|
+
} else if (cmd === 'diff') {
|
|
141
|
+
console.log(
|
|
142
|
+
' diff Show changed files since last prod build',
|
|
143
|
+
);
|
|
144
|
+
console.log(' [N M] Compare version N vs version M');
|
|
145
|
+
console.log(
|
|
146
|
+
' [--copy <dir>] Copy changed files to a directory',
|
|
147
|
+
);
|
|
107
148
|
continue;
|
|
108
149
|
}
|
|
109
150
|
console.log(` ${cmd.padEnd(18)} ${desc}`);
|
|
@@ -111,7 +152,18 @@ function printUsage() {
|
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
function run(cmd: string): void {
|
|
114
|
-
|
|
155
|
+
try {
|
|
156
|
+
execSync(cmd, { cwd: process.cwd(), stdio: 'inherit' });
|
|
157
|
+
} catch (err: unknown) {
|
|
158
|
+
const code = (err as { status?: number }).status;
|
|
159
|
+
if (code != null) {
|
|
160
|
+
console.error(`tada ${command} failed with exit code ${code}`);
|
|
161
|
+
process.exit(code);
|
|
162
|
+
} else {
|
|
163
|
+
console.error(`tada ${command} failed`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
115
167
|
}
|
|
116
168
|
|
|
117
169
|
function copyDirRecursive(src: string, dest: string): void {
|
|
@@ -127,14 +179,69 @@ function copyDirRecursive(src: string, dest: string): void {
|
|
|
127
179
|
}
|
|
128
180
|
}
|
|
129
181
|
|
|
182
|
+
const FLAG_TO_KEY: Record<string, string> = {
|
|
183
|
+
title: 'title',
|
|
184
|
+
symbol: 'symbol',
|
|
185
|
+
'theme-color': 'themeColor',
|
|
186
|
+
'tint-hue': 'tintHue',
|
|
187
|
+
'tint-amount': 'tintAmount',
|
|
188
|
+
'default-time-zone': 'defaultTimeZone',
|
|
189
|
+
'prod-base': 'prodBase',
|
|
190
|
+
'prod-base-path': 'prodBasePath',
|
|
191
|
+
};
|
|
192
|
+
|
|
130
193
|
async function initCommand(args: string[]): Promise<void> {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
194
|
+
let values: Record<string, string | boolean | undefined>;
|
|
195
|
+
let positionals: string[];
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
({ values, positionals } = parseArgs({
|
|
199
|
+
args,
|
|
200
|
+
options: {
|
|
201
|
+
'no-interactive': { type: 'boolean', default: false },
|
|
202
|
+
bare: { type: 'boolean', default: false },
|
|
203
|
+
title: { type: 'string', default: INIT_QUESTIONS.title.defaultValue },
|
|
204
|
+
symbol: { type: 'string', default: INIT_QUESTIONS.symbol.defaultValue },
|
|
205
|
+
'theme-color': {
|
|
206
|
+
type: 'string',
|
|
207
|
+
default: INIT_QUESTIONS.themeColor.defaultValue,
|
|
208
|
+
},
|
|
209
|
+
'tint-hue': {
|
|
210
|
+
type: 'string',
|
|
211
|
+
default: INIT_QUESTIONS.tintHue.defaultValue,
|
|
212
|
+
},
|
|
213
|
+
'tint-amount': {
|
|
214
|
+
type: 'string',
|
|
215
|
+
default: INIT_QUESTIONS.tintAmount.defaultValue,
|
|
216
|
+
},
|
|
217
|
+
'default-time-zone': {
|
|
218
|
+
type: 'string',
|
|
219
|
+
default: INIT_QUESTIONS.defaultTimeZone.defaultValue,
|
|
220
|
+
},
|
|
221
|
+
'prod-base': {
|
|
222
|
+
type: 'string',
|
|
223
|
+
default: INIT_QUESTIONS.prodBase.defaultValue,
|
|
224
|
+
},
|
|
225
|
+
'prod-base-path': {
|
|
226
|
+
type: 'string',
|
|
227
|
+
default: INIT_QUESTIONS.prodBasePath.defaultValue,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
strict: true,
|
|
231
|
+
allowPositionals: true,
|
|
232
|
+
}));
|
|
233
|
+
} catch (e: unknown) {
|
|
234
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const noInteractive = values['no-interactive'] as boolean;
|
|
239
|
+
const bare = values.bare as boolean;
|
|
240
|
+
const dirname = positionals[0];
|
|
134
241
|
|
|
135
242
|
if (!dirname) {
|
|
136
243
|
console.error('Error: Provide a name for the new directory');
|
|
137
|
-
console.log('Usage: tada init <dirname> [--
|
|
244
|
+
console.log('Usage: tada init <dirname> [--no-interactive] [--bare]');
|
|
138
245
|
process.exit(1);
|
|
139
246
|
}
|
|
140
247
|
|
|
@@ -145,9 +252,9 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
145
252
|
}
|
|
146
253
|
|
|
147
254
|
const message = `Creating a new Tada site in ${projectDir}`;
|
|
148
|
-
if (
|
|
255
|
+
if (noInteractive && bare) {
|
|
149
256
|
console.log(message + ' using default config (bare)');
|
|
150
|
-
} else if (
|
|
257
|
+
} else if (noInteractive) {
|
|
151
258
|
console.log(message + ' using default config');
|
|
152
259
|
} else if (bare) {
|
|
153
260
|
console.log(message + ' (bare)');
|
|
@@ -157,9 +264,15 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
157
264
|
|
|
158
265
|
const config: Record<string, string> = {};
|
|
159
266
|
|
|
160
|
-
if (
|
|
161
|
-
for (const [
|
|
162
|
-
|
|
267
|
+
if (noInteractive) {
|
|
268
|
+
for (const [flag, key] of Object.entries(FLAG_TO_KEY)) {
|
|
269
|
+
const value = values[flag] as string;
|
|
270
|
+
const error = INIT_QUESTIONS[key].validate(value);
|
|
271
|
+
if (error) {
|
|
272
|
+
console.error(`Error: --${flag}: ${error}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
config[key] = value;
|
|
163
276
|
}
|
|
164
277
|
} else {
|
|
165
278
|
const questions = Object.entries(INIT_QUESTIONS);
|
|
@@ -223,6 +336,7 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
223
336
|
base: 'http://localhost:8080',
|
|
224
337
|
basePath: '/',
|
|
225
338
|
internalDomains: ['localhost'],
|
|
339
|
+
features: { search: true, code: true, favicon: false },
|
|
226
340
|
});
|
|
227
341
|
|
|
228
342
|
const prodConfig = createSiteConfig({
|
|
@@ -235,6 +349,7 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
235
349
|
base: prodBase,
|
|
236
350
|
basePath: prodBasePath,
|
|
237
351
|
internalDomains: [prodDomain],
|
|
352
|
+
features: { search: true, code: true, favicon: true },
|
|
238
353
|
});
|
|
239
354
|
|
|
240
355
|
fs.writeFileSync(
|
|
@@ -268,23 +383,59 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
268
383
|
) + '\n',
|
|
269
384
|
);
|
|
270
385
|
} else {
|
|
271
|
-
//
|
|
272
|
-
fs.
|
|
273
|
-
path.join(packageDir, 'config/nav.json'),
|
|
386
|
+
// Create nav and authors data files in the project root
|
|
387
|
+
fs.writeFileSync(
|
|
274
388
|
path.join(projectDir, 'nav.json'),
|
|
389
|
+
JSON.stringify(
|
|
390
|
+
[
|
|
391
|
+
{
|
|
392
|
+
title: 'Navigation',
|
|
393
|
+
links: [{ text: 'Home', internal: '/index.html' }],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
title: 'Topics',
|
|
397
|
+
links: [
|
|
398
|
+
{ text: 'Lectures', internal: '/lectures/index.html' },
|
|
399
|
+
{ text: 'Labs', internal: '/labs/index.html' },
|
|
400
|
+
{
|
|
401
|
+
text: 'Problem Sets',
|
|
402
|
+
internal: '/problem_sets/index.html',
|
|
403
|
+
disabled: true,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
title: 'Links',
|
|
409
|
+
links: [
|
|
410
|
+
{ text: 'Zoom', external: 'https://zoom.com' },
|
|
411
|
+
{
|
|
412
|
+
text: 'Canvas',
|
|
413
|
+
external: 'https://www.instructure.com/',
|
|
414
|
+
disabled: true,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
null,
|
|
420
|
+
2,
|
|
421
|
+
) + '\n',
|
|
275
422
|
);
|
|
276
|
-
fs.
|
|
277
|
-
path.join(packageDir, 'config/authors.json'),
|
|
423
|
+
fs.writeFileSync(
|
|
278
424
|
path.join(projectDir, 'authors.json'),
|
|
425
|
+
JSON.stringify(
|
|
426
|
+
{ alex: { name: 'Alex Breen', avatar: '/avatars/alex.jpg' } },
|
|
427
|
+
null,
|
|
428
|
+
2,
|
|
429
|
+
) + '\n',
|
|
279
430
|
);
|
|
280
431
|
|
|
281
432
|
// Copy content/ and public/ from the package
|
|
282
433
|
copyDirRecursive(
|
|
283
|
-
path.join(packageDir, 'content'),
|
|
434
|
+
path.join(packageDir, 'init/content'),
|
|
284
435
|
path.join(projectDir, 'content'),
|
|
285
436
|
);
|
|
286
437
|
copyDirRecursive(
|
|
287
|
-
path.join(packageDir, 'public'),
|
|
438
|
+
path.join(packageDir, 'init/public'),
|
|
288
439
|
path.join(projectDir, 'public'),
|
|
289
440
|
);
|
|
290
441
|
}
|
|
@@ -296,6 +447,147 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
296
447
|
console.log(` tada serve`);
|
|
297
448
|
}
|
|
298
449
|
|
|
450
|
+
function cleanCommand(args: string[]): void {
|
|
451
|
+
if (args.includes('--prod')) {
|
|
452
|
+
const prodBase = path.resolve(process.cwd(), 'dist-prod');
|
|
453
|
+
const removed = pruneOldVersions(prodBase);
|
|
454
|
+
if (removed.length === 0) {
|
|
455
|
+
console.log('Nothing to clean.');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
for (const v of removed) {
|
|
459
|
+
console.log(`Removed v${v}`);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const distDir = path.resolve(process.cwd(), 'dist');
|
|
465
|
+
if (!fs.existsSync(distDir)) {
|
|
466
|
+
console.log('Nothing to clean.');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
fs.rmSync(distDir, { recursive: true });
|
|
470
|
+
console.log('Cleaned dist/');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function diffCommand(args: string[]): void {
|
|
474
|
+
const projectDir = process.cwd();
|
|
475
|
+
const prodBase = path.resolve(projectDir, 'dist-prod');
|
|
476
|
+
const versions = getVersions(prodBase);
|
|
477
|
+
|
|
478
|
+
if (versions.length === 0) {
|
|
479
|
+
console.error('Error: No prod builds found. Run "tada prod" first.');
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const copyIdx = args.indexOf('--copy');
|
|
484
|
+
const versionArgs = copyIdx === -1 ? args : args.slice(0, copyIdx);
|
|
485
|
+
const numericArgs = versionArgs
|
|
486
|
+
.map(a => parseInt(a, 10))
|
|
487
|
+
.filter(n => !isNaN(n));
|
|
488
|
+
|
|
489
|
+
let oldVer: number;
|
|
490
|
+
let newVer: number;
|
|
491
|
+
|
|
492
|
+
if (numericArgs.length === 2) {
|
|
493
|
+
[oldVer, newVer] = numericArgs.sort((a, b) => a - b);
|
|
494
|
+
} else if (numericArgs.length === 0) {
|
|
495
|
+
if (versions.length < 2) {
|
|
496
|
+
console.error(
|
|
497
|
+
'Error: Need at least two prod builds to diff. Run "tada prod" again.',
|
|
498
|
+
);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
oldVer = versions[versions.length - 2];
|
|
502
|
+
newVer = versions[versions.length - 1];
|
|
503
|
+
} else {
|
|
504
|
+
console.error('Error: Provide zero or two version numbers.');
|
|
505
|
+
console.log('Usage: tada diff [N M] [--copy <dir>]');
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const oldManifestPath = path.join(prodBase, `v${oldVer}.manifest.json`);
|
|
510
|
+
const newManifestPath = path.join(prodBase, `v${newVer}.manifest.json`);
|
|
511
|
+
|
|
512
|
+
const oldManifest = loadManifest(oldManifestPath);
|
|
513
|
+
if (!oldManifest) {
|
|
514
|
+
console.error(`Error: No manifest for v${oldVer}.`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
const newManifest = loadManifest(newManifestPath);
|
|
518
|
+
if (!newManifest) {
|
|
519
|
+
console.error(`Error: No manifest for v${newVer}.`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const diff = diffManifests(oldManifest, newManifest);
|
|
524
|
+
const totalChanges =
|
|
525
|
+
diff.added.length + diff.changed.length + diff.removed.length;
|
|
526
|
+
|
|
527
|
+
const fmtDate = (iso: string) => new Date(iso).toLocaleString();
|
|
528
|
+
console.log(`v${oldVer} ${fmtDate(oldManifest.buildTime)}`);
|
|
529
|
+
console.log(`v${newVer} ${fmtDate(newManifest.buildTime)}`);
|
|
530
|
+
|
|
531
|
+
if (totalChanges === 0) {
|
|
532
|
+
console.log('\nNo changes between builds.');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (diff.added.length > 0) {
|
|
537
|
+
console.log(`\nAdded (${diff.added.length}):`);
|
|
538
|
+
for (const f of diff.added) {
|
|
539
|
+
console.log(` + ${f}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (diff.changed.length > 0) {
|
|
543
|
+
console.log(`\nChanged (${diff.changed.length}):`);
|
|
544
|
+
for (const f of diff.changed) {
|
|
545
|
+
console.log(` ~ ${f}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (diff.removed.length > 0) {
|
|
549
|
+
console.log(`\nRemoved (${diff.removed.length}):`);
|
|
550
|
+
for (const f of diff.removed) {
|
|
551
|
+
console.log(` - ${f}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const noun = (n: number) => (n === 1 ? 'file' : 'files');
|
|
556
|
+
console.log(`\nTotal: ${totalChanges} ${noun(totalChanges)} differ`);
|
|
557
|
+
|
|
558
|
+
if (copyIdx !== -1) {
|
|
559
|
+
const outDirArg = args[copyIdx + 1];
|
|
560
|
+
if (!outDirArg) {
|
|
561
|
+
console.error('Error: --copy requires a directory argument');
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const newDistDir = path.join(prodBase, `v${newVer}`);
|
|
566
|
+
const resolvedOutDir = path.resolve(projectDir, outDirArg);
|
|
567
|
+
copyChangedFiles(diff, newDistDir, resolvedOutDir);
|
|
568
|
+
|
|
569
|
+
// Always include pagefind/ (excluded from manifest but needed for search)
|
|
570
|
+
const pagefindSrc = path.join(newDistDir, 'pagefind');
|
|
571
|
+
if (fs.existsSync(pagefindSrc)) {
|
|
572
|
+
copyDirRecursive(pagefindSrc, path.join(resolvedOutDir, 'pagefind'));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const manifestSrc = path.join(prodBase, `v${newVer}.manifest.json`);
|
|
576
|
+
fs.copyFileSync(manifestSrc, path.join(resolvedOutDir, 'manifest.json'));
|
|
577
|
+
|
|
578
|
+
const copiedCount = diff.added.length + diff.changed.length;
|
|
579
|
+
console.log(
|
|
580
|
+
`\nCopied ${copiedCount} ${noun(copiedCount)} + manifest.json to ${outDirArg}`,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
if (diff.removed.length > 0) {
|
|
584
|
+
console.log(
|
|
585
|
+
`\nNote: ${diff.removed.length} ${noun(diff.removed.length)} were removed from the build.`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
299
591
|
/*
|
|
300
592
|
* Start of script
|
|
301
593
|
*/
|
|
@@ -335,11 +627,11 @@ switch (command) {
|
|
|
335
627
|
}
|
|
336
628
|
|
|
337
629
|
case 'clean':
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
630
|
+
cleanCommand(process.argv.slice(3));
|
|
631
|
+
break;
|
|
632
|
+
|
|
633
|
+
case 'diff':
|
|
634
|
+
diffCommand(process.argv.slice(3));
|
|
343
635
|
break;
|
|
344
636
|
|
|
345
637
|
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);
|