generator-white-label 5.0.0 → 8.0.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 (299) hide show
  1. package/.editorconfig +15 -0
  2. package/.yarnrc.yml +6 -0
  3. package/CLI.md +82 -0
  4. package/LICENSE +9 -0
  5. package/PACKAGE_MANAGERS.md +35 -0
  6. package/README.md +204 -124
  7. package/app/404.tsx +28 -0
  8. package/app/README.md +66 -8
  9. package/app/assets/data/view/global.json +7 -47
  10. package/app/assets/data/view/index.json +9 -1
  11. package/app/assets/script/index.tsx +13 -0
  12. package/app/assets/script/tasks/TaskApplication.ts +77 -0
  13. package/app/assets/script/tasks/TaskMediator.ts +20 -0
  14. package/app/assets/script/tasks/TaskModel.ts +32 -0
  15. package/app/assets/script/tasks/TaskRouter.ts +30 -0
  16. package/app/assets/script/tasks/TaskView.tsx +24 -0
  17. package/app/assets/style/global.css +111 -0
  18. package/app/assets/style/print.css +8 -0
  19. package/app/assets/view/CodeBlock.tsx +17 -0
  20. package/app/assets/view/examples/tasks/TaskExample.tsx +63 -0
  21. package/app/assets/view/examples/tasks/task-state.ts +46 -0
  22. package/app/assets/view/layout/SiteFooter.tsx +11 -0
  23. package/app/assets/view/layout/SiteHeader.tsx +23 -0
  24. package/app/assets/view/page-data.ts +14 -0
  25. package/app/assets/view/sections/GettingStartedSection.tsx +27 -0
  26. package/app/assets/view/sections/HeroSection.tsx +21 -0
  27. package/app/assets/view/sections/LiveExampleSection.tsx +29 -0
  28. package/app/assets/view/sections/PackageDocsSection.tsx +69 -0
  29. package/app/assets/view/sections/SourceGuideSection.tsx +28 -0
  30. package/app/index.tsx +54 -0
  31. package/cli/index.ts +117 -0
  32. package/dist/app/404.d.ts +1 -0
  33. package/dist/app/404.js +6 -0
  34. package/dist/app/404.js.map +1 -0
  35. package/dist/app/assets/script/index.d.ts +8 -2
  36. package/dist/app/assets/script/index.js +11 -1
  37. package/dist/app/assets/script/index.js.map +1 -1
  38. package/dist/app/assets/script/tasks/TaskApplication.d.ts +20 -0
  39. package/dist/app/assets/script/tasks/TaskApplication.js +63 -0
  40. package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -0
  41. package/dist/app/assets/script/tasks/TaskMediator.d.ts +15 -0
  42. package/dist/app/assets/script/tasks/TaskMediator.js +5 -0
  43. package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -0
  44. package/dist/app/assets/script/tasks/TaskModel.d.ts +13 -0
  45. package/dist/app/assets/script/tasks/TaskModel.js +33 -0
  46. package/dist/app/assets/script/tasks/TaskModel.js.map +1 -0
  47. package/dist/app/assets/script/tasks/TaskRouter.d.ts +12 -0
  48. package/dist/app/assets/script/tasks/TaskRouter.js +28 -0
  49. package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -0
  50. package/dist/app/assets/script/tasks/TaskView.d.ts +9 -0
  51. package/dist/app/assets/script/tasks/TaskView.js +24 -0
  52. package/dist/app/assets/script/tasks/TaskView.js.map +1 -0
  53. package/dist/app/assets/view/CodeBlock.d.ts +11 -0
  54. package/dist/app/assets/view/CodeBlock.js +12 -0
  55. package/dist/app/assets/view/CodeBlock.js.map +1 -0
  56. package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +11 -0
  57. package/dist/app/assets/view/examples/tasks/TaskExample.js +16 -0
  58. package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -0
  59. package/dist/app/assets/view/examples/tasks/task-state.d.ts +22 -0
  60. package/dist/app/assets/view/examples/tasks/task-state.js +30 -0
  61. package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -0
  62. package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -0
  63. package/dist/app/assets/view/layout/SiteFooter.js +6 -0
  64. package/dist/app/assets/view/layout/SiteFooter.js.map +1 -0
  65. package/dist/app/assets/view/layout/SiteHeader.d.ts +8 -0
  66. package/dist/app/assets/view/layout/SiteHeader.js +12 -0
  67. package/dist/app/assets/view/layout/SiteHeader.js.map +1 -0
  68. package/dist/app/assets/view/page-data.d.ts +16 -0
  69. package/dist/app/assets/view/page-data.js +2 -0
  70. package/dist/app/assets/view/page-data.js.map +1 -0
  71. package/dist/app/assets/view/sections/GettingStartedSection.d.ts +2 -0
  72. package/dist/app/assets/view/sections/GettingStartedSection.js +11 -0
  73. package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -0
  74. package/dist/app/assets/view/sections/HeroSection.d.ts +8 -0
  75. package/dist/app/assets/view/sections/HeroSection.js +12 -0
  76. package/dist/app/assets/view/sections/HeroSection.js.map +1 -0
  77. package/dist/app/assets/view/sections/LiveExampleSection.d.ts +6 -0
  78. package/dist/app/assets/view/sections/LiveExampleSection.js +13 -0
  79. package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -0
  80. package/dist/app/assets/view/sections/PackageDocsSection.d.ts +2 -0
  81. package/dist/app/assets/view/sections/PackageDocsSection.js +31 -0
  82. package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -0
  83. package/dist/app/assets/view/sections/SourceGuideSection.d.ts +7 -0
  84. package/dist/app/assets/view/sections/SourceGuideSection.js +11 -0
  85. package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -0
  86. package/dist/app/index.d.ts +8 -0
  87. package/dist/app/index.js +22 -0
  88. package/dist/app/index.js.map +1 -0
  89. package/dist/cli/index.d.ts +13 -0
  90. package/dist/cli/index.js +97 -0
  91. package/dist/cli/index.js.map +1 -0
  92. package/dist/scaffold/index.d.ts +64 -0
  93. package/dist/scaffold/index.js +109 -0
  94. package/dist/scaffold/index.js.map +1 -0
  95. package/dist/scripts/build.d.ts +5 -13
  96. package/dist/scripts/build.js +48 -79
  97. package/dist/scripts/build.js.map +1 -1
  98. package/package.json +77 -40
  99. package/pnpm-workspace.yaml +4 -0
  100. package/scaffold/index.ts +127 -0
  101. package/scaffold/no-jsx/README.md +47 -0
  102. package/scaffold/no-jsx/app/404.ts +10 -0
  103. package/scaffold/no-jsx/app/assets/script/index.ts +7 -0
  104. package/scaffold/no-jsx/app/assets/script/tasks/TaskView.ts +19 -0
  105. package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +39 -0
  106. package/scaffold/no-jsx/app/index.ts +58 -0
  107. package/scripts/build.ts +55 -86
  108. package/server/handler.ts +75 -0
  109. package/template-test/site.test.js +233 -0
  110. package/tsconfig.site.json +6 -5
  111. package/tsconfig.site.no-jsx.json +24 -0
  112. package/app/404.hbs +0 -20
  113. package/app/assets/data/coutries.json +0 -992
  114. package/app/assets/data/states.json +0 -204
  115. package/app/assets/data/view/toolkit/colors.json +0 -17
  116. package/app/assets/data/view/toolkit/components.json +0 -39
  117. package/app/assets/data/view/toolkit/elements.json +0 -119
  118. package/app/assets/data/view/toolkit/grids.json +0 -17
  119. package/app/assets/data/view/toolkit/icons.json +0 -17
  120. package/app/assets/data/view/toolkit/index.json +0 -17
  121. package/app/assets/data/view/toolkit/type.json +0 -17
  122. package/app/assets/font/opensans/LICENSE.txt +0 -202
  123. package/app/assets/font/opensans/opensans-bold-webfont.woff2 +0 -0
  124. package/app/assets/font/opensans/opensans-bolditalic-webfont.woff2 +0 -0
  125. package/app/assets/font/opensans/opensans-extrabold-webfont.woff2 +0 -0
  126. package/app/assets/font/opensans/opensans-extrabolditalic-webfont.woff2 +0 -0
  127. package/app/assets/font/opensans/opensans-italic-webfont.woff2 +0 -0
  128. package/app/assets/font/opensans/opensans-light-webfont.woff2 +0 -0
  129. package/app/assets/font/opensans/opensans-lightitalic-webfont.woff2 +0 -0
  130. package/app/assets/font/opensans/opensans-regular-webfont.woff2 +0 -0
  131. package/app/assets/font/opensans/opensans-semibold-webfont.woff2 +0 -0
  132. package/app/assets/font/opensans/opensans-semibolditalic-webfont.woff2 +0 -0
  133. package/app/assets/font/opensans/stylesheet.css +0 -120
  134. package/app/assets/image/apple-touch-icon.png +0 -0
  135. package/app/assets/image/demo/fallback.avif +0 -0
  136. package/app/assets/image/demo/fallback.jpg +0 -0
  137. package/app/assets/image/demo/fallback.webp +0 -0
  138. package/app/assets/image/demo/large.avif +0 -0
  139. package/app/assets/image/demo/large.webp +0 -0
  140. package/app/assets/image/demo/small.avif +0 -0
  141. package/app/assets/image/demo/small.webp +0 -0
  142. package/app/assets/image/favicon.png +0 -0
  143. package/app/assets/image/graphic-css3.svg +0 -30
  144. package/app/assets/image/graphic-html5.svg +0 -29
  145. package/app/assets/markup/element/a.hbs +0 -29
  146. package/app/assets/markup/element/button.hbs +0 -30
  147. package/app/assets/markup/element/div.hbs +0 -11
  148. package/app/assets/markup/element/head.hbs +0 -78
  149. package/app/assets/markup/element/iframe.hbs +0 -20
  150. package/app/assets/markup/element/img.hbs +0 -30
  151. package/app/assets/markup/element/input.hbs +0 -43
  152. package/app/assets/markup/element/label.hbs +0 -14
  153. package/app/assets/markup/element/legend.hbs +0 -5
  154. package/app/assets/markup/element/meta.hbs +0 -11
  155. package/app/assets/markup/element/nav.hbs +0 -13
  156. package/app/assets/markup/element/p.hbs +0 -8
  157. package/app/assets/markup/element/picture.hbs +0 -58
  158. package/app/assets/markup/element/script.hbs +0 -3
  159. package/app/assets/markup/element/select.hbs +0 -35
  160. package/app/assets/markup/element/span.hbs +0 -8
  161. package/app/assets/markup/element/svg.hbs +0 -11
  162. package/app/assets/markup/element/table.hbs +0 -50
  163. package/app/assets/markup/element/textarea.hbs +0 -24
  164. package/app/assets/markup/element/ul.hbs +0 -13
  165. package/app/assets/markup/modal/modal-example-1.handlebars +0 -8
  166. package/app/assets/markup/toolkit/accessibility-skip-to-element-1.handlebars +0 -1
  167. package/app/assets/markup/toolkit/accessibility-speaker-1.handlebars +0 -1
  168. package/app/assets/markup/toolkit/anchor-1.handlebars +0 -1
  169. package/app/assets/markup/toolkit/button-1.handlebars +0 -1
  170. package/app/assets/markup/toolkit/footer-1.handlebars +0 -3
  171. package/app/assets/markup/toolkit/header-1.handlebars +0 -1
  172. package/app/assets/markup/toolkit/heading-1.handlebars +0 -1
  173. package/app/assets/markup/toolkit/heading-2.handlebars +0 -1
  174. package/app/assets/markup/toolkit/heading-3.handlebars +0 -1
  175. package/app/assets/markup/toolkit/heading-4.handlebars +0 -1
  176. package/app/assets/markup/toolkit/image-1.handlebars +0 -1
  177. package/app/assets/markup/toolkit/input-checkbox-1.handlebars +0 -1
  178. package/app/assets/markup/toolkit/input-error-1.handlebars +0 -8
  179. package/app/assets/markup/toolkit/input-radio-1.handlebars +0 -1
  180. package/app/assets/markup/toolkit/input-text-1.handlebars +0 -1
  181. package/app/assets/markup/toolkit/label-1.handlebars +0 -1
  182. package/app/assets/markup/toolkit/legend-1.handlebars +0 -1
  183. package/app/assets/markup/toolkit/list-1.handlebars +0 -1
  184. package/app/assets/markup/toolkit/modal-1.handlebars +0 -4
  185. package/app/assets/markup/toolkit/nav-1.handlebars +0 -1
  186. package/app/assets/markup/toolkit/picture-1.handlebars +0 -1
  187. package/app/assets/markup/toolkit/select-1.handlebars +0 -1
  188. package/app/assets/markup/toolkit/table-1.handlebars +0 -1
  189. package/app/assets/markup/toolkit/text-1.handlebars +0 -1
  190. package/app/assets/markup/toolkit/textarea-1.handlebars +0 -1
  191. package/app/assets/markup/toolkit/youtube-player-1.handlebars +0 -1
  192. package/app/assets/script/api/facebook.ts +0 -41
  193. package/app/assets/script/api/youtube.ts +0 -22
  194. package/app/assets/script/index.ts +0 -2
  195. package/app/assets/script/mediator/global.ts +0 -4
  196. package/app/assets/script/model/singletons/countries.ts +0 -5
  197. package/app/assets/script/model/singletons/global.ts +0 -5
  198. package/app/assets/script/model/singletons/states.ts +0 -5
  199. package/app/assets/script/model/user.ts +0 -4
  200. package/app/assets/script/toolkit.ts +0 -11
  201. package/app/assets/script/types.d.ts +0 -16
  202. package/app/assets/script/utility/ajax.ts +0 -15
  203. package/app/assets/script/utility/form.ts +0 -119
  204. package/app/assets/script/utility/regex.ts +0 -26
  205. package/app/assets/script/utility/share.ts +0 -77
  206. package/app/assets/script/utility/string.ts +0 -148
  207. package/app/assets/script/view/default.tsx +0 -14
  208. package/app/assets/script/view/toolkit/youtube-player-1.ts +0 -31
  209. package/app/assets/style/base/_toolkit.scss +0 -39
  210. package/app/assets/style/base/colors/_colors.scss +0 -18
  211. package/app/assets/style/base/components/_modal-1.scss +0 -80
  212. package/app/assets/style/base/components/_sticky-1.scss +0 -19
  213. package/app/assets/style/base/components/_tabpanel-1.scss +0 -115
  214. package/app/assets/style/base/components/_tabpanel-2.scss +0 -64
  215. package/app/assets/style/base/components/_tabpanel-base-1.scss +0 -13
  216. package/app/assets/style/base/components/_youtube-player-1.scss +0 -16
  217. package/app/assets/style/base/elements/_anchors.scss +0 -27
  218. package/app/assets/style/base/elements/_buttons.scss +0 -25
  219. package/app/assets/style/base/elements/_fieldsets.scss +0 -9
  220. package/app/assets/style/base/elements/_images.scss +0 -8
  221. package/app/assets/style/base/elements/_inputs.scss +0 -78
  222. package/app/assets/style/base/elements/_labels.scss +0 -16
  223. package/app/assets/style/base/elements/_landmarks.scss +0 -3
  224. package/app/assets/style/base/elements/_legends.scss +0 -11
  225. package/app/assets/style/base/elements/_lists.scss +0 -15
  226. package/app/assets/style/base/elements/_pictures.scss +0 -4
  227. package/app/assets/style/base/elements/_tables.scss +0 -39
  228. package/app/assets/style/base/layouts/_staggered-1.scss +0 -29
  229. package/app/assets/style/base/type/_fonts.scss +0 -125
  230. package/app/assets/style/base/type/_headings.scss +0 -36
  231. package/app/assets/style/base/type/_text.scss +0 -9
  232. package/app/assets/style/base/utilities/_accessibility.scss +0 -55
  233. package/app/assets/style/base/utilities/_functions.scss +0 -98
  234. package/app/assets/style/base/utilities/_scroll.scss +0 -11
  235. package/app/assets/style/base/utilities/_type.scss +0 -13
  236. package/app/assets/style/global.scss +0 -123
  237. package/app/assets/style/print.scss +0 -6
  238. package/app/assets/style/toolkit.scss +0 -387
  239. package/app/index.hbs +0 -21
  240. package/app/robots.txt +0 -1
  241. package/app/toolkit/colors.hbs +0 -102
  242. package/app/toolkit/components.hbs +0 -261
  243. package/app/toolkit/elements.hbs +0 -497
  244. package/app/toolkit/grids.hbs +0 -413
  245. package/app/toolkit/icons.hbs +0 -87
  246. package/app/toolkit/index.hbs +0 -144
  247. package/app/toolkit/type.hbs +0 -120
  248. package/dist/app/assets/data/coutries.json +0 -992
  249. package/dist/app/assets/data/states.json +0 -204
  250. package/dist/app/assets/script/api/facebook.d.ts +0 -8
  251. package/dist/app/assets/script/api/facebook.js +0 -42
  252. package/dist/app/assets/script/api/facebook.js.map +0 -1
  253. package/dist/app/assets/script/api/youtube.d.ts +0 -8
  254. package/dist/app/assets/script/api/youtube.js +0 -22
  255. package/dist/app/assets/script/api/youtube.js.map +0 -1
  256. package/dist/app/assets/script/mediator/global.d.ts +0 -4
  257. package/dist/app/assets/script/mediator/global.js +0 -4
  258. package/dist/app/assets/script/mediator/global.js.map +0 -1
  259. package/dist/app/assets/script/model/singletons/countries.d.ts +0 -20
  260. package/dist/app/assets/script/model/singletons/countries.js +0 -5
  261. package/dist/app/assets/script/model/singletons/countries.js.map +0 -1
  262. package/dist/app/assets/script/model/singletons/global.d.ts +0 -20
  263. package/dist/app/assets/script/model/singletons/global.js +0 -5
  264. package/dist/app/assets/script/model/singletons/global.js.map +0 -1
  265. package/dist/app/assets/script/model/singletons/states.d.ts +0 -20
  266. package/dist/app/assets/script/model/singletons/states.js +0 -5
  267. package/dist/app/assets/script/model/singletons/states.js.map +0 -1
  268. package/dist/app/assets/script/model/user.d.ts +0 -4
  269. package/dist/app/assets/script/model/user.js +0 -6
  270. package/dist/app/assets/script/model/user.js.map +0 -1
  271. package/dist/app/assets/script/toolkit.d.ts +0 -1
  272. package/dist/app/assets/script/toolkit.js +0 -7
  273. package/dist/app/assets/script/toolkit.js.map +0 -1
  274. package/dist/app/assets/script/utility/ajax.d.ts +0 -8
  275. package/dist/app/assets/script/utility/ajax.js +0 -17
  276. package/dist/app/assets/script/utility/ajax.js.map +0 -1
  277. package/dist/app/assets/script/utility/form.d.ts +0 -64
  278. package/dist/app/assets/script/utility/form.js +0 -111
  279. package/dist/app/assets/script/utility/form.js.map +0 -1
  280. package/dist/app/assets/script/utility/regex.d.ts +0 -14
  281. package/dist/app/assets/script/utility/regex.js +0 -27
  282. package/dist/app/assets/script/utility/regex.js.map +0 -1
  283. package/dist/app/assets/script/utility/share.d.ts +0 -68
  284. package/dist/app/assets/script/utility/share.js +0 -81
  285. package/dist/app/assets/script/utility/share.js.map +0 -1
  286. package/dist/app/assets/script/utility/string.d.ts +0 -64
  287. package/dist/app/assets/script/utility/string.js +0 -135
  288. package/dist/app/assets/script/utility/string.js.map +0 -1
  289. package/dist/app/assets/script/view/default.d.ts +0 -11
  290. package/dist/app/assets/script/view/default.js +0 -15
  291. package/dist/app/assets/script/view/default.js.map +0 -1
  292. package/dist/app/assets/script/view/toolkit/youtube-player-1.d.ts +0 -23
  293. package/dist/app/assets/script/view/toolkit/youtube-player-1.js +0 -32
  294. package/dist/app/assets/script/view/toolkit/youtube-player-1.js.map +0 -1
  295. package/dist/generators/app/index.d.ts +0 -18
  296. package/dist/generators/app/index.js +0 -59
  297. package/dist/generators/app/index.js.map +0 -1
  298. package/generators/app/index.js +0 -2
  299. package/generators/app/index.ts +0 -61
