@abreen/tada 1.0.1 → 1.1.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 +29 -33
- package/bin/tada.ts +356 -0
- package/bin/validators.test.ts +204 -0
- package/bin/validators.ts +83 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
- package/build/bundle.ts +117 -0
- package/{webpack/code.test.js → build/code.test.ts} +6 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +315 -0
- package/build/generate-favicon.ts +165 -0
- package/build/generate-fonts.ts +31 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +101 -0
- package/{webpack/globals.js → build/globals.ts} +28 -13
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +93 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
- package/{webpack → build}/site.schema.json +3 -10
- package/{webpack/templates.js → build/templates.ts} +35 -33
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +172 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +80 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +573 -0
- package/content/index.md +9 -3
- package/content/markdown.md +2 -1
- package/content/problem_sets/index.html +14 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/package.json +28 -19
- package/src/_alerts.scss +92 -0
- package/src/_base.scss +106 -0
- package/src/{layout.scss → _layout.scss} +0 -2
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +1 -2
- package/src/question/style.scss +1 -1
- package/src/search/index.ts +36 -15
- package/src/search/style.scss +9 -15
- package/src/style.scss +6 -269
- package/src/toc/style.scss +5 -39
- package/src/util.ts +8 -5
- package/templates/_theme.scss +38 -14
- package/tsconfig.json +10 -6
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/untyped-modules.d.ts +40 -0
- package/bin/tada.js +0 -361
- package/content/problem_sets/index.md +0 -6
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -147
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
- /package/types/{dev.ts → dev.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ A static site generator. The successor to Presto.
|
|
|
11
11
|
* Renders headings, alert boxes, and `<hr>` elements
|
|
12
12
|
* Highlights the heading currently being viewed
|
|
13
13
|
- Built-in search powered by [Pagefind][pagefind]
|
|
14
|
-
* Only pages
|
|
14
|
+
* Only pages in `content/` reachable from `/index.html` are indexed
|
|
15
15
|
- Generated HTML pages for source code
|
|
16
16
|
* Automatic code highlighting, clickable line numbers
|
|
17
17
|
* Dynamic table of contents for each method/function
|
|
18
18
|
* Converts new Markdown comment syntax ([added in Java 23][jep467]) to HTML
|
|
19
|
-
* Indexed by Pagefind
|
|
19
|
+
* Indexed by Pagefind (classes, interfaces, methods, and fields)
|
|
20
20
|
- PDF files are copied into `dist/`
|
|
21
21
|
- Text of each PDF page is extracted using [mutool][mutool] and indexed
|
|
22
22
|
- External link handling (special visual treatment for external links)
|
|
@@ -69,14 +69,14 @@ Visit [http://localhost:8080/index.html](http://localhost:8080/index.html).
|
|
|
69
69
|
### `tada init <dirname>`
|
|
70
70
|
|
|
71
71
|
Create a new Tada site in a new directory. Prompts for:
|
|
72
|
-
- **Site title
|
|
73
|
-
- **Symbol
|
|
74
|
-
- **Theme color
|
|
75
|
-
- **Background tint hue
|
|
76
|
-
- **Background tint amount
|
|
77
|
-
- **Default time zone
|
|
78
|
-
- **Production base URL
|
|
79
|
-
- **Production base path
|
|
72
|
+
- **Site title**: displayed in the header and `<title>` tag
|
|
73
|
+
- **Symbol**: short text (1-5 uppercase characters) shown in the logo and favicon
|
|
74
|
+
- **Theme color**: HSL color, e.g. `hsl(195 70% 40%)`
|
|
75
|
+
- **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `20`)
|
|
76
|
+
- **Background tint amount**: percentage (0-100) of tint to apply (defaults to `100`)
|
|
77
|
+
- **Default time zone**: for `<time>` elements (defaults to your system zone)
|
|
78
|
+
- **Production base URL**: e.g. `https://example.edu`
|
|
79
|
+
- **Production base path**: e.g. `/cs101` (defaults to `/`)
|
|
80
80
|
|
|
81
81
|
Pass `--default` to use the default values for all options without being
|
|
82
82
|
prompted.
|
|
@@ -84,7 +84,7 @@ prompted.
|
|
|
84
84
|
|
|
85
85
|
### `tada dev`
|
|
86
86
|
|
|
87
|
-
Build the site for local development (using `
|
|
87
|
+
Build the site for local development (using `site.dev.json`)
|
|
88
88
|
into the `dist/` directory.
|
|
89
89
|
|
|
90
90
|
|
|
@@ -106,7 +106,7 @@ Remove the `dist/` directory.
|
|
|
106
106
|
|
|
107
107
|
### `tada prod`
|
|
108
108
|
|
|
109
|
-
Build the site for production (uses `
|
|
109
|
+
Build the site for production (uses `site.prod.json`).
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
## Prerequisites
|
|
@@ -125,10 +125,10 @@ Build the site for production (uses `config/site.prod.json`).
|
|
|
125
125
|
|
|
126
126
|
Build-time site config lives in:
|
|
127
127
|
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
- `
|
|
131
|
-
- `
|
|
128
|
+
- `site.dev.json` (used by `tada dev` / `tada watch`)
|
|
129
|
+
- `site.prod.json` (used by `tada prod`)
|
|
130
|
+
- `nav.json` (navigation structure)
|
|
131
|
+
- `authors.json` (author data)
|
|
132
132
|
|
|
133
133
|
Example site configuration JSON file:
|
|
134
134
|
|
|
@@ -156,7 +156,7 @@ Example site configuration JSON file:
|
|
|
156
156
|
| `title` | Title for the whole site (also used to derive `titlePostfix`) |
|
|
157
157
|
| `titlePostfix` | *Optional*, the string to append to each page's `title` |
|
|
158
158
|
| `symbol` | Text (1-5 chars) displayed in header (also used as the favicon symbol) |
|
|
159
|
-
| `themeColor` |
|
|
159
|
+
| `themeColor` | Theme color for the site (e.g., `"tomato"`, `"#c04040"`, `"hsl(195 70% 40%)"`) |
|
|
160
160
|
| `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `20`) |
|
|
161
161
|
| `tintAmount` | *Optional*, percentage (0-100) of tint to apply (default `100`) |
|
|
162
162
|
| `faviconSymbol` | *Optional*, the text to use instead of `symbol` in the favicon |
|
|
@@ -167,7 +167,7 @@ Example site configuration JSON file:
|
|
|
167
167
|
| `basePath` | URL prefix for deployment under a subpath (e.g., `"/cs101"`), use `"/"` at root |
|
|
168
168
|
| `internalDomains` | Domain names treated as internal by link processing (not marked external) |
|
|
169
169
|
| `codeLanguages` | Map file extension to Shiki language (e.g., `"java": "java"`) |
|
|
170
|
-
| `faviconColor` | *Optional*,
|
|
170
|
+
| `faviconColor` | *Optional*, background color for favicon (defaults to `themeColor`) |
|
|
171
171
|
| `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
|
|
172
172
|
| `vars` | Arbitrary key/value variables exposed to templates/content (e.g., `<%= staffEmail %>`) |
|
|
173
173
|
|
|
@@ -239,12 +239,12 @@ TADA_LOG_LEVEL=debug tada dev
|
|
|
239
239
|
## Content
|
|
240
240
|
|
|
241
241
|
Site content lives in the `content/` directory. Markdown is converted to HTML.
|
|
242
|
-
HTML files should contain front matter and are also built
|
|
243
|
-
like Markdown files are.
|
|
242
|
+
HTML files should contain front matter and are also built.
|
|
244
243
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
with zero processing.
|
|
244
|
+
Any other kinds of files are copied into `dist/` in the same locations.
|
|
245
|
+
|
|
246
|
+
All files in `public/` are copied directly into `dist/` with zero processing.
|
|
247
|
+
Files in `public/` are **not** included in the search index.
|
|
248
248
|
|
|
249
249
|
|
|
250
250
|
### Front matter fields
|
|
@@ -256,31 +256,27 @@ list of variables parsed using the [`front-matter`][front-matter] library).
|
|
|
256
256
|
|-------|-------------|
|
|
257
257
|
| `title` (required) | Page title (`<title>` tag and page heading) |
|
|
258
258
|
| `skip` | Set to `true` to skip building this page completely |
|
|
259
|
-
| `author` | Author handle (e.g. `jsmith`) resolved to a full object via `
|
|
259
|
+
| `author` | Author handle (e.g. `jsmith`) resolved to a full object via `authors.json` |
|
|
260
260
|
| `description` | Meta description for the page |
|
|
261
261
|
| `toc` | Set to `true` to show a table of contents |
|
|
262
262
|
| `parent` & `parentLabel` | URL and label for a breadcrumb link displayed above the title |
|
|
263
263
|
| `published` | Year, month, and day of publishing (e.g, `2025-09-09`) |
|
|
264
264
|
|
|
265
|
+
You may also add arbitrary fields in a page's front matter, and access them
|
|
266
|
+
using Lodash syntax (see below).
|
|
267
|
+
|
|
265
268
|
|
|
266
269
|
### Variable substitution
|
|
267
270
|
|
|
268
271
|
Plain text content (e.g., HTML and Markdown) are processed using [Lodash
|
|
269
272
|
templates][lodash].
|
|
270
273
|
|
|
271
|
-
- Site config values are available under `site` (e.g., `site.title
|
|
272
|
-
- Page variables (from front matter) are available under `page`
|
|
274
|
+
- Site config values are available under `site` (e.g., `site.title`)
|
|
275
|
+
- Page variables (from front matter) are available under `page`
|
|
273
276
|
- Custom variables from the `"vars"` property of the config are available
|
|
274
277
|
without any prefix (e.g., `<%= staffEmail %>`)
|
|
275
278
|
|
|
276
279
|
|
|
277
|
-
## Templates
|
|
278
|
-
|
|
279
|
-
HTML page layouts are internal to the Tada package. You don't need to modify
|
|
280
|
-
them; they are designed to work with the client-side components and styles
|
|
281
|
-
bundled in the package.
|
|
282
|
-
|
|
283
|
-
|
|
284
280
|
|
|
285
281
|
[inter]: https://fonts.google.com/specimen/Inter
|
|
286
282
|
[lodash]: https://lodash.info/doc/template
|
package/bin/tada.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
6
|
+
import {
|
|
7
|
+
validateSymbol,
|
|
8
|
+
validateColor,
|
|
9
|
+
validateHue,
|
|
10
|
+
validateUrl,
|
|
11
|
+
validateBasePath,
|
|
12
|
+
createSiteConfig,
|
|
13
|
+
} from './validators.js';
|
|
14
|
+
|
|
15
|
+
const { version } = packageJson;
|
|
16
|
+
|
|
17
|
+
const SYSTEM_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18
|
+
|
|
19
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
function requireSiteConfig(env: string): void {
|
|
22
|
+
const configPath = path.resolve(process.cwd(), `site.${env}.json`);
|
|
23
|
+
if (!fs.existsSync(configPath)) {
|
|
24
|
+
console.error(`Error: Missing config file: site.${env}.json`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COMMANDS = {
|
|
30
|
+
init: null,
|
|
31
|
+
dev: 'Build the site for development',
|
|
32
|
+
prod: 'Build the site for production',
|
|
33
|
+
watch: 'Watch for changes and rebuild',
|
|
34
|
+
serve: 'Start a local development server',
|
|
35
|
+
clean: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface InitQuestion {
|
|
39
|
+
prompt: string;
|
|
40
|
+
defaultValue: string;
|
|
41
|
+
validate: (value: string) => string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const INIT_QUESTIONS: Record<string, InitQuestion> = {
|
|
45
|
+
title: {
|
|
46
|
+
prompt: 'Site title',
|
|
47
|
+
defaultValue: 'Introduction to Computer Science',
|
|
48
|
+
validate: (v: string) => (v ? null : 'Title is required'),
|
|
49
|
+
},
|
|
50
|
+
symbol: {
|
|
51
|
+
prompt: 'Logo symbol (1-5 uppercase chars)',
|
|
52
|
+
defaultValue: 'CS 0',
|
|
53
|
+
validate: validateSymbol,
|
|
54
|
+
},
|
|
55
|
+
themeColor: {
|
|
56
|
+
prompt: 'Theme color',
|
|
57
|
+
defaultValue: 'hsl(195 70% 40%)',
|
|
58
|
+
validate: validateColor,
|
|
59
|
+
},
|
|
60
|
+
tintHue: {
|
|
61
|
+
prompt: 'Background tint hue (0-360)',
|
|
62
|
+
defaultValue: '20',
|
|
63
|
+
validate: validateHue,
|
|
64
|
+
},
|
|
65
|
+
tintAmount: {
|
|
66
|
+
prompt: 'Background tint amount (0-100)',
|
|
67
|
+
defaultValue: '100',
|
|
68
|
+
validate: (v: string) => {
|
|
69
|
+
const n = Number(v);
|
|
70
|
+
if (!Number.isInteger(n) || n < 0 || n > 100) {
|
|
71
|
+
return 'Must be an integer from 0 to 100';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
defaultTimeZone: {
|
|
77
|
+
prompt: 'Default time zone',
|
|
78
|
+
defaultValue: SYSTEM_TIME_ZONE,
|
|
79
|
+
validate: (v: string) => (v ? null : 'Time zone is required'),
|
|
80
|
+
},
|
|
81
|
+
prodBase: {
|
|
82
|
+
prompt: 'Production base URL',
|
|
83
|
+
defaultValue: 'https://example.edu',
|
|
84
|
+
validate: validateUrl,
|
|
85
|
+
},
|
|
86
|
+
prodBasePath: {
|
|
87
|
+
prompt: 'Production base path',
|
|
88
|
+
defaultValue: '/',
|
|
89
|
+
validate: validateBasePath,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function printUsage() {
|
|
94
|
+
console.log(`tada v${version}\n`);
|
|
95
|
+
console.log('Usage: tada <command>\n');
|
|
96
|
+
console.log('Commands:');
|
|
97
|
+
for (const [cmd, desc] of Object.entries(COMMANDS)) {
|
|
98
|
+
if (cmd === 'init') {
|
|
99
|
+
console.log(' init <dirname> Initialize a new site');
|
|
100
|
+
console.log(' [--default] (Use defaults for all options)');
|
|
101
|
+
console.log(
|
|
102
|
+
' [--bare] (Create a minimal site with one page)',
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
} else if (cmd === 'clean') {
|
|
106
|
+
console.log(' clean Remove the dist/ directory');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
console.log(` ${cmd.padEnd(18)} ${desc}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function run(cmd: string): void {
|
|
114
|
+
execSync(cmd, { cwd: process.cwd(), stdio: 'inherit' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function copyDirRecursive(src: string, dest: string): void {
|
|
118
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
119
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
120
|
+
const srcPath = path.join(src, entry.name);
|
|
121
|
+
const destPath = path.join(dest, entry.name);
|
|
122
|
+
if (entry.isDirectory()) {
|
|
123
|
+
copyDirRecursive(srcPath, destPath);
|
|
124
|
+
} else {
|
|
125
|
+
fs.copyFileSync(srcPath, destPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
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];
|
|
134
|
+
|
|
135
|
+
if (!dirname) {
|
|
136
|
+
console.error('Error: Provide a name for the new directory');
|
|
137
|
+
console.log('Usage: tada init <dirname> [--default] [--bare]');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const projectDir = path.resolve(process.cwd(), dirname);
|
|
142
|
+
if (fs.existsSync(projectDir)) {
|
|
143
|
+
console.error(`Error: "${dirname}" already exists`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const message = `Creating a new Tada site in ${projectDir}`;
|
|
148
|
+
if (useDefaults && bare) {
|
|
149
|
+
console.log(message + ' using default config (bare)');
|
|
150
|
+
} else if (useDefaults) {
|
|
151
|
+
console.log(message + ' using default config');
|
|
152
|
+
} else if (bare) {
|
|
153
|
+
console.log(message + ' (bare)');
|
|
154
|
+
} else {
|
|
155
|
+
console.log(message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const config: Record<string, string> = {};
|
|
159
|
+
|
|
160
|
+
if (useDefaults) {
|
|
161
|
+
for (const [key, { defaultValue }] of Object.entries(INIT_QUESTIONS)) {
|
|
162
|
+
config[key] = defaultValue;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const questions = Object.entries(INIT_QUESTIONS);
|
|
166
|
+
let qi = 0;
|
|
167
|
+
let { prompt, defaultValue, validate } = questions[qi][1];
|
|
168
|
+
let suffix = defaultValue != null ? ` (default: ${defaultValue})` : '';
|
|
169
|
+
process.stdout.write(`${prompt}${suffix}? `);
|
|
170
|
+
|
|
171
|
+
for await (const line of console) {
|
|
172
|
+
const value = line.trim() || defaultValue || '';
|
|
173
|
+
|
|
174
|
+
if (validate) {
|
|
175
|
+
const error = validate(value);
|
|
176
|
+
if (error) {
|
|
177
|
+
console.error(`Error: ${error}`);
|
|
178
|
+
process.stdout.write(`${prompt}${suffix}? `);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
config[questions[qi][0]] = value;
|
|
184
|
+
qi++;
|
|
185
|
+
|
|
186
|
+
if (qi >= questions.length) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
({ prompt, defaultValue, validate } = questions[qi][1]);
|
|
191
|
+
|
|
192
|
+
suffix = defaultValue != null ? ` (default: ${defaultValue})` : '';
|
|
193
|
+
|
|
194
|
+
process.stdout.write(`${prompt}${suffix}? `);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const {
|
|
199
|
+
title,
|
|
200
|
+
symbol,
|
|
201
|
+
themeColor,
|
|
202
|
+
tintHue,
|
|
203
|
+
tintAmount,
|
|
204
|
+
defaultTimeZone,
|
|
205
|
+
prodBase,
|
|
206
|
+
prodBasePath,
|
|
207
|
+
} = config;
|
|
208
|
+
|
|
209
|
+
// Derive internal domain from production base URL
|
|
210
|
+
const prodDomain = new URL(prodBase).hostname;
|
|
211
|
+
|
|
212
|
+
// Create project directory
|
|
213
|
+
fs.mkdirSync(projectDir);
|
|
214
|
+
|
|
215
|
+
// Generate site configs
|
|
216
|
+
const devConfig = createSiteConfig({
|
|
217
|
+
title,
|
|
218
|
+
symbol,
|
|
219
|
+
themeColor,
|
|
220
|
+
tintHue,
|
|
221
|
+
tintAmount,
|
|
222
|
+
defaultTimeZone,
|
|
223
|
+
base: 'http://localhost:8080',
|
|
224
|
+
basePath: '/',
|
|
225
|
+
internalDomains: ['localhost'],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const prodConfig = createSiteConfig({
|
|
229
|
+
title,
|
|
230
|
+
symbol,
|
|
231
|
+
themeColor,
|
|
232
|
+
tintHue,
|
|
233
|
+
tintAmount,
|
|
234
|
+
defaultTimeZone,
|
|
235
|
+
base: prodBase,
|
|
236
|
+
basePath: prodBasePath,
|
|
237
|
+
internalDomains: [prodDomain],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(projectDir, 'site.dev.json'),
|
|
242
|
+
JSON.stringify(devConfig, null, 2) + '\n',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
fs.writeFileSync(
|
|
246
|
+
path.join(projectDir, 'site.prod.json'),
|
|
247
|
+
JSON.stringify(prodConfig, null, 2) + '\n',
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (bare) {
|
|
251
|
+
fs.mkdirSync(path.join(projectDir, 'content'), { recursive: true });
|
|
252
|
+
fs.writeFileSync(
|
|
253
|
+
path.join(projectDir, 'content', 'index.md'),
|
|
254
|
+
'title: Home\n\nWelcome to your new site.\n',
|
|
255
|
+
);
|
|
256
|
+
fs.mkdirSync(path.join(projectDir, 'public'), { recursive: true });
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
path.join(projectDir, 'nav.json'),
|
|
259
|
+
JSON.stringify(
|
|
260
|
+
[
|
|
261
|
+
{
|
|
262
|
+
title: 'Navigation',
|
|
263
|
+
links: [{ text: 'Home', internal: '/index.html' }],
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
null,
|
|
267
|
+
2,
|
|
268
|
+
) + '\n',
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
// Copy nav and authors data files to the project root
|
|
272
|
+
fs.copyFileSync(
|
|
273
|
+
path.join(packageDir, 'config/nav.json'),
|
|
274
|
+
path.join(projectDir, 'nav.json'),
|
|
275
|
+
);
|
|
276
|
+
fs.copyFileSync(
|
|
277
|
+
path.join(packageDir, 'config/authors.json'),
|
|
278
|
+
path.join(projectDir, 'authors.json'),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Copy content/ and public/ from the package
|
|
282
|
+
copyDirRecursive(
|
|
283
|
+
path.join(packageDir, 'content'),
|
|
284
|
+
path.join(projectDir, 'content'),
|
|
285
|
+
);
|
|
286
|
+
copyDirRecursive(
|
|
287
|
+
path.join(packageDir, 'public'),
|
|
288
|
+
path.join(projectDir, 'public'),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`\nGenerated a new site in ${projectDir}`);
|
|
293
|
+
console.log(`\nNext steps:`);
|
|
294
|
+
console.log(` cd ${dirname}`);
|
|
295
|
+
console.log(` tada dev`);
|
|
296
|
+
console.log(` tada serve`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/*
|
|
300
|
+
* Start of script
|
|
301
|
+
*/
|
|
302
|
+
|
|
303
|
+
const command = process.argv[2];
|
|
304
|
+
|
|
305
|
+
switch (command) {
|
|
306
|
+
case 'init':
|
|
307
|
+
initCommand(process.argv.slice(3));
|
|
308
|
+
break;
|
|
309
|
+
|
|
310
|
+
case 'dev':
|
|
311
|
+
requireSiteConfig('dev');
|
|
312
|
+
run(`bun ${path.join(packageDir, 'build/pipeline.ts')} dev`);
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
case 'prod':
|
|
316
|
+
requireSiteConfig('prod');
|
|
317
|
+
run(`bun ${path.join(packageDir, 'build/pipeline.ts')} prod`);
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case 'watch': {
|
|
321
|
+
requireSiteConfig('dev');
|
|
322
|
+
const watchArgs = process.argv.slice(3).join(' ');
|
|
323
|
+
run(
|
|
324
|
+
`bun ${path.join(packageDir, 'build/watch.ts')}${watchArgs ? ' ' + watchArgs : ''}`,
|
|
325
|
+
);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case 'serve': {
|
|
330
|
+
const serveArgs = process.argv.slice(3).join(' ');
|
|
331
|
+
run(
|
|
332
|
+
`bun ${path.join(packageDir, 'build/serve.ts')}${serveArgs ? ' ' + serveArgs : ''}`,
|
|
333
|
+
);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
case 'clean':
|
|
338
|
+
fs.rmSync(path.resolve(process.cwd(), 'dist'), {
|
|
339
|
+
recursive: true,
|
|
340
|
+
force: true,
|
|
341
|
+
});
|
|
342
|
+
console.log('Cleaned dist/');
|
|
343
|
+
break;
|
|
344
|
+
|
|
345
|
+
case '--version':
|
|
346
|
+
case '-v':
|
|
347
|
+
console.log(`tada v${version}`);
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
default:
|
|
351
|
+
printUsage();
|
|
352
|
+
if (command && command !== '--help' && command !== '-h') {
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { SiteConfigInput } from '../build/types.js';
|
|
3
|
+
import {
|
|
4
|
+
validateSymbol,
|
|
5
|
+
validateColor,
|
|
6
|
+
validateHue,
|
|
7
|
+
validateUrl,
|
|
8
|
+
validateBasePath,
|
|
9
|
+
createSiteConfig,
|
|
10
|
+
} from './validators.js';
|
|
11
|
+
|
|
12
|
+
describe('validateSymbol', () => {
|
|
13
|
+
test('accepts a single uppercase letter', () => {
|
|
14
|
+
expect(validateSymbol('A')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('accepts digits and uppercase letters up to 5 chars', () => {
|
|
18
|
+
expect(validateSymbol('CS 0')).toBeNull();
|
|
19
|
+
expect(validateSymbol('CS101')).toBeNull();
|
|
20
|
+
expect(validateSymbol('AB-CD')).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rejects empty string', () => {
|
|
24
|
+
expect(validateSymbol('')).not.toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('rejects value longer than 5 chars', () => {
|
|
28
|
+
expect(validateSymbol('TOOLONG')).not.toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('rejects lowercase letters', () => {
|
|
32
|
+
expect(validateSymbol('abc')).not.toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('rejects special characters', () => {
|
|
36
|
+
expect(validateSymbol('A@B')).not.toBeNull();
|
|
37
|
+
expect(validateSymbol('A.B')).not.toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('validateColor', () => {
|
|
42
|
+
test('accepts CSS color names', () => {
|
|
43
|
+
expect(validateColor('red')).toBeNull();
|
|
44
|
+
expect(validateColor('tomato')).toBeNull();
|
|
45
|
+
expect(validateColor('blue')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('accepts hex colors', () => {
|
|
49
|
+
expect(validateColor('#f00')).toBeNull();
|
|
50
|
+
expect(validateColor('#ff0000')).toBeNull();
|
|
51
|
+
expect(validateColor('#c04040')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('accepts HSL colors', () => {
|
|
55
|
+
expect(validateColor('hsl(195 70% 40%)')).toBeNull();
|
|
56
|
+
expect(validateColor('hsl(195, 70%, 40%)')).toBeNull();
|
|
57
|
+
expect(validateColor('hsl(195deg 70% 40%)')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('accepts RGB colors', () => {
|
|
61
|
+
expect(validateColor('rgb(0 0 0)')).toBeNull();
|
|
62
|
+
expect(validateColor('rgb(255, 99, 71)')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('rejects empty string', () => {
|
|
66
|
+
expect(validateColor('')).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('rejects invalid color strings', () => {
|
|
70
|
+
expect(validateColor('notacolor')).not.toBeNull();
|
|
71
|
+
expect(validateColor('hsl 195 70% 40%')).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('validateHue', () => {
|
|
76
|
+
test('accepts integer', () => {
|
|
77
|
+
expect(validateHue('0')).toBeNull();
|
|
78
|
+
expect(validateHue('180')).toBeNull();
|
|
79
|
+
expect(validateHue('360')).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('accepts integer with deg suffix', () => {
|
|
83
|
+
expect(validateHue('90deg')).toBeNull();
|
|
84
|
+
expect(validateHue('360deg')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('rejects empty string', () => {
|
|
88
|
+
expect(validateHue('')).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('rejects out of range', () => {
|
|
92
|
+
expect(validateHue('-1')).not.toBeNull();
|
|
93
|
+
expect(validateHue('361')).not.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('rejects non-integer', () => {
|
|
97
|
+
expect(validateHue('1.5')).not.toBeNull();
|
|
98
|
+
expect(validateHue('abc')).not.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('validateUrl', () => {
|
|
103
|
+
test('accepts https URL', () => {
|
|
104
|
+
expect(validateUrl('https://example.edu')).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('accepts http URL', () => {
|
|
108
|
+
expect(validateUrl('http://localhost')).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('accepts URL with port', () => {
|
|
112
|
+
expect(validateUrl('http://localhost:8080')).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('rejects empty string', () => {
|
|
116
|
+
expect(validateUrl('')).not.toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('rejects trailing slash', () => {
|
|
120
|
+
expect(validateUrl('https://example.edu/')).not.toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('rejects URL with path', () => {
|
|
124
|
+
expect(validateUrl('https://example.edu/foo')).not.toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('rejects non-http scheme', () => {
|
|
128
|
+
expect(validateUrl('ftp://example.edu')).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('rejects bare domain', () => {
|
|
132
|
+
expect(validateUrl('example.edu')).not.toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('validateBasePath', () => {
|
|
137
|
+
test('accepts root path', () => {
|
|
138
|
+
expect(validateBasePath('/')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('accepts simple path segment', () => {
|
|
142
|
+
expect(validateBasePath('/foo')).toBeNull();
|
|
143
|
+
expect(validateBasePath('/foo-bar')).toBeNull();
|
|
144
|
+
expect(validateBasePath('/cs101')).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('rejects path without leading slash', () => {
|
|
148
|
+
expect(validateBasePath('foo')).not.toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('rejects trailing slash', () => {
|
|
152
|
+
expect(validateBasePath('/foo/')).not.toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('rejects spaces', () => {
|
|
156
|
+
expect(validateBasePath('/foo bar')).not.toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('rejects nested paths', () => {
|
|
160
|
+
expect(validateBasePath('/foo/bar')).not.toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('createSiteConfig', () => {
|
|
165
|
+
const base: SiteConfigInput = {
|
|
166
|
+
title: 'Test Site',
|
|
167
|
+
symbol: 'TS',
|
|
168
|
+
themeColor: 'hsl(200 50% 40%)',
|
|
169
|
+
tintHue: '20',
|
|
170
|
+
tintAmount: '100',
|
|
171
|
+
defaultTimeZone: 'America/New_York',
|
|
172
|
+
base: 'https://example.edu',
|
|
173
|
+
basePath: '/',
|
|
174
|
+
internalDomains: ['example.edu'],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
test('returns an object with all expected keys', () => {
|
|
178
|
+
const config = createSiteConfig(base);
|
|
179
|
+
expect(config).toMatchObject({
|
|
180
|
+
title: 'Test Site',
|
|
181
|
+
symbol: 'TS',
|
|
182
|
+
themeColor: 'hsl(200 50% 40%)',
|
|
183
|
+
base: 'https://example.edu',
|
|
184
|
+
basePath: '/',
|
|
185
|
+
internalDomains: ['example.edu'],
|
|
186
|
+
defaultTimeZone: 'America/New_York',
|
|
187
|
+
features: { search: true, code: true, favicon: true },
|
|
188
|
+
codeLanguages: { java: 'java', py: 'python' },
|
|
189
|
+
vars: {},
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('coerces tintHue and tintAmount to numbers', () => {
|
|
194
|
+
const config = createSiteConfig(base);
|
|
195
|
+
expect(config.tintHue).toBe(20);
|
|
196
|
+
expect(config.tintAmount).toBe(100);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('coerces numeric string tintHue correctly', () => {
|
|
200
|
+
const config = createSiteConfig({ ...base, tintHue: '0', tintAmount: '0' });
|
|
201
|
+
expect(config.tintHue).toBe(0);
|
|
202
|
+
expect(config.tintAmount).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
});
|