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.
- package/.editorconfig +15 -0
- package/.yarnrc.yml +6 -0
- package/CLI.md +82 -0
- package/LICENSE +9 -0
- package/PACKAGE_MANAGERS.md +35 -0
- package/README.md +204 -124
- package/app/404.tsx +28 -0
- package/app/README.md +66 -8
- package/app/assets/data/view/global.json +7 -47
- package/app/assets/data/view/index.json +9 -1
- package/app/assets/script/index.tsx +13 -0
- package/app/assets/script/tasks/TaskApplication.ts +77 -0
- package/app/assets/script/tasks/TaskMediator.ts +20 -0
- package/app/assets/script/tasks/TaskModel.ts +32 -0
- package/app/assets/script/tasks/TaskRouter.ts +30 -0
- package/app/assets/script/tasks/TaskView.tsx +24 -0
- package/app/assets/style/global.css +111 -0
- package/app/assets/style/print.css +8 -0
- package/app/assets/view/CodeBlock.tsx +17 -0
- package/app/assets/view/examples/tasks/TaskExample.tsx +63 -0
- package/app/assets/view/examples/tasks/task-state.ts +46 -0
- package/app/assets/view/layout/SiteFooter.tsx +11 -0
- package/app/assets/view/layout/SiteHeader.tsx +23 -0
- package/app/assets/view/page-data.ts +14 -0
- package/app/assets/view/sections/GettingStartedSection.tsx +27 -0
- package/app/assets/view/sections/HeroSection.tsx +21 -0
- package/app/assets/view/sections/LiveExampleSection.tsx +29 -0
- package/app/assets/view/sections/PackageDocsSection.tsx +69 -0
- package/app/assets/view/sections/SourceGuideSection.tsx +28 -0
- package/app/index.tsx +54 -0
- package/cli/index.ts +117 -0
- package/dist/app/404.d.ts +1 -0
- package/dist/app/404.js +6 -0
- package/dist/app/404.js.map +1 -0
- package/dist/app/assets/script/index.d.ts +8 -2
- package/dist/app/assets/script/index.js +11 -1
- package/dist/app/assets/script/index.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskApplication.d.ts +20 -0
- package/dist/app/assets/script/tasks/TaskApplication.js +63 -0
- package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskMediator.d.ts +15 -0
- package/dist/app/assets/script/tasks/TaskMediator.js +5 -0
- package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskModel.d.ts +13 -0
- package/dist/app/assets/script/tasks/TaskModel.js +33 -0
- package/dist/app/assets/script/tasks/TaskModel.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskRouter.d.ts +12 -0
- package/dist/app/assets/script/tasks/TaskRouter.js +28 -0
- package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskView.d.ts +9 -0
- package/dist/app/assets/script/tasks/TaskView.js +24 -0
- package/dist/app/assets/script/tasks/TaskView.js.map +1 -0
- package/dist/app/assets/view/CodeBlock.d.ts +11 -0
- package/dist/app/assets/view/CodeBlock.js +12 -0
- package/dist/app/assets/view/CodeBlock.js.map +1 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +11 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.js +16 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -0
- package/dist/app/assets/view/examples/tasks/task-state.d.ts +22 -0
- package/dist/app/assets/view/examples/tasks/task-state.js +30 -0
- package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -0
- package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -0
- package/dist/app/assets/view/layout/SiteFooter.js +6 -0
- package/dist/app/assets/view/layout/SiteFooter.js.map +1 -0
- package/dist/app/assets/view/layout/SiteHeader.d.ts +8 -0
- package/dist/app/assets/view/layout/SiteHeader.js +12 -0
- package/dist/app/assets/view/layout/SiteHeader.js.map +1 -0
- package/dist/app/assets/view/page-data.d.ts +16 -0
- package/dist/app/assets/view/page-data.js +2 -0
- package/dist/app/assets/view/page-data.js.map +1 -0
- package/dist/app/assets/view/sections/GettingStartedSection.d.ts +2 -0
- package/dist/app/assets/view/sections/GettingStartedSection.js +11 -0
- package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -0
- package/dist/app/assets/view/sections/HeroSection.d.ts +8 -0
- package/dist/app/assets/view/sections/HeroSection.js +12 -0
- package/dist/app/assets/view/sections/HeroSection.js.map +1 -0
- package/dist/app/assets/view/sections/LiveExampleSection.d.ts +6 -0
- package/dist/app/assets/view/sections/LiveExampleSection.js +13 -0
- package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -0
- package/dist/app/assets/view/sections/PackageDocsSection.d.ts +2 -0
- package/dist/app/assets/view/sections/PackageDocsSection.js +31 -0
- package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -0
- package/dist/app/assets/view/sections/SourceGuideSection.d.ts +7 -0
- package/dist/app/assets/view/sections/SourceGuideSection.js +11 -0
- package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +22 -0
- package/dist/app/index.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +97 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/scaffold/index.d.ts +64 -0
- package/dist/scaffold/index.js +109 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scripts/build.d.ts +5 -13
- package/dist/scripts/build.js +48 -79
- package/dist/scripts/build.js.map +1 -1
- package/package.json +77 -40
- package/pnpm-workspace.yaml +4 -0
- package/scaffold/index.ts +127 -0
- package/scaffold/no-jsx/README.md +47 -0
- package/scaffold/no-jsx/app/404.ts +10 -0
- package/scaffold/no-jsx/app/assets/script/index.ts +7 -0
- package/scaffold/no-jsx/app/assets/script/tasks/TaskView.ts +19 -0
- package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +39 -0
- package/scaffold/no-jsx/app/index.ts +58 -0
- package/scripts/build.ts +55 -86
- package/server/handler.ts +75 -0
- package/template-test/site.test.js +233 -0
- package/tsconfig.site.json +6 -5
- package/tsconfig.site.no-jsx.json +24 -0
- package/app/404.hbs +0 -20
- package/app/assets/data/coutries.json +0 -992
- package/app/assets/data/states.json +0 -204
- package/app/assets/data/view/toolkit/colors.json +0 -17
- package/app/assets/data/view/toolkit/components.json +0 -39
- package/app/assets/data/view/toolkit/elements.json +0 -119
- package/app/assets/data/view/toolkit/grids.json +0 -17
- package/app/assets/data/view/toolkit/icons.json +0 -17
- package/app/assets/data/view/toolkit/index.json +0 -17
- package/app/assets/data/view/toolkit/type.json +0 -17
- package/app/assets/font/opensans/LICENSE.txt +0 -202
- package/app/assets/font/opensans/opensans-bold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-bolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-extrabold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-extrabolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-italic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-light-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-lightitalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-regular-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-semibold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-semibolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/stylesheet.css +0 -120
- package/app/assets/image/apple-touch-icon.png +0 -0
- package/app/assets/image/demo/fallback.avif +0 -0
- package/app/assets/image/demo/fallback.jpg +0 -0
- package/app/assets/image/demo/fallback.webp +0 -0
- package/app/assets/image/demo/large.avif +0 -0
- package/app/assets/image/demo/large.webp +0 -0
- package/app/assets/image/demo/small.avif +0 -0
- package/app/assets/image/demo/small.webp +0 -0
- package/app/assets/image/favicon.png +0 -0
- package/app/assets/image/graphic-css3.svg +0 -30
- package/app/assets/image/graphic-html5.svg +0 -29
- package/app/assets/markup/element/a.hbs +0 -29
- package/app/assets/markup/element/button.hbs +0 -30
- package/app/assets/markup/element/div.hbs +0 -11
- package/app/assets/markup/element/head.hbs +0 -78
- package/app/assets/markup/element/iframe.hbs +0 -20
- package/app/assets/markup/element/img.hbs +0 -30
- package/app/assets/markup/element/input.hbs +0 -43
- package/app/assets/markup/element/label.hbs +0 -14
- package/app/assets/markup/element/legend.hbs +0 -5
- package/app/assets/markup/element/meta.hbs +0 -11
- package/app/assets/markup/element/nav.hbs +0 -13
- package/app/assets/markup/element/p.hbs +0 -8
- package/app/assets/markup/element/picture.hbs +0 -58
- package/app/assets/markup/element/script.hbs +0 -3
- package/app/assets/markup/element/select.hbs +0 -35
- package/app/assets/markup/element/span.hbs +0 -8
- package/app/assets/markup/element/svg.hbs +0 -11
- package/app/assets/markup/element/table.hbs +0 -50
- package/app/assets/markup/element/textarea.hbs +0 -24
- package/app/assets/markup/element/ul.hbs +0 -13
- package/app/assets/markup/modal/modal-example-1.handlebars +0 -8
- package/app/assets/markup/toolkit/accessibility-skip-to-element-1.handlebars +0 -1
- package/app/assets/markup/toolkit/accessibility-speaker-1.handlebars +0 -1
- package/app/assets/markup/toolkit/anchor-1.handlebars +0 -1
- package/app/assets/markup/toolkit/button-1.handlebars +0 -1
- package/app/assets/markup/toolkit/footer-1.handlebars +0 -3
- package/app/assets/markup/toolkit/header-1.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-1.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-2.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-3.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-4.handlebars +0 -1
- package/app/assets/markup/toolkit/image-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-checkbox-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-error-1.handlebars +0 -8
- package/app/assets/markup/toolkit/input-radio-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-text-1.handlebars +0 -1
- package/app/assets/markup/toolkit/label-1.handlebars +0 -1
- package/app/assets/markup/toolkit/legend-1.handlebars +0 -1
- package/app/assets/markup/toolkit/list-1.handlebars +0 -1
- package/app/assets/markup/toolkit/modal-1.handlebars +0 -4
- package/app/assets/markup/toolkit/nav-1.handlebars +0 -1
- package/app/assets/markup/toolkit/picture-1.handlebars +0 -1
- package/app/assets/markup/toolkit/select-1.handlebars +0 -1
- package/app/assets/markup/toolkit/table-1.handlebars +0 -1
- package/app/assets/markup/toolkit/text-1.handlebars +0 -1
- package/app/assets/markup/toolkit/textarea-1.handlebars +0 -1
- package/app/assets/markup/toolkit/youtube-player-1.handlebars +0 -1
- package/app/assets/script/api/facebook.ts +0 -41
- package/app/assets/script/api/youtube.ts +0 -22
- package/app/assets/script/index.ts +0 -2
- package/app/assets/script/mediator/global.ts +0 -4
- package/app/assets/script/model/singletons/countries.ts +0 -5
- package/app/assets/script/model/singletons/global.ts +0 -5
- package/app/assets/script/model/singletons/states.ts +0 -5
- package/app/assets/script/model/user.ts +0 -4
- package/app/assets/script/toolkit.ts +0 -11
- package/app/assets/script/types.d.ts +0 -16
- package/app/assets/script/utility/ajax.ts +0 -15
- package/app/assets/script/utility/form.ts +0 -119
- package/app/assets/script/utility/regex.ts +0 -26
- package/app/assets/script/utility/share.ts +0 -77
- package/app/assets/script/utility/string.ts +0 -148
- package/app/assets/script/view/default.tsx +0 -14
- package/app/assets/script/view/toolkit/youtube-player-1.ts +0 -31
- package/app/assets/style/base/_toolkit.scss +0 -39
- package/app/assets/style/base/colors/_colors.scss +0 -18
- package/app/assets/style/base/components/_modal-1.scss +0 -80
- package/app/assets/style/base/components/_sticky-1.scss +0 -19
- package/app/assets/style/base/components/_tabpanel-1.scss +0 -115
- package/app/assets/style/base/components/_tabpanel-2.scss +0 -64
- package/app/assets/style/base/components/_tabpanel-base-1.scss +0 -13
- package/app/assets/style/base/components/_youtube-player-1.scss +0 -16
- package/app/assets/style/base/elements/_anchors.scss +0 -27
- package/app/assets/style/base/elements/_buttons.scss +0 -25
- package/app/assets/style/base/elements/_fieldsets.scss +0 -9
- package/app/assets/style/base/elements/_images.scss +0 -8
- package/app/assets/style/base/elements/_inputs.scss +0 -78
- package/app/assets/style/base/elements/_labels.scss +0 -16
- package/app/assets/style/base/elements/_landmarks.scss +0 -3
- package/app/assets/style/base/elements/_legends.scss +0 -11
- package/app/assets/style/base/elements/_lists.scss +0 -15
- package/app/assets/style/base/elements/_pictures.scss +0 -4
- package/app/assets/style/base/elements/_tables.scss +0 -39
- package/app/assets/style/base/layouts/_staggered-1.scss +0 -29
- package/app/assets/style/base/type/_fonts.scss +0 -125
- package/app/assets/style/base/type/_headings.scss +0 -36
- package/app/assets/style/base/type/_text.scss +0 -9
- package/app/assets/style/base/utilities/_accessibility.scss +0 -55
- package/app/assets/style/base/utilities/_functions.scss +0 -98
- package/app/assets/style/base/utilities/_scroll.scss +0 -11
- package/app/assets/style/base/utilities/_type.scss +0 -13
- package/app/assets/style/global.scss +0 -123
- package/app/assets/style/print.scss +0 -6
- package/app/assets/style/toolkit.scss +0 -387
- package/app/index.hbs +0 -21
- package/app/robots.txt +0 -1
- package/app/toolkit/colors.hbs +0 -102
- package/app/toolkit/components.hbs +0 -261
- package/app/toolkit/elements.hbs +0 -497
- package/app/toolkit/grids.hbs +0 -413
- package/app/toolkit/icons.hbs +0 -87
- package/app/toolkit/index.hbs +0 -144
- package/app/toolkit/type.hbs +0 -120
- package/dist/app/assets/data/coutries.json +0 -992
- package/dist/app/assets/data/states.json +0 -204
- package/dist/app/assets/script/api/facebook.d.ts +0 -8
- package/dist/app/assets/script/api/facebook.js +0 -42
- package/dist/app/assets/script/api/facebook.js.map +0 -1
- package/dist/app/assets/script/api/youtube.d.ts +0 -8
- package/dist/app/assets/script/api/youtube.js +0 -22
- package/dist/app/assets/script/api/youtube.js.map +0 -1
- package/dist/app/assets/script/mediator/global.d.ts +0 -4
- package/dist/app/assets/script/mediator/global.js +0 -4
- package/dist/app/assets/script/mediator/global.js.map +0 -1
- package/dist/app/assets/script/model/singletons/countries.d.ts +0 -20
- package/dist/app/assets/script/model/singletons/countries.js +0 -5
- package/dist/app/assets/script/model/singletons/countries.js.map +0 -1
- package/dist/app/assets/script/model/singletons/global.d.ts +0 -20
- package/dist/app/assets/script/model/singletons/global.js +0 -5
- package/dist/app/assets/script/model/singletons/global.js.map +0 -1
- package/dist/app/assets/script/model/singletons/states.d.ts +0 -20
- package/dist/app/assets/script/model/singletons/states.js +0 -5
- package/dist/app/assets/script/model/singletons/states.js.map +0 -1
- package/dist/app/assets/script/model/user.d.ts +0 -4
- package/dist/app/assets/script/model/user.js +0 -6
- package/dist/app/assets/script/model/user.js.map +0 -1
- package/dist/app/assets/script/toolkit.d.ts +0 -1
- package/dist/app/assets/script/toolkit.js +0 -7
- package/dist/app/assets/script/toolkit.js.map +0 -1
- package/dist/app/assets/script/utility/ajax.d.ts +0 -8
- package/dist/app/assets/script/utility/ajax.js +0 -17
- package/dist/app/assets/script/utility/ajax.js.map +0 -1
- package/dist/app/assets/script/utility/form.d.ts +0 -64
- package/dist/app/assets/script/utility/form.js +0 -111
- package/dist/app/assets/script/utility/form.js.map +0 -1
- package/dist/app/assets/script/utility/regex.d.ts +0 -14
- package/dist/app/assets/script/utility/regex.js +0 -27
- package/dist/app/assets/script/utility/regex.js.map +0 -1
- package/dist/app/assets/script/utility/share.d.ts +0 -68
- package/dist/app/assets/script/utility/share.js +0 -81
- package/dist/app/assets/script/utility/share.js.map +0 -1
- package/dist/app/assets/script/utility/string.d.ts +0 -64
- package/dist/app/assets/script/utility/string.js +0 -135
- package/dist/app/assets/script/utility/string.js.map +0 -1
- package/dist/app/assets/script/view/default.d.ts +0 -11
- package/dist/app/assets/script/view/default.js +0 -15
- package/dist/app/assets/script/view/default.js.map +0 -1
- package/dist/app/assets/script/view/toolkit/youtube-player-1.d.ts +0 -23
- package/dist/app/assets/script/view/toolkit/youtube-player-1.js +0 -32
- package/dist/app/assets/script/view/toolkit/youtube-player-1.js.map +0 -1
- package/dist/generators/app/index.d.ts +0 -18
- package/dist/generators/app/index.js +0 -59
- package/dist/generators/app/index.js.map +0 -1
- package/generators/app/index.js +0 -2
- 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;
|
|
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 {
|
|
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
|
-
|
|
30
|
+
siteUrl,
|
|
31
31
|
version,
|
|
32
32
|
www: values.www || '/'
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
70
|
-
|
|
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(
|
|
65
|
+
const relative = path.relative(appRoot, page);
|
|
74
66
|
const pageDataPath = path.join(
|
|
75
|
-
projectRoot, 'app/assets/data/view', relative.replace(/\.
|
|
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
|
|
86
|
-
const
|
|
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,
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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('&', '&')
|
|
15
|
+
.replaceAll('<', '<')
|
|
16
|
+
.replaceAll('>', '>')
|
|
17
|
+
.replaceAll('"', '"')
|
|
18
|
+
.replaceAll("'", ''');
|
|
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 <strong>Ada<\/strong>\./);
|
|
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
|
+
});
|
package/tsconfig.site.json
CHANGED
|
@@ -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
|
-
"
|
|
24
|
-
"app
|
|
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>
|