package/scripts/build.ts CHANGED
@@ -1,20 +1,16 @@
1
1
  /** Values injected into page templates and the generated config file. */
2
- export interface BuildConfig {cdn: string; production: boolean; service: string; version: string; www: string}
2
+ export interface BuildConfig {cdn: string; production: boolean; siteUrl: string; version: string; www: string}
3
+ import {execFile} from 'node:child_process';
3
4
  import fs from 'node:fs/promises';
4
5
  import path from 'node:path';
5
- import {fileURLToPath} from 'node:url';
6
+ import {promisify} from 'node:util';
7
+ import {fileURLToPath, pathToFileURL} from 'node:url';
6
8
  import {build as buildJavaScript} from 'esbuild';
7
- import Handlebars from 'handlebars';
8
- import * as sass from 'sass';
9
9
 
10
+ const execFileAsync = promisify(execFile);
10
11
  const defaultProjectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
11
12
 
12
- /**
13
- * Read deployment flags and reject version strings that contain path separators.
14
- * @param argumentsList - Command-line arguments excluding the Node executable and script.
15
- * @returns Deployment configuration with defaults for omitted flags.
16
- * @throws When input does not satisfy the documented contract.
17
- */
13
+ /** Read deployment flags and reject unsafe or invalid deployment values. */
18
14
  export function parseArguments(argumentsList: string[]): BuildConfig {
19
15
  const values = Object.fromEntries(argumentsList.map((argument) => {
20
16
  const [key, ...value] = argument.replace(/^--/, '').split('=');
@@ -24,20 +20,25 @@ export function parseArguments(argumentsList: string[]): BuildConfig {
24
20
  if (!/^[A-Za-z0-9._-]+$/.test(version)) {
25
21
  throw new Error('Version may contain only letters, numbers, dots, underscores, and hyphens');
26
22
  }
23
+ const siteUrl = (values['site-url'] || 'http://localhost:8080').replace(/\/$/, '');
24
+ if (!URL.canParse(siteUrl) || (values.production === 'true' && !siteUrl.startsWith('https://'))) {
25
+ throw new Error('Production builds require an absolute HTTPS --site-url');
26
+ }
27
27
  return {
28
28
  cdn: values.cdn || '/',
29
29
  production: values.production === 'true',
30
- service: values.service || '/service-endpoint',
30
+ siteUrl,
31
31
  version,
32
32
  www: values.www || '/'
33
33
  };
34
34
  }
35
35
 
36
- /**
37
- * Recursively collect files below a directory, propagating filesystem failures.
38
- * @param directory - Directory to traverse recursively.
39
- * @returns A promise resolving to all descendant file paths.
40
- */
36
+ /** Return the local Tailwind executable name for the current platform. */
37
+ export function tailwindExecutable(platform: NodeJS.Platform = process.platform) {
38
+ return platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss';
39
+ }
40
+
41
+ /** Recursively collect files below a directory, propagating filesystem failures. */
41
42
  async function filesUnder(directory: string): Promise<string[]> {
42
43
  const entries = await fs.readdir(directory, {withFileTypes: true});
43
44
  const files = await Promise.all(entries.map(async (entry) => {
@@ -47,85 +48,59 @@ async function filesUnder(directory: string): Promise<string[]> {
47
48
  return files.flat();
48
49
  }
49
50
 
50
- /**
51
- * Register partials and render pages using global, page-specific, and deployment data.
52
- * @param config - Validated deployment or database configuration.
53
- * @param outputRoot - Destination for rendered pages.
54
- * @param projectRoot - Project containing app sources and installed dependencies.
55
- * @returns A promise resolving after all pages have been written.
56
- */
57
- async function renderMarkup(config: BuildConfig, outputRoot: string, projectRoot: string) {
58
- const markupRoot = path.join(projectRoot, 'app/assets/markup');
59
- for (const partialPath of await filesUnder(markupRoot)) {
60
- if (/\.(hbs|handlebars)$/.test(partialPath)) {
61
- const name = path.relative(markupRoot, partialPath).replace(/\\/g, '/').replace(/\.(hbs|handlebars)$/, '');
62
- Handlebars.registerPartial(name, await fs.readFile(partialPath, 'utf8'));
63
- }
64
- }
51
+ interface PageModule {
52
+ default(data: Record<string, unknown>): {toString(): string} | string;
53
+ }
65
54
 
55
+ /** Render compiled TypeScript or TSX pages using global, page-specific, and deployment data. */
56
+ async function renderMarkup(config: BuildConfig, outputRoot: string, projectRoot: string) {
66
57
  const globalData = JSON.parse(await fs.readFile(
67
58
  path.join(projectRoot, 'app/assets/data/view/global.json'), 'utf8'
68
- ));
69
- const pages = (await filesUnder(path.join(projectRoot, 'app')))
70
- .filter((file) => file.endsWith('.hbs') && !file.includes(`${path.sep}assets${path.sep}`));
59
+ )) as Record<string, unknown>;
60
+ const appRoot = path.join(projectRoot, 'app');
61
+ const pages = (await filesUnder(appRoot))
62
+ .filter((file) => /\.tsx?$/.test(file) && !file.includes(`${path.sep}assets${path.sep}`));
71
63
 
72
64
  await Promise.all(pages.map(async (page) => {
73
- const relative = path.relative(path.join(projectRoot, 'app'), page);
65
+ const relative = path.relative(appRoot, page);
74
66
  const pageDataPath = path.join(
75
- projectRoot, 'app/assets/data/view', relative.replace(/\.hbs$/, '.json')
67
+ projectRoot, 'app/assets/data/view', relative.replace(/\.tsx?$/, '.json')
76
68
  );
77
- let pageData = {};
69
+ let pageData: Record<string, unknown> = {};
78
70
  try {
79
- pageData = JSON.parse(await fs.readFile(pageDataPath, 'utf8'));
71
+ pageData = JSON.parse(await fs.readFile(pageDataPath, 'utf8')) as Record<string, unknown>;
80
72
  } catch (error) {
81
73
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
82
74
  throw error;
83
75
  }
84
76
  }
85
- const template = Handlebars.compile(await fs.readFile(page, 'utf8'));
86
- const destination = path.join(outputRoot, relative.replace(/\.hbs$/, '.html'));
77
+ const compiledPage = path.join(projectRoot, 'dist/app', relative.replace(/\.tsx?$/, '.js'));
78
+ const module = await import(pathToFileURL(compiledPage).href) as PageModule;
79
+ if (typeof module.default !== 'function') {
80
+ throw new TypeError(`Page module ${relative} must export a default render function`);
81
+ }
82
+ const markup = module.default({...globalData, ...pageData, ...config});
83
+ const destination = path.join(outputRoot, relative.replace(/\.tsx?$/, '.html'));
87
84
  await fs.mkdir(path.dirname(destination), {recursive: true});
88
- await fs.writeFile(destination, template({...globalData, ...pageData, ...config}));
85
+ await fs.writeFile(destination, `<!DOCTYPE html>\n${String(markup)}\n`);
89
86
  }));
90
87
  }
91
88
 
92
- /**
93
- * Compile project Sass and include the full published Bootstrap stylesheet.
94
- * @param outputAssets - Destination for compiled asset directories.
95
- * @param production - Enable compressed output and omit development source maps.
96
- * @param projectRoot - Project containing app sources and installed dependencies.
97
- * @returns A promise resolving after all stylesheets have been written.
98
- */
89
+ /** Compile Tailwind and project CSS into the deploy directory. */
99
90
  async function compileStyles(outputAssets: string, production: boolean, projectRoot: string) {
100
91
  const styleRoot = path.join(projectRoot, 'app/assets/style');
101
- await Promise.all(['global', 'print', 'toolkit'].map(async (name) => {
102
- const result = sass.compile(path.join(styleRoot, `${name}.scss`), {
103
- loadPaths: [path.join(projectRoot, 'node_modules')],
104
- style: production ? 'compressed' : 'expanded'
105
- });
106
- const destination = path.join(outputAssets, 'style', `${name}.css`);
107
- await fs.mkdir(path.dirname(destination), {recursive: true});
108
- // Bootstrap publishes compiled CSS, so consuming it avoids recompiling third-party
109
- // Sass and keeps dependency deprecation warnings out of project builds.
110
- const bootstrapSource = name === 'global'
111
- ? (await fs.readFile(
112
- path.join(projectRoot, 'node_modules/bootstrap/dist/css/bootstrap.css'), 'utf8'
113
- )).replace(/\/\*# sourceMappingURL=bootstrap\.css\.map \*\//, '')
114
- : '';
115
- const bootstrapCss = production && bootstrapSource
116
- ? sass.compileString(bootstrapSource, {style: 'compressed', syntax: 'css'}).css
117
- : bootstrapSource;
118
- await fs.writeFile(destination, `${bootstrapCss}${bootstrapCss ? '\n' : ''}${result.css}`);
119
- }));
92
+ await fs.mkdir(path.join(outputAssets, 'style'), {recursive: true});
93
+ const destination = path.join(outputAssets, 'style/global.css');
94
+ const argumentsList = [
95
+ '-i', path.join(styleRoot, 'global.css'),
96
+ '-o', destination
97
+ ];
98
+ if (production) {argumentsList.push('--minify');}
99
+ await execFileAsync(tailwindExecutable(), argumentsList, {cwd: projectRoot});
100
+ await fs.copyFile(path.join(styleRoot, 'print.css'), path.join(outputAssets, 'style/print.css'));
120
101
  }
121
102
 
122
- /**
123
- * Bundle TypeScript entry points for browsers, excluding declaration-only files.
124
- * @param outputAssets - Destination for compiled asset directories.
125
- * @param production - Enable compressed output and omit development source maps.
126
- * @param projectRoot - Project containing app sources and installed dependencies.
127
- * @returns A promise resolving after all script bundles have been written.
128
- */
103
+ /** Bundle TypeScript entry points for browsers, excluding declaration-only files. */
129
104
  async function compileScripts(outputAssets: string, production: boolean, projectRoot: string) {
130
105
  const scriptRoot = path.join(projectRoot, 'app/assets/script');
131
106
  const entries = (await fs.readdir(scriptRoot, {withFileTypes: true}))
@@ -146,28 +121,22 @@ async function compileScripts(outputAssets: string, production: boolean, project
146
121
  await fs.writeFile(path.join(outputAssets, 'script/global.compiled.js'), '');
147
122
  }
148
123
 
149
- /**
150
- * Replace the project deployment directory with compiled pages, scripts, styles, and static assets.
151
- * @param config - Validated deployment or database configuration.
152
- * @param projectRoot - Project containing app sources and installed dependencies.
153
- * @returns A promise resolving after the deployment directory has been rebuilt.
154
- */
124
+ /** Replace the deployment directory with compiled pages, scripts, styles, and static assets. */
155
125
  export async function build(config = parseArguments(process.argv.slice(2)), projectRoot = defaultProjectRoot) {
156
126
  const outputRoot = path.join(projectRoot, '_deploy');
157
127
  const outputAssets = path.join(outputRoot, 'release', config.version, 'assets');
158
128
  await fs.rm(outputRoot, {recursive: true, force: true});
159
129
  await fs.mkdir(outputAssets, {recursive: true});
160
130
 
161
- await Promise.all([
162
- fs.cp(path.join(projectRoot, 'app/assets/data'), path.join(outputAssets, 'data'), {recursive: true}),
163
- fs.cp(path.join(projectRoot, 'app/assets/font'), path.join(outputAssets, 'font'), {recursive: true}),
164
- fs.cp(path.join(projectRoot, 'app/assets/image'), path.join(outputAssets, 'image'), {recursive: true})
165
- ]);
131
+ await fs.cp(path.join(projectRoot, 'app/assets/data'), path.join(outputAssets, 'data'), {recursive: true});
166
132
  await fs.writeFile(path.join(outputAssets, 'data/config.json'), JSON.stringify(config));
167
133
  await renderMarkup(config, outputRoot, projectRoot);
168
134
  await compileStyles(outputAssets, config.production, projectRoot);
169
135
  await compileScripts(outputAssets, config.production, projectRoot);
170
- await fs.copyFile(path.join(projectRoot, 'app/robots.txt'), path.join(outputRoot, 'robots.txt'));
136
+ const robots = `User-agent: *\nAllow: /\nSitemap: ${config.siteUrl}/sitemap.xml\n`;
137
+ await fs.writeFile(path.join(outputRoot, 'robots.txt'), robots);
138
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${config.siteUrl}/</loc></url></urlset>\n`;
139
+ await fs.writeFile(path.join(outputRoot, 'sitemap.xml'), sitemap);
171
140
  }
172
141
 
173
142
  if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
@@ -0,0 +1,75 @@
1
+ import Mediator from 'white-label-mediator';
2
+ import {Model} from 'white-label-model';
3
+ import Router from 'white-label-router';
4
+ import View from 'white-label-view/server';
5
+
6
+ type ServerState = {
7
+ status: number;
8
+ title: string;
9
+ message: string;
10
+ };
11
+
12
+ function escapeHtml(value: string) {
13
+ return value
14
+ .replaceAll('&', '&amp;')
15
+ .replaceAll('<', '&lt;')
16
+ .replaceAll('>', '&gt;')
17
+ .replaceAll('"', '&quot;')
18
+ .replaceAll("'", '&#39;');
19
+ }
20
+
21
+ /**
22
+ * Provider-neutral serverless example using the Web Request/Response contract.
23
+ *
24
+ * Keep mutable White Label instances request-scoped. Cloud adapters should only
25
+ * translate their provider event into a Request and translate the Response back.
26
+ */
27
+ export async function handleRequest(request: Request): Promise<Response> {
28
+ const url = new URL(request.url);
29
+ const mediator = new Mediator();
30
+ const model = new Model({
31
+ status: 404,
32
+ title: 'Not found',
33
+ message: `No route for ${url.pathname}.`
34
+ });
35
+ const router = new Router();
36
+ const view = new View({
37
+ model,
38
+ template(data) {
39
+ const state = data as ServerState;
40
+ return `<main><h1>${escapeHtml(state.title)}</h1><p>${escapeHtml(state.message)}</p></main>`;
41
+ }
42
+ });
43
+
44
+ router.mediator = mediator;
45
+ router.routes = {
46
+ '/health': () => model.update({
47
+ status: 200,
48
+ title: 'Healthy',
49
+ message: 'Serverless request handled.'
50
+ }),
51
+ '/hello': (_scope, location) => {
52
+ const name = String(location.data.query.name ?? 'world');
53
+ return model.update({
54
+ status: 200,
55
+ title: 'Hello',
56
+ message: `Hello ${name}.`
57
+ });
58
+ },
59
+ defaultRoute: () => true
60
+ };
61
+
62
+ try {
63
+ router.navigate(`${url.pathname}${url.search}`);
64
+ view.initialize();
65
+ return new Response(`<!doctype html>${view.toString()}`, {
66
+ status: model.get().status,
67
+ headers: {'content-type': 'text/html; charset=utf-8'}
68
+ });
69
+ } finally {
70
+ view.destroy();
71
+ router.destroy();
72
+ mediator.destroy();
73
+ model.destroy();
74
+ }
75
+ }
@@ -0,0 +1,233 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {execFileSync} from 'node:child_process';
4
+ import {readFile} from 'node:fs/promises';
5
+ import axe from 'axe-core';
6
+ import {build as buildBundle} from 'esbuild';
7
+ import {HtmlValidate} from 'html-validate';
8
+ import {JSDOM} from 'jsdom';
9
+
10
+ const applicationMarkup = `<!doctype html><html><body><main><div data-task-example><section data-task-app><form data-task-form><input name="task"></form><nav><a href="/?tasks=all#example" data-task-filter="all" data-pushstate aria-current="page"></a><a href="/?tasks=active#example" data-task-filter="active" data-pushstate aria-current="false"></a><a href="/?tasks=completed#example" data-task-filter="completed" data-pushstate aria-current="false"></a></nav><ul><li><input type="checkbox" data-task-toggle data-task-id="1"></li><li><input type="checkbox" data-task-toggle data-task-id="2"></li></ul></section><p data-task-status role="status" aria-live="polite" aria-atomic="true"></p></div></main></body></html>`;
11
+
12
+ /** Install one JSDOM window as the browser globals consumed by the packages. */
13
+ function installBrowser(markup = applicationMarkup) {
14
+ const dom = new JSDOM(markup, {url: 'https://example.com/', pretendToBeVisual: true, runScripts: 'dangerously'});
15
+ globalThis.window = dom.window;
16
+ globalThis.document = dom.window.document;
17
+ globalThis.DOMParser = dom.window.DOMParser;
18
+ globalThis.Element = dom.window.Element;
19
+ globalThis.Node = dom.window.Node;
20
+ return dom;
21
+ }
22
+
23
+ async function withoutBrowserGlobals(callback) {
24
+ const previous = {
25
+ window: globalThis.window,
26
+ document: globalThis.document,
27
+ DOMParser: globalThis.DOMParser,
28
+ Element: globalThis.Element,
29
+ Node: globalThis.Node
30
+ };
31
+ delete globalThis.window;
32
+ delete globalThis.document;
33
+ delete globalThis.DOMParser;
34
+ delete globalThis.Element;
35
+ delete globalThis.Node;
36
+ try {
37
+ return await callback();
38
+ } finally {
39
+ Object.assign(globalThis, previous);
40
+ }
41
+ }
42
+
43
+ function relativeLuminance(hex) {
44
+ const channels = hex.slice(1).match(/.{2}/g).map(value => Number.parseInt(value, 16) / 255);
45
+ const [red, green, blue] = channels.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
46
+ return .2126 * red + .7152 * green + .0722 * blue;
47
+ }
48
+
49
+ function contrastRatio(foreground, background) {
50
+ const first = relativeLuminance(foreground);
51
+ const second = relativeLuminance(background);
52
+ return (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
53
+ }
54
+
55
+ async function assertAccessible(name, html) {
56
+ const validator = new HtmlValidate({extends: ['html-validate:recommended']});
57
+ const validation = await validator.validateString(html);
58
+ assert.equal(validation.valid, true, `${name}: ${validation.results.flatMap(result => result.messages).map(message => message.message).join('\n')}`);
59
+ const dom = new JSDOM(html, {runScripts: 'dangerously', url: 'https://example.com/'});
60
+ dom.window.eval(axe.source);
61
+ const results = await dom.window.axe.run(dom.window.document, {runOnly: {type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']}});
62
+ dom.window.close();
63
+ assert.equal(results.violations.length, 0, `${name}: ${results.violations.map(violation => violation.id).join(', ')}`);
64
+ }
65
+
66
+ const bootDom = installBrowser();
67
+ const {initializeTaskApplication, normalizeTaskFilter} = await import('../dist/app/assets/script/index.js');
68
+
69
+ test('production output is valid, accessible static HTML', async () => {
70
+ const [html, notFound] = await Promise.all(['index.html', '404.html'].map(file => readFile(`_deploy/${file}`, 'utf8')));
71
+ await assertAccessible('index', html);
72
+ await assertAccessible('404', notFound);
73
+
74
+ const dom = new JSDOM(html);
75
+ const root = dom.window.document;
76
+ assert.equal(root.querySelector('.skip-link').getAttribute('href'), '#main');
77
+ assert.equal(root.querySelector('main#main').getAttribute('tabindex'), '-1');
78
+ assert.equal(root.querySelector('[data-task-status]').getAttribute('role'), 'status');
79
+ assert.equal(root.querySelector('[data-task-status]').getAttribute('aria-live'), 'polite');
80
+ assert.equal(root.querySelector('[data-task-status]').getAttribute('aria-atomic'), 'true');
81
+ assert.equal(root.querySelector('[data-task-app] [data-task-status]'), null);
82
+ assert.equal(root.querySelector('[data-task-filter][aria-current="page"]').dataset.taskFilter, 'all');
83
+ assert.ok([...root.querySelectorAll('[data-task-filter]')].every(link => link instanceof dom.window.HTMLAnchorElement && link.hasAttribute('href')));
84
+ assert.ok(root.querySelector('[data-task-form] label[for="task-title"]'));
85
+
86
+ const codeBlocks = [...root.querySelectorAll('pre > code')];
87
+ assert.ok(codeBlocks.length > 0);
88
+ assert.ok(codeBlocks.every(code => code.firstChild === code.firstElementChild && code.lastChild === code.lastElementChild));
89
+ dom.window.close();
90
+ });
91
+
92
+ test('production output is crawlable and supplies complete SEO signals', async () => {
93
+ const [html, robots, sitemap] = await Promise.all(['index.html', 'robots.txt', 'sitemap.xml'].map(file => readFile(`_deploy/${file}`, 'utf8')));
94
+ const dom = new JSDOM(html);
95
+ const root = dom.window.document;
96
+ assert.equal(root.querySelector('link[rel="canonical"]').href, 'https://example.com/');
97
+ assert.match(root.querySelector('meta[name="robots"]').content, /^index,follow/);
98
+ assert.ok(root.querySelector('title').textContent.length > 20);
99
+ assert.ok(root.querySelector('meta[name="description"]').content.length > 50);
100
+ assert.equal(JSON.parse(root.querySelector('script[type="application/ld+json"]').textContent)['@type'], 'WebPage');
101
+ assert.ok([...root.querySelectorAll('a')].every(link => link.hasAttribute('href')));
102
+ assert.match(robots, /User-agent: \*\nAllow: \//);
103
+ assert.match(robots, /Sitemap: https:\/\/example\.com\/sitemap\.xml/);
104
+ assert.match(sitemap, /<loc>https:\/\/example\.com\/<\/loc>/);
105
+ assert.doesNotMatch(html, /service-endpoint|white-label-service|<%|{{/);
106
+ dom.window.close();
107
+ });
108
+
109
+ test('all client packages cooperate through the task application without losing focus', async () => {
110
+ const dom = installBrowser();
111
+ const application = initializeTaskApplication(dom.window.document);
112
+ const input = dom.window.document.querySelector('[name="task"]');
113
+ input.value = 'Verify generated app';
114
+ dom.window.document.querySelector('[data-task-form]').dispatchEvent(new dom.window.Event('submit', {bubbles: true, cancelable: true}));
115
+ assert.equal(application.model.get().tasks.length, 3);
116
+ assert.equal(dom.window.document.activeElement.getAttribute('name'), 'task');
117
+ assert.match(dom.window.document.querySelector('[data-task-status]').textContent, /Showing 3 tasks for the all filter/);
118
+
119
+ const toggle = dom.window.document.querySelector('[data-task-id="2"]');
120
+ toggle.focus();
121
+ toggle.dispatchEvent(new dom.window.Event('change', {bubbles: true}));
122
+ assert.equal(application.model.get().tasks[1].complete, true);
123
+ assert.equal(dom.window.document.activeElement.dataset.taskId, '2');
124
+
125
+ const activeFilter = dom.window.document.querySelector('[data-task-filter="active"]');
126
+ activeFilter.focus();
127
+ activeFilter.dispatchEvent(new dom.window.MouseEvent('click', {bubbles: true, button: 0}));
128
+ await Promise.resolve();
129
+ assert.equal(application.model.get().filter, 'active');
130
+ assert.match(dom.window.document.querySelector('[data-task-status]').textContent, /Showing 1 task/);
131
+ assert.equal(dom.window.location.search, '?tasks=active');
132
+ assert.equal(dom.window.document.activeElement.dataset.taskFilter, 'active');
133
+
134
+ const completedFilter = dom.window.document.querySelector('[data-task-filter="completed"]');
135
+ completedFilter.focus();
136
+ completedFilter.dispatchEvent(new dom.window.MouseEvent('click', {bubbles: true, button: 0}));
137
+ await Promise.resolve();
138
+ assert.equal(application.model.get().filter, 'completed');
139
+ assert.match(dom.window.document.querySelector('[data-task-status]').textContent, /Showing 2 tasks/);
140
+ assert.equal(dom.window.document.activeElement.dataset.taskFilter, 'completed');
141
+
142
+ assert.equal(normalizeTaskFilter('nope'), 'all');
143
+ application.destroy();
144
+ assert.equal(application.mediator.listenerCount('task:add'), 0);
145
+ });
146
+
147
+ test('serverless handler keeps warm and concurrent requests isolated', async () => {
148
+ await withoutBrowserGlobals(async () => {
149
+ const {handleRequest} = await import('../dist/server/handler.js');
150
+ const first = await handleRequest(new Request('https://example.com/hello?name=Ada'));
151
+ const second = await handleRequest(new Request('https://example.com/hello?name=Grace'));
152
+ assert.equal(first.status, 200);
153
+ assert.equal(second.status, 200);
154
+ assert.match(await first.text(), /Hello Ada\./);
155
+ assert.match(await second.text(), /Hello Grace\./);
156
+
157
+ const [alan, katherine] = await Promise.all([
158
+ handleRequest(new Request('https://example.com/hello?name=Alan')),
159
+ handleRequest(new Request('https://example.com/hello?name=Katherine'))
160
+ ]);
161
+ const [alanHtml, katherineHtml] = await Promise.all([alan.text(), katherine.text()]);
162
+ assert.match(alanHtml, /Hello Alan\./);
163
+ assert.doesNotMatch(alanHtml, /Katherine/);
164
+ assert.match(katherineHtml, /Hello Katherine\./);
165
+ assert.doesNotMatch(katherineHtml, /Alan/);
166
+
167
+ const escaped = await handleRequest(new Request('https://example.com/hello?name=%3Cstrong%3EAda%3C%2Fstrong%3E'));
168
+ assert.match(await escaped.text(), /Hello &lt;strong&gt;Ada&lt;\/strong&gt;\./);
169
+ const missing = await handleRequest(new Request('https://example.com/missing'));
170
+ assert.equal(missing.status, 404);
171
+ });
172
+ });
173
+
174
+ test('serverless composition bundles for Web-standard runtimes within the size budget', async () => {
175
+ const result = await buildBundle({
176
+ stdin: {
177
+ contents: `
178
+ import Mediator from 'white-label-mediator';
179
+ import {Model} from 'white-label-model';
180
+ import Router from 'white-label-router';
181
+ import View from 'white-label-view/server';
182
+ const mediator = new Mediator();
183
+ const model = new Model({ok: true});
184
+ const router = new Router();
185
+ const view = new View({model, template: data => '<p>' + String(data.ok) + '</p>'});
186
+ router.mediator = mediator;
187
+ export {mediator, model, router, view};
188
+ `,
189
+ loader: 'ts',
190
+ resolveDir: process.cwd(),
191
+ sourcefile: 'serverless-portability-smoke.ts'
192
+ },
193
+ bundle: true,
194
+ format: 'esm',
195
+ logLevel: 'silent',
196
+ minify: true,
197
+ platform: 'browser',
198
+ target: 'es2022',
199
+ treeShaking: true,
200
+ write: false
201
+ });
202
+ const bundle = result.outputFiles[0].text;
203
+ const bytes = Buffer.byteLength(bundle);
204
+ assert.ok(bytes <= 100_000, `serverless runtime bundle grew to ${bytes} bytes (100000 byte budget)`);
205
+ assert.doesNotMatch(bundle, /(?:from|require\()["']node:/);
206
+ });
207
+
208
+ test('fresh serverless handler import stays within the cold-start budget', () => {
209
+ const script = "const start=performance.now(); await import('./dist/server/handler.js'); console.log(performance.now()-start);";
210
+ const elapsed = Number(execFileSync(process.execPath, ['--input-type=module', '-e', script], {encoding: 'utf8'}).trim());
211
+ assert.ok(Number.isFinite(elapsed));
212
+ assert.ok(elapsed <= 750, `serverless handler import took ${elapsed.toFixed(1)}ms (750ms budget)`);
213
+ });
214
+
215
+ test('starter code syntax colors remain grayscale and meet AA contrast', async () => {
216
+ const styles = await readFile('app/assets/style/global.css', 'utf8');
217
+ const codeColors = [...styles.matchAll(/\.(?:code-block__number|code-syntax-(?:keyword|type|value|muted))\s*\{[^}]*color:\s*(#[0-9a-f]{6})/gi)]
218
+ .map(match => match[1].toLowerCase());
219
+ assert.equal(codeColors.length, 5);
220
+ for (const color of codeColors) {
221
+ assert.match(color, /^#([0-9a-f]{2})\1\1$/i);
222
+ assert.ok(contrastRatio(color, '#f4f4f4') >= 4.5, `${color} must meet 4.5:1 against the code background`);
223
+ }
224
+ });
225
+
226
+ test.after(() => {
227
+ bootDom.window.close();
228
+ delete globalThis.window;
229
+ delete globalThis.document;
230
+ delete globalThis.DOMParser;
231
+ delete globalThis.Element;
232
+ delete globalThis.Node;
233
+ });
@@ -10,17 +10,18 @@
10
10
  "inlineSources": true,
11
11
  "outDir": "dist",
12
12
  "rootDir": ".",
13
- "jsx": "react-jsx",
14
13
  "resolveJsonModule": true,
14
+ "jsx": "react-jsx",
15
+ "jsxImportSource": "white-label-view",
15
16
  "types": [
16
- "node",
17
- "react"
17
+ "node"
18
18
  ],
19
19
  "noEmitOnError": true
20
20
  },
21
21
  "include": [
22
22
  "scripts/**/*.ts",
23
- "app/assets/script/**/*.ts",
24
- "app/assets/script/**/*.tsx"
23
+ "server/**/*.ts",
24
+ "app/**/*.ts",
25
+ "app/**/*.tsx"
25
26
  ]
26
27
  }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "declaration": true,
9
+ "sourceMap": true,
10
+ "inlineSources": true,
11
+ "outDir": "dist",
12
+ "rootDir": ".",
13
+ "resolveJsonModule": true,
14
+ "types": [
15
+ "node"
16
+ ],
17
+ "noEmitOnError": true
18
+ },
19
+ "include": [
20
+ "scripts/**/*.ts",
21
+ "server/**/*.ts",
22
+ "app/**/*.ts"
23
+ ]
24
+ }
package/app/404.hbs DELETED
@@ -1,20 +0,0 @@
1
- <!DOCTYPE html>
2
- <html id="error-404" lang="en-US" dir="ltr">
3
- {{> element/head}}
4
- <body itemscope itemtype="http://schema.org/WebPage">
5
- <div id="wrapper">
6
- <main id="main" tabindex="-1" class="container">
7
- <p>Sorry, but the page you were trying to view does not exist.</p>
8
- </main>
9
- </div>
10
- {{#if scripts.global}}
11
- {{> element/script script=scripts.global}}
12
- {{/if}}
13
- {{#if scripts.section}}
14
- {{> element/script script=scripts.section}}
15
- {{/if}}
16
- {{#if scripts.page}}
17
- {{> element/script script=scripts.page}}
18
- {{/if}}
19
- </body>
20
- </html>