@abreen/tada 1.0.2 → 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 -151
- 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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { getDevSiteVariables, getProdSiteVariables } from './site-variables.js';
|
|
4
|
+
import { compileTemplates } from './templates.js';
|
|
5
|
+
import { getDistDir, getContentDir, getPublicDir } from './utils/paths.js';
|
|
6
|
+
import { isFeatureEnabled } from './features.js';
|
|
7
|
+
import { bundle } from './bundle.js';
|
|
8
|
+
import { copyFonts } from './generate-fonts.js';
|
|
9
|
+
import { generateFavicons } from './generate-favicon.js';
|
|
10
|
+
import { generateManifest } from './generate-manifest.js';
|
|
11
|
+
import { copyPublicFiles, copyContentAssets } from './copy.js';
|
|
12
|
+
import { getProcessedExtensions } from './utils/file-types.js';
|
|
13
|
+
import { ContentRenderer } from './generate-content-assets.js';
|
|
14
|
+
import { runPagefind } from './pagefind.js';
|
|
15
|
+
import { makeLogger, printFlair } from './log.js';
|
|
16
|
+
|
|
17
|
+
const log = makeLogger(__filename);
|
|
18
|
+
|
|
19
|
+
async function runPipeline(mode: 'development' | 'production'): Promise<void> {
|
|
20
|
+
const isDev = mode === 'development';
|
|
21
|
+
const siteVariables = isDev ? getDevSiteVariables() : getProdSiteVariables();
|
|
22
|
+
const distDir = getDistDir();
|
|
23
|
+
const contentDir = getContentDir();
|
|
24
|
+
const publicDir = getPublicDir();
|
|
25
|
+
|
|
26
|
+
// Ensure dist/ exists
|
|
27
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
// Phase 1: Setup
|
|
30
|
+
compileTemplates(siteVariables);
|
|
31
|
+
const contentRenderer = new ContentRenderer(siteVariables);
|
|
32
|
+
await contentRenderer.initHighlighter();
|
|
33
|
+
|
|
34
|
+
// Phase 2: Bundle + assets (parallel)
|
|
35
|
+
const parallelTasks: (Promise<unknown> | void)[] = [
|
|
36
|
+
bundle(siteVariables, { mode }),
|
|
37
|
+
copyFonts(distDir),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
41
|
+
parallelTasks.push(generateFavicons(siteVariables, distDir));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const results = await Promise.all(parallelTasks);
|
|
45
|
+
const assetFiles = results[0] as string[]; // bundle output filenames
|
|
46
|
+
|
|
47
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
48
|
+
generateManifest(siteVariables, distDir);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const publicRelPaths = copyPublicFiles(publicDir, distDir);
|
|
52
|
+
const processedExtensions = getProcessedExtensions(
|
|
53
|
+
Object.keys(siteVariables.codeLanguages || {}),
|
|
54
|
+
);
|
|
55
|
+
copyContentAssets(contentDir, distDir, processedExtensions, publicRelPaths);
|
|
56
|
+
|
|
57
|
+
// Phase 3: Content rendering
|
|
58
|
+
const { errors, htmlAssetsByPath } = contentRenderer.processContent({
|
|
59
|
+
distDir,
|
|
60
|
+
assetFiles,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
for (const err of errors) {
|
|
64
|
+
log.error`${err.message}`;
|
|
65
|
+
}
|
|
66
|
+
if (errors.length > 0) {
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Phase 4: Post-processing
|
|
71
|
+
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
72
|
+
await runPagefind({ siteVariables, distPath: distDir, htmlAssetsByPath });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
printFlair();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CLI entry point
|
|
79
|
+
const arg = process.argv[2];
|
|
80
|
+
if (arg === 'dev') {
|
|
81
|
+
runPipeline('development').catch(err => {
|
|
82
|
+
log.error`Build failed: ${err.message}`;
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
} else if (arg === 'prod') {
|
|
86
|
+
runPipeline('production').catch(err => {
|
|
87
|
+
log.error`Build failed: ${err.message}`;
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
console.error('Usage: pipeline.js <dev|prod>');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
3
|
collectDirectSiteAssetLinks,
|
|
4
4
|
collectReachableSiteAssets,
|
|
5
|
-
}
|
|
5
|
+
} from './reachability.js';
|
|
6
6
|
|
|
7
7
|
describe('reachability', () => {
|
|
8
8
|
test('collectDirectSiteAssetLinks resolves relative links and ignores excluded links', () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
3
|
|
|
4
|
-
function stripQueryAndHash(href) {
|
|
4
|
+
function stripQueryAndHash(href: string): string {
|
|
5
5
|
return href.split('#')[0].split('?')[0];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function isExternalOrAnchor(href) {
|
|
8
|
+
function isExternalOrAnchor(href: string): boolean {
|
|
9
9
|
if (!href || href.startsWith('#') || href.startsWith('//')) {
|
|
10
10
|
return true;
|
|
11
11
|
}
|
|
@@ -13,7 +13,7 @@ function isExternalOrAnchor(href) {
|
|
|
13
13
|
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function normalizeUrlPath(pathname) {
|
|
16
|
+
function normalizeUrlPath(pathname: string): string {
|
|
17
17
|
const normalized = path.posix.normalize(pathname);
|
|
18
18
|
if (normalized === '.' || normalized === '') {
|
|
19
19
|
return '/';
|
|
@@ -22,7 +22,7 @@ function normalizeUrlPath(pathname) {
|
|
|
22
22
|
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function stripBasePath(pathname, basePath) {
|
|
25
|
+
function stripBasePath(pathname: string, basePath: string): string {
|
|
26
26
|
const normalizedPath = normalizeUrlPath(pathname);
|
|
27
27
|
const normalizedBasePath = normalizeUrlPath(basePath || '/');
|
|
28
28
|
|
|
@@ -42,7 +42,7 @@ function stripBasePath(pathname, basePath) {
|
|
|
42
42
|
return normalizedPath;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function toCandidateAssetPaths(urlPath) {
|
|
45
|
+
function toCandidateAssetPaths(urlPath: string): string[] {
|
|
46
46
|
const normalizedPath = normalizeUrlPath(urlPath);
|
|
47
47
|
|
|
48
48
|
if (normalizedPath === '/') {
|
|
@@ -65,7 +65,17 @@ function toCandidateAssetPaths(urlPath) {
|
|
|
65
65
|
return [`${withoutLeadingSlash}/index.html`, `${withoutLeadingSlash}.html`];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
interface ResolveInternalPathOptions {
|
|
69
|
+
href: string | null;
|
|
70
|
+
fromAssetPath: string;
|
|
71
|
+
basePath?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveInternalPath({
|
|
75
|
+
href,
|
|
76
|
+
fromAssetPath,
|
|
77
|
+
basePath = '/',
|
|
78
|
+
}: ResolveInternalPathOptions): string | null {
|
|
69
79
|
if (!href) {
|
|
70
80
|
return null;
|
|
71
81
|
}
|
|
@@ -80,7 +90,7 @@ function resolveInternalPath({ href, fromAssetPath, basePath = '/' }) {
|
|
|
80
90
|
return null;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
|
|
93
|
+
const resolvedPath = pathname.startsWith('/')
|
|
84
94
|
? stripBasePath(pathname, basePath)
|
|
85
95
|
: normalizeUrlPath(
|
|
86
96
|
path.posix.join(path.posix.dirname(`/${fromAssetPath}`), pathname),
|
|
@@ -93,12 +103,19 @@ function resolveInternalPath({ href, fromAssetPath, basePath = '/' }) {
|
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
interface ResolveHrefToHtmlOptions {
|
|
107
|
+
href: string | null;
|
|
108
|
+
fromAssetPath: string;
|
|
109
|
+
basePath?: string;
|
|
110
|
+
knownAssets: Set<string>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveHrefToHtmlAssetPath({
|
|
97
114
|
href,
|
|
98
115
|
fromAssetPath,
|
|
99
116
|
basePath = '/',
|
|
100
117
|
knownAssets,
|
|
101
|
-
}) {
|
|
118
|
+
}: ResolveHrefToHtmlOptions): string | null {
|
|
102
119
|
const decodedPath = resolveInternalPath({ href, fromAssetPath, basePath });
|
|
103
120
|
if (!decodedPath) {
|
|
104
121
|
return null;
|
|
@@ -113,12 +130,19 @@ function resolveHrefToHtmlAssetPath({
|
|
|
113
130
|
return null;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
interface ResolveHrefToPdfOptions {
|
|
134
|
+
href: string | null;
|
|
135
|
+
fromAssetPath: string;
|
|
136
|
+
basePath?: string;
|
|
137
|
+
knownPdfPaths: Set<string>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveHrefToPdfPath({
|
|
117
141
|
href,
|
|
118
142
|
fromAssetPath,
|
|
119
143
|
basePath = '/',
|
|
120
144
|
knownPdfPaths,
|
|
121
|
-
}) {
|
|
145
|
+
}: ResolveHrefToPdfOptions): string | null {
|
|
122
146
|
const decodedPath = resolveInternalPath({ href, fromAssetPath, basePath });
|
|
123
147
|
if (!decodedPath || !decodedPath.toLowerCase().endsWith('.pdf')) {
|
|
124
148
|
return null;
|
|
@@ -127,15 +151,28 @@ function resolveHrefToPdfPath({
|
|
|
127
151
|
return knownPdfPaths.has(decodedPath) ? decodedPath : null;
|
|
128
152
|
}
|
|
129
153
|
|
|
130
|
-
|
|
154
|
+
interface CollectDirectLinksOptions {
|
|
155
|
+
html: string;
|
|
156
|
+
fromAssetPath: string;
|
|
157
|
+
knownAssets: Set<string>;
|
|
158
|
+
knownPdfPaths?: Set<string>;
|
|
159
|
+
basePath?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface DirectSiteAssetLinks {
|
|
163
|
+
htmlAssetPaths: string[];
|
|
164
|
+
pdfPaths: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function collectDirectSiteAssetLinks({
|
|
131
168
|
html,
|
|
132
169
|
fromAssetPath,
|
|
133
170
|
knownAssets,
|
|
134
171
|
knownPdfPaths = new Set(),
|
|
135
172
|
basePath = '/',
|
|
136
|
-
}) {
|
|
137
|
-
const htmlAssetPaths = new Set();
|
|
138
|
-
const pdfPaths = new Set();
|
|
173
|
+
}: CollectDirectLinksOptions): DirectSiteAssetLinks {
|
|
174
|
+
const htmlAssetPaths = new Set<string>();
|
|
175
|
+
const pdfPaths = new Set<string>();
|
|
139
176
|
const dom = new JSDOM(html);
|
|
140
177
|
|
|
141
178
|
try {
|
|
@@ -189,29 +226,41 @@ function collectDirectSiteAssetLinks({
|
|
|
189
226
|
};
|
|
190
227
|
}
|
|
191
228
|
|
|
192
|
-
|
|
229
|
+
interface CollectReachableOptions {
|
|
230
|
+
htmlAssetsByPath: Map<string, string>;
|
|
231
|
+
knownPdfPaths?: Set<string>;
|
|
232
|
+
rootPath?: string;
|
|
233
|
+
basePath?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
interface ReachableSiteAssets {
|
|
237
|
+
reachableHtmlPaths: string[];
|
|
238
|
+
reachablePdfPaths: string[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function collectReachableSiteAssets({
|
|
193
242
|
htmlAssetsByPath,
|
|
194
243
|
knownPdfPaths = new Set(),
|
|
195
244
|
rootPath = 'index.html',
|
|
196
245
|
basePath = '/',
|
|
197
|
-
}) {
|
|
246
|
+
}: CollectReachableOptions): ReachableSiteAssets {
|
|
198
247
|
if (!htmlAssetsByPath.has(rootPath)) {
|
|
199
248
|
throw new Error(`Pagefind reachability root not found: ${rootPath}`);
|
|
200
249
|
}
|
|
201
250
|
|
|
202
251
|
const knownAssets = new Set(htmlAssetsByPath.keys());
|
|
203
|
-
const reachable = new Set();
|
|
204
|
-
const reachablePdfPaths = new Set();
|
|
205
|
-
const pending = [rootPath];
|
|
252
|
+
const reachable = new Set<string>();
|
|
253
|
+
const reachablePdfPaths = new Set<string>();
|
|
254
|
+
const pending: string[] = [rootPath];
|
|
206
255
|
|
|
207
256
|
while (pending.length > 0) {
|
|
208
|
-
const currentPath = pending.pop()
|
|
257
|
+
const currentPath = pending.pop()!;
|
|
209
258
|
if (reachable.has(currentPath)) {
|
|
210
259
|
continue;
|
|
211
260
|
}
|
|
212
261
|
reachable.add(currentPath);
|
|
213
262
|
|
|
214
|
-
const html = htmlAssetsByPath.get(currentPath)
|
|
263
|
+
const html = htmlAssetsByPath.get(currentPath)!;
|
|
215
264
|
const { htmlAssetPaths, pdfPaths } = collectDirectSiteAssetLinks({
|
|
216
265
|
html,
|
|
217
266
|
fromAssetPath: currentPath,
|
|
@@ -237,11 +286,13 @@ function collectReachableSiteAssets({
|
|
|
237
286
|
};
|
|
238
287
|
}
|
|
239
288
|
|
|
240
|
-
function collectReachableHtmlAssets(
|
|
289
|
+
export function collectReachableHtmlAssets(
|
|
290
|
+
options: CollectReachableOptions,
|
|
291
|
+
): string[] {
|
|
241
292
|
return collectReachableSiteAssets(options).reachableHtmlPaths;
|
|
242
293
|
}
|
|
243
294
|
|
|
244
|
-
function getMetaRefreshTarget(document) {
|
|
295
|
+
function getMetaRefreshTarget(document: Document): string | null {
|
|
245
296
|
const refreshTags = document.querySelectorAll('meta[http-equiv]');
|
|
246
297
|
for (const tag of refreshTags) {
|
|
247
298
|
const httpEquiv = tag.getAttribute('http-equiv');
|
|
@@ -263,11 +314,3 @@ function getMetaRefreshTarget(document) {
|
|
|
263
314
|
|
|
264
315
|
return null;
|
|
265
316
|
}
|
|
266
|
-
|
|
267
|
-
module.exports = {
|
|
268
|
-
collectDirectSiteAssetLinks,
|
|
269
|
-
collectReachableHtmlAssets,
|
|
270
|
-
collectReachableSiteAssets,
|
|
271
|
-
resolveHrefToHtmlAssetPath,
|
|
272
|
-
resolveHrefToPdfPath,
|
|
273
|
-
};
|
package/build/serve.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getDistDir } from './util.js';
|
|
4
|
+
import { makeLogger } from './log.js';
|
|
5
|
+
import { B } from './colors.js';
|
|
6
|
+
|
|
7
|
+
const log = makeLogger(__filename);
|
|
8
|
+
|
|
9
|
+
function messageReady(port: number): void {
|
|
10
|
+
if (process.send) {
|
|
11
|
+
process.send({ ready: true, port });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let distDir: string;
|
|
16
|
+
try {
|
|
17
|
+
distDir = getDistDir();
|
|
18
|
+
} catch (err) {
|
|
19
|
+
log.error`Failed to start server: ${err}`;
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolvePathname(pathname: string): string | null {
|
|
24
|
+
let decodedPath: string;
|
|
25
|
+
try {
|
|
26
|
+
decodedPath = decodeURIComponent(pathname);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolvedPath = path.resolve(distDir, '.' + decodedPath);
|
|
32
|
+
const relativePath = path.relative(distDir, resolvedPath);
|
|
33
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let stat: fs.Stats;
|
|
38
|
+
try {
|
|
39
|
+
stat = fs.statSync(resolvedPath);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!stat.isFile()) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return resolvedPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createResponse(req: Request): Response {
|
|
56
|
+
const url = new URL(req.url);
|
|
57
|
+
const filePath = resolvePathname(url.pathname);
|
|
58
|
+
if (!filePath) {
|
|
59
|
+
return new Response('Not Found', { status: 404 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response(Bun.file(filePath));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getPortArg(): number {
|
|
66
|
+
const idx = process.argv.indexOf('--port');
|
|
67
|
+
if (idx === -1) {
|
|
68
|
+
return 8080;
|
|
69
|
+
}
|
|
70
|
+
const raw = process.argv[idx + 1];
|
|
71
|
+
if (!raw) {
|
|
72
|
+
log.error`--port requires a value`;
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const port = parseInt(raw, 10);
|
|
76
|
+
if (isNaN(port) || port <= 0 || port >= 65536) {
|
|
77
|
+
log.error`Invalid port value: ${raw}`;
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return port;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listen(port: number): void {
|
|
84
|
+
try {
|
|
85
|
+
const server = Bun.serve({
|
|
86
|
+
port,
|
|
87
|
+
fetch: createResponse,
|
|
88
|
+
error(error: Error) {
|
|
89
|
+
log.error`Request failed: ${error}`;
|
|
90
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
log.info`Dev server: ${B`http://localhost:${server.port}/index.html`}`;
|
|
95
|
+
messageReady(server.port as number);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
log.error`Failed to start server on port ${port}: ${err}`;
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
listen(getPortArg());
|
|
103
|
+
|
|
104
|
+
process.on('uncaughtException', err => {
|
|
105
|
+
log.error`Uncaught exception: ${err}`;
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
process.on('unhandledRejection', reason => {
|
|
110
|
+
log.error`Unhandled rejection: ${reason}`;
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { compile as compileJsonSchema, doValidation } from './json-schema.js';
|
|
4
|
+
import { getProjectDir } from './utils/paths.js';
|
|
5
|
+
import type { SiteVariables } from './types.js';
|
|
6
|
+
import siteSchema from './site.schema.json' with { type: 'json' };
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const configDir = getProjectDir();
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const DEFAULT: Partial<SiteVariables> = {
|
|
11
|
+
basePath: '/',
|
|
12
|
+
features: { search: true, code: true },
|
|
13
|
+
};
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
const isValid = compileJsonSchema(siteSchema);
|
|
16
|
+
|
|
17
|
+
function getJson(filePath: string): Record<string, unknown> {
|
|
12
18
|
return JSON.parse(
|
|
13
19
|
fs.readFileSync(path.resolve(configDir, filePath), 'utf-8'),
|
|
14
20
|
);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
function getSiteVariables(env) {
|
|
23
|
+
function getSiteVariables(env: string): SiteVariables {
|
|
18
24
|
const fileName = `site.${env}.json`;
|
|
19
25
|
const fromFile = getJson(fileName);
|
|
20
26
|
const variables = {
|
|
21
27
|
...DEFAULT,
|
|
22
28
|
...fromFile,
|
|
23
|
-
features: {
|
|
24
|
-
|
|
29
|
+
features: {
|
|
30
|
+
...DEFAULT.features,
|
|
31
|
+
...((fromFile.features as Record<string, unknown>) || {}),
|
|
32
|
+
},
|
|
33
|
+
} as SiteVariables;
|
|
25
34
|
|
|
26
35
|
// Derive faviconSymbol from symbol if not explicitly set
|
|
27
36
|
if (variables.symbol && !variables.faviconSymbol) {
|
|
@@ -42,12 +51,10 @@ function getSiteVariables(env) {
|
|
|
42
51
|
return variables;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
function getDevSiteVariables() {
|
|
54
|
+
export function getDevSiteVariables(): SiteVariables {
|
|
46
55
|
return getSiteVariables('dev');
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
function getProdSiteVariables() {
|
|
58
|
+
export function getProdSiteVariables(): SiteVariables {
|
|
50
59
|
return getSiteVariables('prod');
|
|
51
60
|
}
|
|
52
|
-
|
|
53
|
-
module.exports = { getDevSiteVariables, getProdSiteVariables };
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"search": { "type": "boolean", "default": true },
|
|
10
10
|
"code": { "type": "boolean", "default": true },
|
|
11
|
-
"favicon": { "type": "boolean", "default": false }
|
|
12
|
-
"literateJava": { "type": "boolean", "default": false }
|
|
11
|
+
"favicon": { "type": "boolean", "default": false }
|
|
13
12
|
},
|
|
14
13
|
"required": ["search", "code", "favicon"],
|
|
15
14
|
"additionalProperties": false
|
|
@@ -29,10 +28,7 @@
|
|
|
29
28
|
"symbol": { "type": "string", "pattern": "^[A-Z0-9\\- ]{1,5}$" },
|
|
30
29
|
"titlePostfix": { "type": "string" },
|
|
31
30
|
"courseId": { "type": "string" },
|
|
32
|
-
"themeColor": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"pattern": "^hsl\\(\\d+(deg)? \\d+% \\d+%\\)$"
|
|
35
|
-
},
|
|
31
|
+
"themeColor": { "type": "string" },
|
|
36
32
|
"tintHue": {
|
|
37
33
|
"type": "integer",
|
|
38
34
|
"minimum": 0,
|
|
@@ -45,10 +41,7 @@
|
|
|
45
41
|
"maximum": 100,
|
|
46
42
|
"default": 100
|
|
47
43
|
},
|
|
48
|
-
"faviconColor": {
|
|
49
|
-
"type": "string",
|
|
50
|
-
"pattern": "^hsl\\(\\d+(deg)? \\d+% \\d+%\\)$"
|
|
51
|
-
},
|
|
44
|
+
"faviconColor": { "type": "string" },
|
|
52
45
|
"faviconSymbol": { "type": "string", "pattern": "^[A-Z0-9\\- ]{1,5}$" },
|
|
53
46
|
"faviconFontWeight": {
|
|
54
47
|
"type": "integer",
|
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { compile as compileJsonSchema, doValidation } from './json-schema.js';
|
|
5
|
+
import { makeLogger } from './log.js';
|
|
6
|
+
import { getPackageDir, getConfigDir } from './utils/paths.js';
|
|
7
|
+
import type { ValidateFunction } from 'ajv';
|
|
8
|
+
import type { Logger, SiteVariables } from './types.js';
|
|
6
9
|
|
|
7
|
-
const log = makeLogger(__filename);
|
|
10
|
+
const log: Logger = makeLogger(__filename);
|
|
8
11
|
|
|
9
12
|
// Store all templates in memory (don't read template files during build)
|
|
10
|
-
const templates = {};
|
|
13
|
+
const templates: Record<string, string> = {};
|
|
11
14
|
|
|
12
15
|
// All parsed data (from .json files)
|
|
13
|
-
const jsonData = {};
|
|
16
|
+
const jsonData: Record<string, unknown> = {};
|
|
14
17
|
|
|
15
18
|
// Compiled JSON Schema for the .json files
|
|
16
|
-
const validators = {};
|
|
19
|
+
const validators: Record<string, ValidateFunction> = {};
|
|
17
20
|
|
|
18
21
|
// Keeps track of template call tree
|
|
19
|
-
const renderStack = [];
|
|
20
|
-
let errorStack = null;
|
|
22
|
+
const renderStack: string[] = [];
|
|
23
|
+
let errorStack: string[] | null = null;
|
|
21
24
|
|
|
22
25
|
// JSON data files that live in the user's config directory
|
|
23
|
-
const JSON_DATA_FILES = ['nav.json', 'authors.json'];
|
|
26
|
+
const JSON_DATA_FILES: string[] = ['nav.json', 'authors.json'];
|
|
24
27
|
|
|
25
|
-
function getHtmlTemplatesDir() {
|
|
26
|
-
const { getPackageDir } = require('./utils/paths');
|
|
28
|
+
function getHtmlTemplatesDir(): string {
|
|
27
29
|
return path.resolve(getPackageDir(), 'templates');
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function getJsonDataDir() {
|
|
31
|
-
const { getConfigDir } = require('./utils/paths');
|
|
32
|
+
function getJsonDataDir(): string {
|
|
32
33
|
return getConfigDir();
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function json(fileName) {
|
|
36
|
+
export function json(fileName: string): unknown {
|
|
36
37
|
return jsonData[fileName];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function render(
|
|
40
|
+
export function render(
|
|
41
|
+
fileName: string,
|
|
42
|
+
params?: Record<string, unknown> | null,
|
|
43
|
+
): string | undefined {
|
|
40
44
|
if (params != null) {
|
|
41
45
|
// Allow the template to call render(), it will use our params
|
|
42
|
-
params.render = otherFileName => render(otherFileName, params);
|
|
46
|
+
params.render = (otherFileName: string) => render(otherFileName, params);
|
|
43
47
|
|
|
44
48
|
// Allow the template to read the JSON files we previously read into memory
|
|
45
49
|
params.json = json;
|
|
@@ -47,7 +51,7 @@ function render(fileName, params) {
|
|
|
47
51
|
|
|
48
52
|
renderStack.push(fileName);
|
|
49
53
|
try {
|
|
50
|
-
return _.template(templates[fileName])(params);
|
|
54
|
+
return _.template(templates[fileName])(params ?? undefined);
|
|
51
55
|
} catch (err) {
|
|
52
56
|
if (errorStack == null) {
|
|
53
57
|
errorStack = renderStack.slice();
|
|
@@ -57,15 +61,20 @@ function render(fileName, params) {
|
|
|
57
61
|
}
|
|
58
62
|
} else if (renderStack.length === 1) {
|
|
59
63
|
const topItem = errorStack[errorStack.length - 1];
|
|
60
|
-
throw new Error(`Render error in ${topItem}: ${err}
|
|
64
|
+
throw new Error(`Render error in ${topItem}: ${err}`, { cause: err });
|
|
61
65
|
}
|
|
62
66
|
} finally {
|
|
63
67
|
renderStack.pop();
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
function compileTemplates(
|
|
68
|
-
|
|
71
|
+
export function compileTemplates(
|
|
72
|
+
siteVariables: SiteVariables,
|
|
73
|
+
quiet: boolean = false,
|
|
74
|
+
): void {
|
|
75
|
+
if (!quiet) {
|
|
76
|
+
log.debug`Compiling templates`;
|
|
77
|
+
}
|
|
69
78
|
|
|
70
79
|
Object.keys(templates).forEach(k => delete templates[k]);
|
|
71
80
|
Object.keys(jsonData).forEach(k => delete jsonData[k]);
|
|
@@ -88,7 +97,7 @@ function compileTemplates(siteVariables) {
|
|
|
88
97
|
for (const fileName of JSON_DATA_FILES) {
|
|
89
98
|
const filePath = path.join(jsonDir, fileName);
|
|
90
99
|
if (!fs.existsSync(filePath)) {
|
|
91
|
-
|
|
100
|
+
continue;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
// Schema validation (schemas live in the package templates dir)
|
|
@@ -113,16 +122,9 @@ function compileTemplates(siteVariables) {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
function compileAndSetValidator(schemaPath, fileName) {
|
|
125
|
+
function compileAndSetValidator(schemaPath: string, fileName: string): void {
|
|
117
126
|
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
118
127
|
validators[fileName] = compileJsonSchema(schema);
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
compileTemplates,
|
|
123
|
-
render,
|
|
124
|
-
getHtmlTemplatesDir,
|
|
125
|
-
getJsonDataDir,
|
|
126
|
-
json,
|
|
127
|
-
JSON_DATA_FILES,
|
|
128
|
-
};
|
|
130
|
+
export { getHtmlTemplatesDir, getJsonDataDir, JSON_DATA_FILES };
|