@abreen/tada 1.11.3 → 1.13.4
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 +65 -64
- package/bin/tada.ts +61 -66
- package/build/build-manifest.ts +14 -3
- package/build/bundle.ts +0 -1
- package/build/config-files.ts +183 -0
- package/build/config-loader.ts +159 -0
- package/build/copy.ts +0 -83
- package/build/generate-content-assets.ts +75 -160
- package/build/globals.ts +48 -89
- package/build/log.ts +27 -18
- package/build/pagefind.ts +33 -26
- package/build/pipeline.ts +98 -79
- package/build/reachability.ts +51 -248
- package/build/serve.ts +35 -6
- package/build/site-variables.ts +20 -11
- package/build/source-model.ts +400 -0
- package/build/{watch/records.ts → source-records.ts} +111 -49
- package/build/template-globals.ts +91 -0
- package/build/templates.ts +50 -40
- package/build/test-helpers.ts +15 -0
- package/build/toc-plugin.ts +5 -1
- package/build/types.d.ts +9 -1
- package/build/util.ts +8 -0
- package/build/utils/code.ts +6 -16
- package/build/utils/content-files.ts +57 -126
- package/build/utils/final-html.ts +239 -0
- package/build/utils/markdown.ts +237 -17
- package/build/utils/paths.ts +2 -1
- package/build/utils/render.ts +108 -99
- package/build/utils/trace-core.ts +3 -7
- package/build/validate-config-links.ts +91 -14
- package/build/watch/assets.ts +0 -2
- package/build/watch/build-full.ts +14 -15
- package/build/watch/build-incremental.ts +15 -8
- package/build/watch/compiler-types.d.ts +6 -1
- package/build/watch/compiler.ts +27 -15
- package/build/watch/index.ts +2 -7
- package/build/watch/planner.ts +32 -16
- package/build/watch/reload.ts +4 -0
- package/build/watch/runtime.ts +29 -45
- package/build/watch/snapshot.ts +50 -402
- package/build/watch/validation.ts +6 -3
- package/build/watch-reachability-state.ts +16 -23
- package/build/watch-reload-client.ts +11 -4
- package/init/content/index.md +1 -1
- package/init/content/labs/02/index.md +61 -0
- package/init/content/labs/02/selection_sort.py +23 -0
- package/init/content/labs/index.md +4 -2
- package/init/content/lectures/01/index.md +1 -1
- package/init/content/markdown.md +1 -1
- package/package.json +3 -4
- package/src/_base.scss +67 -0
- package/src/anchor/README.md +9 -0
- package/src/anchor/index.ts +115 -16
- package/src/anchor/style.scss +70 -10
- package/src/code/index.ts +46 -18
- package/src/code.scss +2 -1
- package/src/globals.ts +47 -0
- package/src/index.ts +1 -0
- package/src/navigate/index.ts +2 -5
- package/src/navigate/lifecycle.ts +3 -1
- package/src/navigate/runtime.ts +6 -5
- package/src/page-update/index.ts +20 -16
- package/src/print/style.scss +0 -4
- package/src/search/index.ts +53 -25
- package/src/slides/index.ts +481 -0
- package/src/slides/style.scss +104 -0
- package/src/style.scss +8 -3
- package/src/timezone/index.ts +16 -20
- package/src/top/index.ts +2 -4
- package/src/trace/index.ts +116 -14
- package/src/trace/style.scss +1 -1
- package/src/util.ts +6 -2
- package/templates/_author.html +2 -2
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
- package/templates/_theme.scss +4 -3
- package/templates/_top.html +14 -14
- package/templates/default.html +9 -0
- package/types/dev.d.ts +1 -1
- package/types/site-variables.d.ts +4 -5
- package/watch/fs-commit.ts +63 -10
- package/build/apply-base-path-plugin.ts +0 -186
- package/build/content-watch.ts +0 -104
- package/build/validate-internal-links-plugin.ts +0 -207
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ A static site generator. The successor to Presto.
|
|
|
25
25
|
- Interactive execution traces via `renderTrace()`
|
|
26
26
|
* Supports Java and Python source files
|
|
27
27
|
* Renders step-by-step source highlighting, output, and memory diagrams
|
|
28
|
+
- Slides mode: separate content by `---` and present sections as full-screen slides
|
|
28
29
|
- PDF files are copied into `dist/`
|
|
29
30
|
- Text of each PDF page is extracted using `mutool` (if present) and indexed
|
|
30
31
|
- External link handling (special visual treatment for external links)
|
|
@@ -118,7 +119,7 @@ Available flags: `--title`, `--symbol`, `--theme-color`, `--tint-hue`,
|
|
|
118
119
|
|
|
119
120
|
### `tada dev`
|
|
120
121
|
|
|
121
|
-
Build the site for local development (using `site.dev.
|
|
122
|
+
Build the site for local development (using `site.dev.yaml` by default)
|
|
122
123
|
into the `dist/` directory.
|
|
123
124
|
|
|
124
125
|
### `tada serve`
|
|
@@ -137,7 +138,7 @@ builds (keeps the latest two versions).
|
|
|
137
138
|
|
|
138
139
|
### `tada prod`
|
|
139
140
|
|
|
140
|
-
Build the site for production (uses `site.prod.
|
|
141
|
+
Build the site for production (uses `site.prod.yaml` by default). Each prod build is
|
|
141
142
|
saved to a versioned directory under `dist-prod/` (e.g., `dist-prod/v1/`,
|
|
142
143
|
`dist-prod/v2/`) with a manifest file that records the SHA-256 hash of every
|
|
143
144
|
output file.
|
|
@@ -160,7 +161,7 @@ The output directory will also include a `manifest.json` for the newer build.
|
|
|
160
161
|
## Development vs. production builds
|
|
161
162
|
|
|
162
163
|
`tada dev` and `tada watch` build to the `dist/` directory using
|
|
163
|
-
`site.dev
|
|
164
|
+
`site.dev.*` (typically `localhost` URLs). `tada watch` includes a
|
|
164
165
|
development server with live reload; `tada serve` is a standalone server
|
|
165
166
|
for previewing a `tada dev` build. Dev builds overwrite `dist/` each time.
|
|
166
167
|
|
|
@@ -196,31 +197,39 @@ manually.
|
|
|
196
197
|
|
|
197
198
|
Build-time site config lives in:
|
|
198
199
|
|
|
199
|
-
- `site.dev.
|
|
200
|
-
- `site.prod.
|
|
201
|
-
- `nav.
|
|
202
|
-
- `authors.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
200
|
+
- `site.dev.yaml` (used by `tada dev` / `tada watch`)
|
|
201
|
+
- `site.prod.yaml` (used by `tada prod`)
|
|
202
|
+
- `nav.yaml` (navigation structure)
|
|
203
|
+
- `authors.yaml` (author data)
|
|
204
|
+
|
|
205
|
+
Tada also accepts `.yml` and `.json` for each of these files. Keep only one
|
|
206
|
+
variant for each logical config name; for example, `nav.yaml` and `nav.json`
|
|
207
|
+
in the same site root is an error.
|
|
208
|
+
|
|
209
|
+
Example site configuration YAML file:
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
title: Intro to Computer Science
|
|
213
|
+
titlePostfix: " - CS 0"
|
|
214
|
+
symbol: CS 0
|
|
215
|
+
themeColor: hsl(351 70% 40%)
|
|
216
|
+
tintAmount: 0
|
|
217
|
+
features:
|
|
218
|
+
search: true
|
|
219
|
+
favicon: true
|
|
220
|
+
base: https://example.edu
|
|
221
|
+
basePath: /cs0
|
|
222
|
+
internalDomains:
|
|
223
|
+
- example.edu
|
|
224
|
+
defaultTimeZone: America/New_York
|
|
225
|
+
extensionToShikiLanguage:
|
|
226
|
+
java: java
|
|
227
|
+
py: python
|
|
228
|
+
shikiLanguages:
|
|
229
|
+
- java
|
|
230
|
+
- python
|
|
231
|
+
vars:
|
|
232
|
+
staffEmail: staff@example.edu
|
|
224
233
|
```
|
|
225
234
|
|
|
226
235
|
| Field | Description |
|
|
@@ -244,54 +253,45 @@ Example site configuration JSON file:
|
|
|
244
253
|
| `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
|
|
245
254
|
| `vars` | Arbitrary key/value variables exposed to templates/content as `vars.*` (e.g., `<%= vars.staffEmail %>`) |
|
|
246
255
|
|
|
247
|
-
#### `nav.
|
|
256
|
+
#### `nav.yaml`
|
|
248
257
|
|
|
249
258
|
Defines the site navigation structure. The file contains an array of section
|
|
250
259
|
objects. Each section contains an array of link objects (internal or external,
|
|
251
260
|
and whether the link is disabled). You should specify at least two sections,
|
|
252
261
|
but three or more sections are supported.
|
|
253
262
|
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
"title": "Links",
|
|
273
|
-
"links": [
|
|
274
|
-
{ "text": "Zoom", "external": "https://zoom.com" }
|
|
275
|
-
]
|
|
276
|
-
}
|
|
277
|
-
]
|
|
263
|
+
```yaml
|
|
264
|
+
- title: Navigation
|
|
265
|
+
links:
|
|
266
|
+
- text: Home
|
|
267
|
+
internal: /index.html
|
|
268
|
+
- title: Topics
|
|
269
|
+
links:
|
|
270
|
+
- text: Lectures
|
|
271
|
+
internal: /lectures/index.html
|
|
272
|
+
- text: Problem Sets
|
|
273
|
+
internal: /problem_sets/index.html
|
|
274
|
+
disabled: true
|
|
275
|
+
- title: Links
|
|
276
|
+
links:
|
|
277
|
+
- text: Zoom
|
|
278
|
+
external: https://zoom.com
|
|
278
279
|
```
|
|
279
280
|
|
|
280
|
-
#### `authors.
|
|
281
|
+
#### `authors.yaml`
|
|
281
282
|
|
|
282
283
|
Maps author handles (used in front matter `author` fields) to display names
|
|
283
284
|
and avatars. Each key is a handle (e.g., `jsmith`) and each value is an object
|
|
284
285
|
with `name`, `avatar`, and optionally `url`.
|
|
285
286
|
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
287
|
+
```yaml
|
|
288
|
+
jsmith:
|
|
289
|
+
name: Jane Smith
|
|
290
|
+
avatar: /avatars/jsmith.jpg
|
|
291
|
+
ajones:
|
|
292
|
+
name: Alex Jones
|
|
293
|
+
avatar: /avatars/ajones.jpg
|
|
294
|
+
url: /staff/ajones.html
|
|
295
295
|
```
|
|
296
296
|
|
|
297
297
|
## Content
|
|
@@ -313,9 +313,10 @@ list of variables parsed using the [`front-matter`][front-matter] library).
|
|
|
313
313
|
|-------|-------------|
|
|
314
314
|
| `title` (required) | Page title (`<title>` tag and page heading) |
|
|
315
315
|
| `skip` | Set to `true` to skip building this page completely |
|
|
316
|
-
| `author` | Author handle (e.g. `jsmith`) resolved to a full object via
|
|
316
|
+
| `author` | Author handle (e.g. `jsmith`) resolved to a full object via the authors config |
|
|
317
317
|
| `description` | Meta description for the page |
|
|
318
318
|
| `toc` | Set to `true` to show a table of contents |
|
|
319
|
+
| `slides` | Set to `true` on Markdown pages to treat top-level `---` as slide separators and add presentation controls |
|
|
319
320
|
| `parent` & `parentLabel` | URL and label for a breadcrumb link displayed above the title |
|
|
320
321
|
| `published` | Year, month, and day of publishing (e.g, `2025-09-09`) |
|
|
321
322
|
|
package/bin/tada.ts
CHANGED
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
getVersions,
|
|
20
20
|
pruneOldVersions,
|
|
21
21
|
} from '../build/build-manifest';
|
|
22
|
+
import {
|
|
23
|
+
getDefaultProjectConfigFilePath,
|
|
24
|
+
getDefaultSiteConfigPath,
|
|
25
|
+
resolveSiteConfigFile,
|
|
26
|
+
type SiteEnv,
|
|
27
|
+
} from '../build/config-files';
|
|
22
28
|
|
|
23
29
|
const { version } = packageJson;
|
|
24
30
|
|
|
@@ -26,10 +32,11 @@ const SYSTEM_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
26
32
|
|
|
27
33
|
const packageDir = path.resolve(import.meta.dir, '..');
|
|
28
34
|
|
|
29
|
-
function requireSiteConfig(env:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
function requireSiteConfig(env: SiteEnv): void {
|
|
36
|
+
try {
|
|
37
|
+
resolveSiteConfigFile(process.cwd(), env);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Error: ${(error as Error).message}`);
|
|
33
40
|
process.exit(1);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
@@ -132,9 +139,6 @@ function printUsage() {
|
|
|
132
139
|
console.log(
|
|
133
140
|
' [--port <n>] HTTP server port (default: 8080)',
|
|
134
141
|
);
|
|
135
|
-
console.log(
|
|
136
|
-
' [--ws-port <n>] WebSocket port for live reload (default: 35729)',
|
|
137
|
-
);
|
|
138
142
|
continue;
|
|
139
143
|
} else if (cmd === 'serve') {
|
|
140
144
|
console.log(` ${cmd.padEnd(18)} ${desc}`);
|
|
@@ -175,6 +179,10 @@ function copyDirRecursive(src: string, dest: string): void {
|
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
function stringifyYaml(value: unknown): string {
|
|
183
|
+
return Bun.YAML.stringify(value, null, 2) + '\n';
|
|
184
|
+
}
|
|
185
|
+
|
|
178
186
|
const FLAG_TO_KEY: Record<string, string> = {
|
|
179
187
|
title: 'title',
|
|
180
188
|
symbol: 'symbol',
|
|
@@ -353,13 +361,13 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
353
361
|
});
|
|
354
362
|
|
|
355
363
|
fs.writeFileSync(
|
|
356
|
-
|
|
357
|
-
|
|
364
|
+
getDefaultSiteConfigPath(projectDir, 'dev'),
|
|
365
|
+
stringifyYaml(devConfig),
|
|
358
366
|
);
|
|
359
367
|
|
|
360
368
|
fs.writeFileSync(
|
|
361
|
-
|
|
362
|
-
|
|
369
|
+
getDefaultSiteConfigPath(projectDir, 'prod'),
|
|
370
|
+
stringifyYaml(prodConfig),
|
|
363
371
|
);
|
|
364
372
|
|
|
365
373
|
if (bare) {
|
|
@@ -370,63 +378,53 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
370
378
|
);
|
|
371
379
|
fs.mkdirSync(path.join(projectDir, 'public'), { recursive: true });
|
|
372
380
|
fs.writeFileSync(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
],
|
|
381
|
-
null,
|
|
382
|
-
2,
|
|
383
|
-
) + '\n',
|
|
381
|
+
getDefaultProjectConfigFilePath(projectDir, 'nav'),
|
|
382
|
+
stringifyYaml([
|
|
383
|
+
{
|
|
384
|
+
title: 'Navigation',
|
|
385
|
+
links: [{ text: 'Home', internal: '/index.html' }],
|
|
386
|
+
},
|
|
387
|
+
]),
|
|
384
388
|
);
|
|
385
389
|
} else {
|
|
386
390
|
// Create nav and authors data files in the project root
|
|
387
391
|
fs.writeFileSync(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
],
|
|
419
|
-
null,
|
|
420
|
-
2,
|
|
421
|
-
) + '\n',
|
|
392
|
+
getDefaultProjectConfigFilePath(projectDir, 'nav'),
|
|
393
|
+
stringifyYaml([
|
|
394
|
+
{
|
|
395
|
+
title: 'Navigation',
|
|
396
|
+
links: [{ text: 'Home', internal: '/index.html' }],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
title: 'Topics',
|
|
400
|
+
links: [
|
|
401
|
+
{ text: 'Lectures', internal: '/lectures/index.html' },
|
|
402
|
+
{ text: 'Labs', internal: '/labs/index.html' },
|
|
403
|
+
{
|
|
404
|
+
text: 'Problem Sets',
|
|
405
|
+
internal: '/problem_sets/index.html',
|
|
406
|
+
disabled: true,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
title: 'Links',
|
|
412
|
+
links: [
|
|
413
|
+
{ text: 'Zoom', external: 'https://zoom.com' },
|
|
414
|
+
{
|
|
415
|
+
text: 'Canvas',
|
|
416
|
+
external: 'https://www.instructure.com/',
|
|
417
|
+
disabled: true,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
]),
|
|
422
422
|
);
|
|
423
423
|
fs.writeFileSync(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
2,
|
|
429
|
-
) + '\n',
|
|
424
|
+
getDefaultProjectConfigFilePath(projectDir, 'authors'),
|
|
425
|
+
stringifyYaml({
|
|
426
|
+
alex: { name: 'Alex Breen', avatar: '/avatars/alex.jpg' },
|
|
427
|
+
}),
|
|
430
428
|
);
|
|
431
429
|
|
|
432
430
|
// Copy content/ and public/ from the package
|
|
@@ -627,10 +625,7 @@ try {
|
|
|
627
625
|
const { runWatch } = await import(
|
|
628
626
|
path.join(packageDir, 'build/watch/index.ts')
|
|
629
627
|
);
|
|
630
|
-
await runWatch({
|
|
631
|
-
httpPort: parsePortArg('--port'),
|
|
632
|
-
wsPort: parsePortArg('--ws-port'),
|
|
633
|
-
});
|
|
628
|
+
await runWatch({ httpPort: parsePortArg('--port') });
|
|
634
629
|
break;
|
|
635
630
|
}
|
|
636
631
|
|
package/build/build-manifest.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { globals, type Globals } from './globals';
|
|
3
4
|
import { toPosix } from './utils/paths';
|
|
4
5
|
|
|
5
6
|
const EXCLUDED_DIRS = new Set(['pagefind']);
|
|
6
7
|
const EXCLUDED_FILES = new Set(['tada.manifest.json']);
|
|
7
8
|
|
|
9
|
+
type ManifestGlobals = Pick<
|
|
10
|
+
Globals,
|
|
11
|
+
'createSha256Hasher' | 'now' | 'readFileArrayBuffer' | 'toISOString'
|
|
12
|
+
>;
|
|
13
|
+
|
|
8
14
|
export interface BuildManifest {
|
|
9
15
|
schema: 1;
|
|
10
16
|
build: number;
|
|
@@ -51,8 +57,12 @@ export function diffManifests(
|
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export async function hashFile(filePath: string): Promise<string> {
|
|
54
|
-
const
|
|
55
|
-
|
|
60
|
+
const runtimeGlobals: Pick<
|
|
61
|
+
ManifestGlobals,
|
|
62
|
+
'createSha256Hasher' | 'readFileArrayBuffer'
|
|
63
|
+
> = globals;
|
|
64
|
+
const buffer = await runtimeGlobals.readFileArrayBuffer(filePath);
|
|
65
|
+
const hasher = runtimeGlobals.createSha256Hasher();
|
|
56
66
|
hasher.update(buffer);
|
|
57
67
|
return hasher.digest('hex');
|
|
58
68
|
}
|
|
@@ -120,11 +130,12 @@ export async function generateBuildManifest(
|
|
|
120
130
|
manifestPath: string,
|
|
121
131
|
buildVersion: number,
|
|
122
132
|
): Promise<void> {
|
|
133
|
+
const runtimeGlobals: Pick<ManifestGlobals, 'now' | 'toISOString'> = globals;
|
|
123
134
|
const files = await walkAndHash(distDir);
|
|
124
135
|
const manifest: BuildManifest = {
|
|
125
136
|
schema: 1,
|
|
126
137
|
build: buildVersion,
|
|
127
|
-
buildTime:
|
|
138
|
+
buildTime: runtimeGlobals.toISOString(runtimeGlobals.now()),
|
|
128
139
|
files,
|
|
129
140
|
};
|
|
130
141
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
package/build/bundle.ts
CHANGED
|
@@ -69,7 +69,6 @@ function createDefine(
|
|
|
69
69
|
isDev = false,
|
|
70
70
|
): Record<string, string> {
|
|
71
71
|
return {
|
|
72
|
-
__SITE_BASE__: JSON.stringify(siteVariables.base),
|
|
73
72
|
__SITE_BASE_PATH__: JSON.stringify(siteVariables.basePath),
|
|
74
73
|
__SITE_TITLE_POSTFIX__: JSON.stringify(siteVariables.titlePostfix),
|
|
75
74
|
__SITE_DEFAULT_TIMEZONE__: JSON.stringify(siteVariables.defaultTimeZone),
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export type SiteEnv = 'dev' | 'prod';
|
|
5
|
+
export type SiteConfigName = 'site.dev' | 'site.prod';
|
|
6
|
+
export type ProjectConfigName = 'nav' | 'authors';
|
|
7
|
+
export type ConfigName = SiteConfigName | ProjectConfigName;
|
|
8
|
+
export type ProjectConfigExtension = '.yaml' | '.yml' | '.json';
|
|
9
|
+
|
|
10
|
+
export interface ResolvedProjectConfigFile<
|
|
11
|
+
Name extends ConfigName = ConfigName,
|
|
12
|
+
> {
|
|
13
|
+
name: Name;
|
|
14
|
+
baseName: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
filePath: string;
|
|
17
|
+
extension: ProjectConfigExtension;
|
|
18
|
+
required: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json'] as const;
|
|
22
|
+
export const DEFAULT_CONFIG_FILE_EXTENSION = '.yaml' as const;
|
|
23
|
+
|
|
24
|
+
export const SITE_CONFIG_BASE_NAMES = {
|
|
25
|
+
dev: 'site.dev',
|
|
26
|
+
prod: 'site.prod',
|
|
27
|
+
} as const satisfies Record<SiteEnv, SiteConfigName>;
|
|
28
|
+
|
|
29
|
+
export const PROJECT_CONFIG_BASE_NAMES = {
|
|
30
|
+
nav: 'nav',
|
|
31
|
+
authors: 'authors',
|
|
32
|
+
} as const satisfies Record<ProjectConfigName, ProjectConfigName>;
|
|
33
|
+
|
|
34
|
+
export const REQUIRED_PROJECT_CONFIG_NAMES = ['nav'] as const;
|
|
35
|
+
export const OPTIONAL_PROJECT_CONFIG_NAMES = ['authors'] as const;
|
|
36
|
+
export const PROJECT_CONFIG_NAMES = [
|
|
37
|
+
...REQUIRED_PROJECT_CONFIG_NAMES,
|
|
38
|
+
...OPTIONAL_PROJECT_CONFIG_NAMES,
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
function joinWithOr(items: string[]): string {
|
|
42
|
+
if (items.length <= 1) {
|
|
43
|
+
return items[0] ?? '';
|
|
44
|
+
}
|
|
45
|
+
if (items.length === 2) {
|
|
46
|
+
return `${items[0]} or ${items[1]}`;
|
|
47
|
+
}
|
|
48
|
+
return `${items.slice(0, -1).join(', ')}, or ${items[items.length - 1]}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getResolvedConfigExtension(fileName: string): ProjectConfigExtension {
|
|
52
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
53
|
+
if (ext !== '.yaml' && ext !== '.yml' && ext !== '.json') {
|
|
54
|
+
throw new Error(`Unsupported config file extension for ${fileName}`);
|
|
55
|
+
}
|
|
56
|
+
return ext;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getSiteConfigBaseName(env: SiteEnv): SiteConfigName {
|
|
60
|
+
return SITE_CONFIG_BASE_NAMES[env];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getProjectConfigBaseName(
|
|
64
|
+
name: ProjectConfigName,
|
|
65
|
+
): ProjectConfigName {
|
|
66
|
+
return PROJECT_CONFIG_BASE_NAMES[name];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSupportedConfigFileNames(baseName: string): string[] {
|
|
70
|
+
return CONFIG_FILE_EXTENSIONS.map(ext => `${baseName}${ext}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getSupportedConfigFilePaths(
|
|
74
|
+
projectDir: string,
|
|
75
|
+
baseName: string,
|
|
76
|
+
): string[] {
|
|
77
|
+
return getSupportedConfigFileNames(baseName).map(fileName =>
|
|
78
|
+
path.join(projectDir, fileName),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getSupportedConfigFileNamesText(baseName: string): string {
|
|
83
|
+
return joinWithOr(getSupportedConfigFileNames(baseName));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getDefaultConfigFileName(baseName: string): string {
|
|
87
|
+
return `${baseName}${DEFAULT_CONFIG_FILE_EXTENSION}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getDefaultConfigFilePath(
|
|
91
|
+
projectDir: string,
|
|
92
|
+
baseName: string,
|
|
93
|
+
): string {
|
|
94
|
+
return path.join(projectDir, getDefaultConfigFileName(baseName));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getDefaultSiteConfigFileName(env: SiteEnv): string {
|
|
98
|
+
return getDefaultConfigFileName(getSiteConfigBaseName(env));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getDefaultSiteConfigPath(
|
|
102
|
+
projectDir: string,
|
|
103
|
+
env: SiteEnv,
|
|
104
|
+
): string {
|
|
105
|
+
return getDefaultConfigFilePath(projectDir, getSiteConfigBaseName(env));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getDefaultProjectConfigFileName(
|
|
109
|
+
name: ProjectConfigName,
|
|
110
|
+
): string {
|
|
111
|
+
return getDefaultConfigFileName(getProjectConfigBaseName(name));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getDefaultProjectConfigFilePath(
|
|
115
|
+
projectDir: string,
|
|
116
|
+
name: ProjectConfigName,
|
|
117
|
+
): string {
|
|
118
|
+
return getDefaultConfigFilePath(projectDir, getProjectConfigBaseName(name));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function resolveConfigFile<Name extends ConfigName = ConfigName>(
|
|
122
|
+
projectDir: string,
|
|
123
|
+
{
|
|
124
|
+
name,
|
|
125
|
+
baseName,
|
|
126
|
+
required,
|
|
127
|
+
}: { name: Name; baseName: string; required: boolean },
|
|
128
|
+
): ResolvedProjectConfigFile<Name> | undefined {
|
|
129
|
+
const matches = getSupportedConfigFilePaths(projectDir, baseName).filter(
|
|
130
|
+
filePath => fs.existsSync(filePath),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (matches.length > 1) {
|
|
134
|
+
const fileNames = matches.map(filePath => path.basename(filePath));
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Multiple config files found for ${baseName}: ${joinWithOr(fileNames)}. Keep only one.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (matches.length === 0) {
|
|
141
|
+
if (required) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Missing required config file for ${baseName} (tried ${getSupportedConfigFileNamesText(baseName)})`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const filePath = matches[0];
|
|
150
|
+
const fileName = path.basename(filePath);
|
|
151
|
+
return {
|
|
152
|
+
name,
|
|
153
|
+
baseName,
|
|
154
|
+
fileName,
|
|
155
|
+
filePath,
|
|
156
|
+
extension: getResolvedConfigExtension(fileName),
|
|
157
|
+
required,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolveSiteConfigFile(
|
|
162
|
+
projectDir: string,
|
|
163
|
+
env: SiteEnv,
|
|
164
|
+
): ResolvedProjectConfigFile<SiteConfigName> {
|
|
165
|
+
const baseName = SITE_CONFIG_BASE_NAMES[env];
|
|
166
|
+
return resolveConfigFile(projectDir, {
|
|
167
|
+
name: baseName,
|
|
168
|
+
baseName,
|
|
169
|
+
required: true,
|
|
170
|
+
})!;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveProjectConfigFile(
|
|
174
|
+
projectDir: string,
|
|
175
|
+
name: ProjectConfigName,
|
|
176
|
+
): ResolvedProjectConfigFile<ProjectConfigName> | undefined {
|
|
177
|
+
const baseName = PROJECT_CONFIG_BASE_NAMES[name];
|
|
178
|
+
return resolveConfigFile(projectDir, {
|
|
179
|
+
name,
|
|
180
|
+
baseName,
|
|
181
|
+
required: name === 'nav',
|
|
182
|
+
});
|
|
183
|
+
}
|