@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.
Files changed (120) hide show
  1. package/README.md +29 -33
  2. package/bin/tada.ts +356 -0
  3. package/bin/validators.test.ts +204 -0
  4. package/bin/validators.ts +83 -0
  5. package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
  6. package/build/bundle.ts +117 -0
  7. package/{webpack/code.test.js → build/code.test.ts} +6 -7
  8. package/build/colors.ts +25 -0
  9. package/build/content-watch.ts +107 -0
  10. package/build/copy.ts +118 -0
  11. package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
  12. package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
  13. package/build/features.ts +11 -0
  14. package/build/generate-content-assets.ts +315 -0
  15. package/build/generate-favicon.ts +165 -0
  16. package/build/generate-fonts.ts +31 -0
  17. package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
  18. package/build/globals.test.ts +101 -0
  19. package/{webpack/globals.js → build/globals.ts} +28 -13
  20. package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
  21. package/build/json-schema.test.ts +57 -0
  22. package/build/json-schema.ts +33 -0
  23. package/build/log.test.ts +111 -0
  24. package/build/log.ts +167 -0
  25. package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
  26. package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
  27. package/build/pagefind.ts +339 -0
  28. package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
  29. package/build/pipeline.ts +93 -0
  30. package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
  31. package/{webpack/reachability.js → build/reachability.ts} +77 -34
  32. package/build/serve.ts +112 -0
  33. package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
  34. package/{webpack → build}/site.schema.json +3 -10
  35. package/{webpack/templates.js → build/templates.ts} +35 -33
  36. package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
  37. package/build/toc-plugin.test.ts +105 -0
  38. package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
  39. package/build/types.ts +172 -0
  40. package/build/util.ts +26 -0
  41. package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
  42. package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
  43. package/build/utils/derive-theme.test.ts +111 -0
  44. package/build/utils/derive-theme.ts +85 -0
  45. package/build/utils/file-types.test.ts +61 -0
  46. package/build/utils/file-types.ts +13 -0
  47. package/build/utils/front-matter.test.ts +80 -0
  48. package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
  49. package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
  50. package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
  51. package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
  52. package/build/utils/paths.test.ts +91 -0
  53. package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
  54. package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
  55. package/build/utils/shiki-highlighter.ts +29 -0
  56. package/build/validate-internal-links-plugin.test.ts +106 -0
  57. package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
  58. package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
  59. package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
  60. package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
  61. package/build/watch.ts +573 -0
  62. package/content/index.md +9 -3
  63. package/content/markdown.md +2 -1
  64. package/content/problem_sets/index.html +14 -0
  65. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
  66. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
  67. package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
  68. package/fonts/inter/woff2/InterVariable.woff2 +0 -0
  69. package/package.json +28 -19
  70. package/src/_alerts.scss +92 -0
  71. package/src/_base.scss +106 -0
  72. package/src/{layout.scss → _layout.scss} +0 -2
  73. package/src/anchor/style.scss +1 -9
  74. package/src/code/index.ts +3 -3
  75. package/src/code.scss +1 -1
  76. package/src/critical.scss +5 -0
  77. package/src/header/_base.scss +129 -0
  78. package/src/header/style.scss +3 -131
  79. package/src/index.ts +1 -2
  80. package/src/question/style.scss +1 -1
  81. package/src/search/index.ts +36 -15
  82. package/src/search/style.scss +9 -15
  83. package/src/style.scss +6 -269
  84. package/src/toc/style.scss +5 -39
  85. package/src/util.ts +8 -5
  86. package/templates/_theme.scss +38 -14
  87. package/tsconfig.json +10 -6
  88. package/types/file-system-access.d.ts +5 -0
  89. package/types/markdown-it-plugins.d.ts +11 -0
  90. package/types/untyped-modules.d.ts +40 -0
  91. package/bin/tada.js +0 -361
  92. package/content/problem_sets/index.md +0 -6
  93. package/webpack/build-state.js +0 -97
  94. package/webpack/colors.js +0 -15
  95. package/webpack/config.base.js +0 -151
  96. package/webpack/config.dev.js +0 -23
  97. package/webpack/config.prod.js +0 -32
  98. package/webpack/content-watch-plugin.js +0 -153
  99. package/webpack/features.js +0 -5
  100. package/webpack/generate-content-assets-plugin.js +0 -308
  101. package/webpack/generate-favicon-plugin.js +0 -198
  102. package/webpack/generate-fonts-plugin.js +0 -69
  103. package/webpack/json-schema.js +0 -19
  104. package/webpack/log.js +0 -143
  105. package/webpack/pagefind-plugin.js +0 -379
  106. package/webpack/print-flair-plugin.js +0 -22
  107. package/webpack/serve.js +0 -104
  108. package/webpack/util.js +0 -49
  109. package/webpack/utils/define-plugin.js +0 -20
  110. package/webpack/utils/file-types.js +0 -26
  111. package/webpack/utils/parse-hsl.js +0 -8
  112. package/webpack/utils/shiki-highlighter.js +0 -26
  113. package/webpack/watch.js +0 -166
  114. /package/{webpack → build}/flair.json +0 -0
  115. /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
  116. /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
  117. /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
  118. /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
  119. /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
  120. /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
- const { describe, expect, test } = require('bun:test');
2
- const {
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
3
  collectDirectSiteAssetLinks,
4
4
  collectReachableSiteAssets,
5
- } = require('./reachability');
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
- const path = require('path');
2
- const { JSDOM } = require('jsdom');
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
- function resolveInternalPath({ href, fromAssetPath, basePath = '/' }) {
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
- let resolvedPath = pathname.startsWith('/')
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
- function resolveHrefToHtmlAssetPath({
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
- function resolveHrefToPdfPath({
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
- function collectDirectSiteAssetLinks({
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
- function collectReachableSiteAssets({
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(options) {
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
- const fs = require('fs');
2
- const path = require('path');
3
- const { compile: compileJsonSchema, doValidation } = require('./json-schema');
4
- const { getProjectDir } = require('./utils/paths');
5
- const configDir = path.resolve(getProjectDir(), 'config');
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 DEFAULT = { basePath: '/', features: { search: true, code: true } };
8
+ const configDir = getProjectDir();
8
9
 
9
- const isValid = compileJsonSchema(require('./site.schema.json'));
10
+ const DEFAULT: Partial<SiteVariables> = {
11
+ basePath: '/',
12
+ features: { search: true, code: true },
13
+ };
10
14
 
11
- function getJson(filePath) {
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: { ...DEFAULT.features, ...(fromFile.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
- const fs = require('fs');
2
- const path = require('path');
3
- const _ = require('lodash');
4
- const { compile: compileJsonSchema, doValidation } = require('./json-schema');
5
- const { makeLogger } = require('./log');
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(fileName, params) {
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(siteVariables) {
68
- log.info`Compiling templates`;
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
- throw new Error(`Missing required data file: ${filePath}`);
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
- module.exports = {
122
- compileTemplates,
123
- render,
124
- getHtmlTemplatesDir,
125
- getJsonDataDir,
126
- json,
127
- JSON_DATA_FILES,
128
- };
130
+ export { getHtmlTemplatesDir, getJsonDataDir, JSON_DATA_FILES };
@@ -1,8 +1,8 @@
1
- module.exports = function textToId(value) {
1
+ export default function textToId(value: unknown): string {
2
2
  const text = value == null ? '' : String(value);
3
3
  return text
4
4
  .trim()
5
5
  .toLowerCase()
6
6
  .replace(/\s+/g, '-')
7
7
  .replace(/[^\w-]/g, '');
8
- };
8
+ }