@antelopejs/dms-frontend 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -24
- package/dist/commands/build-action.js +3 -2
- package/dist/commands/dev-action.js +2 -1
- package/dist/commands/prepare-action.js +1 -1
- package/dist/commands/start-action.js +3 -24
- package/dist/commands/workspace-task.js +2 -1
- package/dist/config.js +3 -0
- package/dist/derived-outputs.js +3 -3
- package/dist/discovery.js +17 -6
- package/dist/entry-weight.js +15 -0
- package/dist/fs-sync.js +7 -4
- package/dist/index.js +2 -1
- package/dist/internal/built-workspace.d.ts +4 -0
- package/dist/internal/built-workspace.js +43 -0
- package/dist/internal/ensure.d.ts +1 -0
- package/dist/internal/ensure.js +8 -0
- package/dist/internal/locale-catalogs.d.ts +8 -3
- package/dist/internal/locale-catalogs.js +16 -6
- package/dist/internal/manifest-decoder.d.ts +7 -0
- package/dist/internal/manifest-decoder.js +81 -0
- package/dist/internal/own-release.d.ts +5 -0
- package/dist/internal/own-release.js +20 -0
- package/dist/internal/plain-object.d.ts +1 -0
- package/dist/internal/plain-object.js +6 -0
- package/dist/internal/text-list.d.ts +1 -0
- package/dist/internal/text-list.js +10 -0
- package/dist/internal/vite-manifest.d.ts +6 -0
- package/dist/internal/vite-manifest.js +24 -0
- package/dist/layer-watch.js +25 -24
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +3 -1
- package/dist/locale-files.d.ts +2 -1
- package/dist/locale-files.js +4 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +44 -31
- package/dist/materialize.js +6 -2
- package/dist/module-set.js +2 -1
- package/dist/node-options.d.ts +1 -0
- package/dist/node-options.js +8 -0
- package/dist/output.d.ts +15 -9
- package/dist/output.js +30 -18
- package/dist/private-values.d.ts +1 -1
- package/dist/private-values.js +5 -3
- package/dist/project-modules.js +13 -8
- package/dist/renderer-range.js +5 -3
- package/dist/server-process.js +1 -1
- package/dist/verify-source-build.js +10 -2
- package/dist/verify-source-client-bundle.d.ts +1 -0
- package/dist/verify-source-client-bundle.js +30 -0
- package/dist/verify-source-runner.js +44 -60
- package/dist/verify-source-sources.js +49 -11
- package/dist/workspace-setup.js +1 -1
- package/package.json +6 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/templates/vue/app-runtime.ts +90 -16
- package/templates/vue/client-server-guard.mjs +16 -0
- package/templates/vue/compress-assets.mjs +9 -1
- package/templates/vue/entry-weight.mjs +22 -0
- package/templates/vue/frontend-module.ts +1 -0
- package/templates/vue/frontend-runtime/module-registration.ts +2 -2
- package/templates/vue/frontend-runtime/state.ts +2 -2
- package/templates/vue/frontend-runtime/types.ts +8 -2
- package/templates/vue/globals.d.ts +5 -0
- package/templates/vue/head-order.mjs +7 -0
- package/templates/vue/icon-bundle.ts +31 -0
- package/templates/vue/icon-scan.mjs +4 -0
- package/templates/vue/message-subset.ts +65 -0
- package/templates/vue/network-error.ts +4 -0
- package/templates/vue/server/auth/backend.mjs +41 -4
- package/templates/vue/server/auth/client-ip.mjs +52 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
- package/templates/vue/server/auth/session.mjs +81 -2
- package/templates/vue/server/auth-hooks.mjs +15 -5
- package/templates/vue/server/backend-proxies.mjs +65 -9
- package/templates/vue/server/backend-request.mjs +61 -0
- package/templates/vue/server/backend-response.mjs +23 -6
- package/templates/vue/server/built-files.mjs +7 -0
- package/templates/vue/server/cli-report.mjs +53 -11
- package/templates/vue/server/client-manifest.mjs +23 -1
- package/templates/vue/server/content.mjs +18 -1
- package/templates/vue/server/dev-private-files.mjs +17 -1
- package/templates/vue/server/dev-styles.mjs +38 -0
- package/templates/vue/server/email.mjs +9 -3
- package/templates/vue/server/favicon.mjs +3 -0
- package/templates/vue/server/generated-files.mjs +211 -0
- package/templates/vue/server/homepage.mjs +21 -9
- package/templates/vue/server/html-tag.mjs +4 -0
- package/templates/vue/server/icons.mjs +80 -7
- package/templates/vue/server/in-place-render.mjs +35 -0
- package/templates/vue/server/inertia.mjs +61 -2
- package/templates/vue/server/module-event.mjs +98 -17
- package/templates/vue/server/module-route-types.mjs +78 -0
- package/templates/vue/server/module-routes.mjs +138 -31
- package/templates/vue/server/module-server.mjs +98 -27
- package/templates/vue/server/module-testing.mjs +14 -5
- package/templates/vue/server/page-props.mjs +92 -2
- package/templates/vue/server/page-redirects.mjs +37 -0
- package/templates/vue/server/path-patterns.mjs +13 -1
- package/templates/vue/server/proxy-compression.mjs +22 -0
- package/templates/vue/server/proxy-headers.mjs +11 -1
- package/templates/vue/server/proxy-redirects.mjs +20 -5
- package/templates/vue/server/proxy-request.mjs +57 -0
- package/templates/vue/server/proxy-transfers.mjs +33 -5
- package/templates/vue/server/public-endpoints.mjs +138 -23
- package/templates/vue/server/public-options.mjs +20 -6
- package/templates/vue/server/request-failure.mjs +23 -0
- package/templates/vue/server/request-routing.mjs +26 -9
- package/templates/vue/server/request-target.mjs +9 -3
- package/templates/vue/server/route-rules.mjs +39 -15
- package/templates/vue/server/ssr-module.mjs +32 -0
- package/templates/vue/server/static-files.mjs +24 -1
- package/templates/vue/server/type-guards.mjs +71 -0
- package/templates/vue/server/vite-errors.mjs +32 -5
- package/templates/vue/server/web-body.mjs +3 -0
- package/templates/vue/server-module.ts +10 -1
- package/templates/vue/server.mjs +132 -148
- package/templates/vue/sfc-global-types.mjs +112 -13
- package/templates/vue/ssr-renderer.ts +12 -3
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- package/templates/vue/ui-lazy-icons.mjs +44 -0
- package/templates/vue/ui-request-state.mjs +2 -1
- package/templates/vue/vite.config.ts +2 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { viteManifestOf } from "./generated-files.mjs";
|
|
4
5
|
|
|
5
6
|
const PROJECT_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
6
7
|
const BUILT_TEMPLATE_PATH = join(PROJECT_ROOT, "dist/client/index.html");
|
|
@@ -14,7 +15,9 @@ const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
|
|
|
14
15
|
* Read once and kept: both files are build output and cannot change while
|
|
15
16
|
* the server is up.
|
|
16
17
|
*/
|
|
18
|
+
/** @type {import("vite").Manifest | undefined} */
|
|
17
19
|
let clientManifest;
|
|
20
|
+
/** @type {string | undefined} */
|
|
18
21
|
let productionTemplate;
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -31,13 +34,22 @@ export function productionHtmlTemplate() {
|
|
|
31
34
|
return productionTemplate;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/** @returns {import("vite").Manifest | undefined} */
|
|
34
38
|
function productionClientManifest() {
|
|
35
39
|
if (clientManifest) return clientManifest;
|
|
36
40
|
if (!existsSync(CLIENT_MANIFEST_PATH)) return undefined;
|
|
37
|
-
clientManifest =
|
|
41
|
+
clientManifest = viteManifestOf(
|
|
42
|
+
JSON.parse(readFileSync(CLIENT_MANIFEST_PATH, "utf8")),
|
|
43
|
+
);
|
|
38
44
|
return clientManifest;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {import("vite").Manifest} manifest
|
|
49
|
+
* @param {string} key
|
|
50
|
+
* @param {Set<string>} styles
|
|
51
|
+
* @param {Set<string>} visited
|
|
52
|
+
*/
|
|
41
53
|
function collectManifestStyles(manifest, key, styles, visited) {
|
|
42
54
|
if (visited.has(key)) return;
|
|
43
55
|
visited.add(key);
|
|
@@ -56,11 +68,16 @@ function collectManifestStyles(manifest, key, styles, visited) {
|
|
|
56
68
|
* component, so the lookup needs no guessing from names. A component bundled
|
|
57
69
|
* into another chunk is no key of its own: its stylesheet comes with that
|
|
58
70
|
* chunk's, reached through the `imports` of the component that loaded it.
|
|
71
|
+
*
|
|
72
|
+
* @param {Iterable<string>} [modules]
|
|
73
|
+
* @returns {Set<string>}
|
|
59
74
|
*/
|
|
60
75
|
function renderedModuleStyles(modules = []) {
|
|
61
76
|
const manifest = productionClientManifest();
|
|
77
|
+
/** @type {Set<string>} */
|
|
62
78
|
const styles = new Set();
|
|
63
79
|
if (!manifest) return styles;
|
|
80
|
+
/** @type {Set<string>} */
|
|
64
81
|
const visited = new Set();
|
|
65
82
|
for (const id of modules)
|
|
66
83
|
collectManifestStyles(manifest, id, styles, visited);
|
|
@@ -72,6 +89,9 @@ function renderedModuleStyles(modules = []) {
|
|
|
72
89
|
* template does not link already, so that none of them waits for its chunk's
|
|
73
90
|
* JavaScript to arrive: the HTML would otherwise paint unstyled and shift once
|
|
74
91
|
* it does. A component the render did not reach gets no link.
|
|
92
|
+
*
|
|
93
|
+
* @param {Iterable<string> | undefined} modules
|
|
94
|
+
* @param {string} template
|
|
75
95
|
*/
|
|
76
96
|
export function renderedModuleStyleTags(modules, template) {
|
|
77
97
|
return [...renderedModuleStyles(modules)]
|
|
@@ -85,6 +105,8 @@ export function renderedModuleStyleTags(modules, template) {
|
|
|
85
105
|
* rendered with: the browser fetches it next to the entry rather than once the
|
|
86
106
|
* entry runs and asks for it, which would put a round trip before hydration.
|
|
87
107
|
* Nothing without a production build (Vite serves the sources in development).
|
|
108
|
+
*
|
|
109
|
+
* @param {string | undefined} locale
|
|
88
110
|
*/
|
|
89
111
|
export function localeCatalogPreloadTag(locale) {
|
|
90
112
|
const manifest = productionClientManifest();
|
|
@@ -17,9 +17,13 @@ const DYNAMIC_BROTLI_QUALITY = 4;
|
|
|
17
17
|
* client gets each part when it is written, not when a block fills.
|
|
18
18
|
*
|
|
19
19
|
* Implicit identity is a fallback, not a preference over compression.
|
|
20
|
+
*
|
|
21
|
+
* @param {import("node:http").IncomingMessage} request
|
|
22
|
+
* @param {{ streaming?: boolean }} [options]
|
|
20
23
|
*/
|
|
21
24
|
export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
22
25
|
const accepted = request.headers["accept-encoding"] ?? "";
|
|
26
|
+
/** @type {Map<string, number>} */
|
|
23
27
|
const qualities = new Map();
|
|
24
28
|
for (const entry of accepted.split(",")) {
|
|
25
29
|
const [token, ...parameters] = entry.trim().toLowerCase().split(";");
|
|
@@ -37,6 +41,7 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
37
41
|
Math.min(qualities.get(token.trim()) ?? 1, quality),
|
|
38
42
|
);
|
|
39
43
|
}
|
|
44
|
+
/** @param {string} token */
|
|
40
45
|
const quality = (token) => qualities.get(token) ?? qualities.get("*") ?? 0;
|
|
41
46
|
const br = quality("br");
|
|
42
47
|
const gzip = quality("gzip");
|
|
@@ -61,12 +66,24 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @param {import("node:http").IncomingMessage} request
|
|
71
|
+
* @param {string | Buffer} content
|
|
72
|
+
*/
|
|
64
73
|
function responseEncoder(request, content) {
|
|
65
74
|
if (Buffer.byteLength(content) < MINIMUM_COMPRESSION_BYTES) return undefined;
|
|
66
75
|
return acceptedEncoder(request);
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Send a generated body, compressed when the client accepts it.
|
|
80
|
+
*
|
|
81
|
+
* @param {import("node:http").IncomingMessage} request
|
|
82
|
+
* @param {import("node:http").ServerResponse} response
|
|
83
|
+
* @param {number} status
|
|
84
|
+
* @param {import("node:http").OutgoingHttpHeaders} headers
|
|
85
|
+
* @param {string | Buffer} content
|
|
86
|
+
*/
|
|
70
87
|
export async function writeContent(
|
|
71
88
|
request,
|
|
72
89
|
response,
|
|
@@ -28,6 +28,10 @@ export const DEV_FS_DENY = [
|
|
|
28
28
|
* file, a module's server entry, or a file under a `server/` directory of a
|
|
29
29
|
* module materialized under `root`. Compared without case, as a
|
|
30
30
|
* case-insensitive file system would serve them.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} file
|
|
33
|
+
* @param {string} root
|
|
34
|
+
* @returns {boolean}
|
|
31
35
|
*/
|
|
32
36
|
export function isPrivateFile(file, root) {
|
|
33
37
|
const name = (resolve(file).split(/[\\/]/).at(-1) ?? "").toLowerCase();
|
|
@@ -39,12 +43,22 @@ export function isPrivateFile(file, root) {
|
|
|
39
43
|
);
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} id
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
42
50
|
function stripQuery(id) {
|
|
43
51
|
const query = id.search(/[?#]/);
|
|
44
52
|
return query === -1 ? id : id.slice(0, query);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The file a request URL names under `root`, `undefined` when it names none.
|
|
57
|
+
*
|
|
58
|
+
* @param {string | undefined} url
|
|
59
|
+
* @param {string} root
|
|
60
|
+
* @returns {string | undefined}
|
|
61
|
+
*/
|
|
48
62
|
function requestedFile(url, root) {
|
|
49
63
|
let pathname;
|
|
50
64
|
try {
|
|
@@ -74,6 +88,8 @@ function requestedFile(url, root) {
|
|
|
74
88
|
*
|
|
75
89
|
* The middleware is registered directly, not returned: it runs before Vite's
|
|
76
90
|
* middlewares.
|
|
91
|
+
*
|
|
92
|
+
* @returns {import("vite").Plugin}
|
|
77
93
|
*/
|
|
78
94
|
export function denyPrivateFiles() {
|
|
79
95
|
let root = process.cwd();
|
|
@@ -8,6 +8,13 @@ const SFC_STYLE = /[?&]vue&type=style/;
|
|
|
8
8
|
const STYLE_EXTENSION =
|
|
9
9
|
/\.(?:css|less|pcss|postcss|sass|scss|styl|stylus)(?:$|\?)/;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A stylesheet module of the development server's graph: its URL, and the id
|
|
13
|
+
* its tag is matched by.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {{ url: string, id: string }} StyleModule
|
|
16
|
+
*/
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* The workspace's generated entry stylesheet. `main.ts` is its only importer,
|
|
13
20
|
* so it never enters the SSR module graph, yet it carries Tailwind, Nuxt UI and
|
|
@@ -25,6 +32,16 @@ const ENTRY_STYLESHEET_ID = join(PROJECT_ROOT, "dms-main.css")
|
|
|
25
32
|
.split(sep)
|
|
26
33
|
.join("/");
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{ urlToModuleMap: ReadonlyMap<string, { id: string | null } | undefined> }} StyleGraph
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* environments?: { ssr?: { moduleGraph?: StyleGraph } },
|
|
39
|
+
* moduleGraph?: StyleGraph,
|
|
40
|
+
* transformRequest(url: string): Promise<{ code: string } | null>,
|
|
41
|
+
* }} StyleServer
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** @param {StyleServer} devServer */
|
|
28
45
|
function ssrModuleGraph(devServer) {
|
|
29
46
|
return devServer.environments?.ssr?.moduleGraph ?? devServer.moduleGraph;
|
|
30
47
|
}
|
|
@@ -39,6 +56,10 @@ function ssrModuleGraph(devServer) {
|
|
|
39
56
|
*/
|
|
40
57
|
const TRAILING_LANG = /&(lang\.[^&?]*)$/;
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} url
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
42
63
|
function directUrl(url) {
|
|
43
64
|
if (SFC_STYLE.test(url) && TRAILING_LANG.test(url)) {
|
|
44
65
|
return url.replace(TRAILING_LANG, "&direct&$1");
|
|
@@ -46,6 +67,10 @@ function directUrl(url) {
|
|
|
46
67
|
return `${url}${url.includes("?") ? "&" : "?"}direct`;
|
|
47
68
|
}
|
|
48
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {StyleServer} devServer
|
|
72
|
+
* @returns {StyleModule[]}
|
|
73
|
+
*/
|
|
49
74
|
function styleModules(devServer) {
|
|
50
75
|
const graph = ssrModuleGraph(devServer);
|
|
51
76
|
if (!graph) return [];
|
|
@@ -54,6 +79,7 @@ function styleModules(devServer) {
|
|
|
54
79
|
.map(([url, module]) => ({ url, id: module?.id ?? url }));
|
|
55
80
|
}
|
|
56
81
|
|
|
82
|
+
/** @param {StyleModule} module */
|
|
57
83
|
function linkTag({ url, id }) {
|
|
58
84
|
return `<link rel="stylesheet" ${STYLE_ATTRIBUTE}="${id}" href="${directUrl(url)}">`;
|
|
59
85
|
}
|
|
@@ -64,6 +90,10 @@ function linkTag({ url, id }) {
|
|
|
64
90
|
* stylesheet. They are small; inline them instead. A block that could close its
|
|
65
91
|
* own tag is dropped rather than escaped — CSS has no escape that is valid in
|
|
66
92
|
* every position, and a scoped component rule is not worth the risk.
|
|
93
|
+
*
|
|
94
|
+
* @param {StyleServer} devServer
|
|
95
|
+
* @param {StyleModule} module
|
|
96
|
+
* @returns {Promise<string>}
|
|
67
97
|
*/
|
|
68
98
|
async function inlineTag(devServer, { url, id }) {
|
|
69
99
|
const result = await devServer
|
|
@@ -87,6 +117,9 @@ async function inlineTag(devServer, { url, id }) {
|
|
|
87
117
|
* stylesheet, which is why this module is development-only and lives beside
|
|
88
118
|
* client-manifest.mjs rather than inside server.mjs, whose size the linter
|
|
89
119
|
* caps.
|
|
120
|
+
*
|
|
121
|
+
* @param {StyleServer} devServer
|
|
122
|
+
* @returns {Promise<string>}
|
|
90
123
|
*/
|
|
91
124
|
export async function developmentStyleTags(devServer) {
|
|
92
125
|
const modules = styleModules(devServer);
|
|
@@ -106,6 +139,11 @@ export async function developmentStyleTags(devServer) {
|
|
|
106
139
|
* The style tags a document needs, whichever server is answering it: collected
|
|
107
140
|
* from the live module graph in development, read from the build manifest for
|
|
108
141
|
* the components the render created (`modules`) in production.
|
|
142
|
+
*
|
|
143
|
+
* @param {import("vite").ViteDevServer | undefined} devServer
|
|
144
|
+
* @param {Iterable<string> | undefined} modules
|
|
145
|
+
* @param {string} template
|
|
146
|
+
* @returns {Promise<string>}
|
|
109
147
|
*/
|
|
110
148
|
export async function documentStyleTags(devServer, modules, template) {
|
|
111
149
|
return devServer
|
|
@@ -17,8 +17,11 @@ const EMAIL_BUILD_WAIT_MS = 60_000;
|
|
|
17
17
|
* Settled whenever no email build is running. The version changes with each
|
|
18
18
|
* finished build: Node caches an imported module by URL, so the renderer is
|
|
19
19
|
* imported under a new one to pick up rebuilt templates.
|
|
20
|
+
*
|
|
21
|
+
* @type {Promise<void>}
|
|
20
22
|
*/
|
|
21
23
|
let emailBuild = Promise.resolve();
|
|
24
|
+
/** @type {(() => void) | undefined} */
|
|
22
25
|
let settleEmailBuild;
|
|
23
26
|
let emailBundleVersion = 0;
|
|
24
27
|
/**
|
|
@@ -28,6 +31,7 @@ let emailBundleVersion = 0;
|
|
|
28
31
|
* finished.
|
|
29
32
|
*/
|
|
30
33
|
let rendersInProgress = 0;
|
|
34
|
+
/** @type {(() => void) | undefined} */
|
|
31
35
|
let startPendingBuild;
|
|
32
36
|
|
|
33
37
|
function emailBuildStarted() {
|
|
@@ -44,6 +48,7 @@ function emailBuildSettled() {
|
|
|
44
48
|
settleEmailBuild = undefined;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/** @param {() => void} start */
|
|
47
52
|
function startBuildOnceRendersFinish(start) {
|
|
48
53
|
if (rendersInProgress) startPendingBuild = start;
|
|
49
54
|
else start();
|
|
@@ -107,6 +112,7 @@ async function waitForEmailBuild() {
|
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
/** @returns {Promise<typeof import("../email-renderer.ts")>} */
|
|
110
116
|
function loadEmailRenderer() {
|
|
111
117
|
const url = new URL(EMAIL_RENDERER_URL);
|
|
112
118
|
if (emailBundleVersion) url.searchParams.set("v", emailBundleVersion);
|
|
@@ -121,9 +127,9 @@ function loadEmailRenderer() {
|
|
|
121
127
|
* for now waits for this render, which imports templates and reads catalogs
|
|
122
128
|
* from the bundle until it ends.
|
|
123
129
|
*
|
|
124
|
-
* @param template The template's name
|
|
125
|
-
* @param props The template's props
|
|
126
|
-
* @param options `locale`, the language to render in
|
|
130
|
+
* @param {string} template The template's name
|
|
131
|
+
* @param {Record<string, unknown>} props The template's props
|
|
132
|
+
* @param {{ locale?: string }} [options] `locale`, the language to render in
|
|
127
133
|
*/
|
|
128
134
|
export async function renderEmailTemplate(template, props, options = {}) {
|
|
129
135
|
await waitForEmailBuild();
|
|
@@ -13,6 +13,9 @@ const ICON_LINK = /<link\b[^>]*\srel=(["'])(?:[^"']*\s)?icon(?:\s[^"']*)?\1/i;
|
|
|
13
13
|
* through `useHead` (light and dark, one per `prefers-color-scheme`)
|
|
14
14
|
* competed with a placeholder carried by the template, and the browser could
|
|
15
15
|
* keep the empty one.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} headTags
|
|
18
|
+
* @returns {string}
|
|
16
19
|
*/
|
|
17
20
|
export function withFaviconPlaceholder(headTags) {
|
|
18
21
|
return ICON_LINK.test(headTags)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { isBoolean, isNumber, isRecord, isString } from "./type-guards.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @returns {value is import("./public-options.mjs").FrontendRegistry}
|
|
6
|
+
*/
|
|
7
|
+
function isFrontendRegistry(value) {
|
|
8
|
+
return (
|
|
9
|
+
isRecord(value) &&
|
|
10
|
+
Array.isArray(value.modules) &&
|
|
11
|
+
value.modules.every((module) => isRecord(module) && isString(module.id))
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The module registry the build wrote to `generated-frontend-modules.json`.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} content The file's JSON
|
|
19
|
+
* @returns {import("./public-options.mjs").FrontendRegistry}
|
|
20
|
+
* @throws {Error} When it is not a list of modules that have an id
|
|
21
|
+
*/
|
|
22
|
+
export function frontendRegistryOf(content) {
|
|
23
|
+
if (!isFrontendRegistry(content))
|
|
24
|
+
throw new Error(
|
|
25
|
+
"generated-frontend-modules.json is not a list of modules with an id",
|
|
26
|
+
);
|
|
27
|
+
return content;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {unknown} value
|
|
32
|
+
* @returns {value is Record<string, Record<string, unknown>>}
|
|
33
|
+
*/
|
|
34
|
+
function isObjectOfObjects(value) {
|
|
35
|
+
return (
|
|
36
|
+
isRecord(value) &&
|
|
37
|
+
!Array.isArray(value) &&
|
|
38
|
+
Object.values(value).every(
|
|
39
|
+
(child) => isRecord(child) && !Array.isArray(child),
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {unknown} value
|
|
46
|
+
* @returns {value is Record<string, string[]>}
|
|
47
|
+
*/
|
|
48
|
+
function isObjectOfPaths(value) {
|
|
49
|
+
return (
|
|
50
|
+
isRecord(value) &&
|
|
51
|
+
!Array.isArray(value) &&
|
|
52
|
+
Object.values(value).every(
|
|
53
|
+
(path) => Array.isArray(path) && path.every(isString),
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The server options the build wrote to `generated-server-options.json`: the
|
|
60
|
+
* private options by module id, and for each variable the path of the secret
|
|
61
|
+
* it stands for. A part the file leaves out is empty.
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} content The file's JSON
|
|
64
|
+
* @returns {{
|
|
65
|
+
* modules: Record<string, Record<string, unknown>>,
|
|
66
|
+
* environment: Record<string, string[]>
|
|
67
|
+
* }}
|
|
68
|
+
* @throws {Error} When a part is not what the build writes
|
|
69
|
+
*/
|
|
70
|
+
export function serverOptionsOf(content) {
|
|
71
|
+
const options =
|
|
72
|
+
isRecord(content) && !Array.isArray(content) ? content : undefined;
|
|
73
|
+
const modules = options ? (options.modules ?? {}) : undefined;
|
|
74
|
+
const environment = options ? (options.environment ?? {}) : undefined;
|
|
75
|
+
if (!isObjectOfObjects(modules) || !isObjectOfPaths(environment))
|
|
76
|
+
throw new Error(
|
|
77
|
+
"generated-server-options.json is not what the build writes",
|
|
78
|
+
);
|
|
79
|
+
return { modules, environment };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} value
|
|
84
|
+
* @returns {value is Record<string, unknown>}
|
|
85
|
+
*/
|
|
86
|
+
function isPlainRecord(value) {
|
|
87
|
+
return isRecord(value) && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {unknown} value
|
|
92
|
+
* @returns {value is string[]}
|
|
93
|
+
*/
|
|
94
|
+
function isTextList(value) {
|
|
95
|
+
return Array.isArray(value) && value.every(isString);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} value
|
|
100
|
+
* @returns {value is import("vite").ManifestChunk}
|
|
101
|
+
*/
|
|
102
|
+
function isManifestChunk(value) {
|
|
103
|
+
return (
|
|
104
|
+
isPlainRecord(value) &&
|
|
105
|
+
isString(value.file) &&
|
|
106
|
+
[value.css, value.assets, value.imports, value.dynamicImports].every(
|
|
107
|
+
(list) => list === undefined || isTextList(list),
|
|
108
|
+
) &&
|
|
109
|
+
[value.src, value.name].every(
|
|
110
|
+
(text) => text === undefined || isString(text),
|
|
111
|
+
) &&
|
|
112
|
+
[value.isEntry, value.isDynamicEntry].every(
|
|
113
|
+
(flag) => flag === undefined || isBoolean(flag),
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The manifest `vite build` wrote to `dist/client/.vite/manifest.json`.
|
|
120
|
+
*
|
|
121
|
+
* @param {unknown} content The file's JSON
|
|
122
|
+
* @returns {import("vite").Manifest}
|
|
123
|
+
* @throws {Error} When it is not an object of chunks
|
|
124
|
+
*/
|
|
125
|
+
export function viteManifestOf(content) {
|
|
126
|
+
if (!isPlainRecord(content) || !Object.values(content).every(isManifestChunk))
|
|
127
|
+
throw new Error("the client manifest is not an object of Vite chunks");
|
|
128
|
+
return content;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The members of an Iconify collection that `iconCollectionOf` checks, the
|
|
133
|
+
* ones an icon query and a server render read.
|
|
134
|
+
*
|
|
135
|
+
* @typedef {Pick<
|
|
136
|
+
* Parameters<typeof import("@iconify/utils").getIcons>[0],
|
|
137
|
+
* | "prefix"
|
|
138
|
+
* | "icons"
|
|
139
|
+
* | "aliases"
|
|
140
|
+
* | "lastModified"
|
|
141
|
+
* | "left"
|
|
142
|
+
* | "top"
|
|
143
|
+
* | "width"
|
|
144
|
+
* | "height"
|
|
145
|
+
* | "rotate"
|
|
146
|
+
* | "hFlip"
|
|
147
|
+
* | "vFlip"
|
|
148
|
+
* >} IconCollection
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {unknown} value An icon, an alias or the collection itself
|
|
153
|
+
* @returns {boolean} Whether its size and transformation members, all
|
|
154
|
+
* optional, have the types Iconify gives them
|
|
155
|
+
*/
|
|
156
|
+
function hasIconifyOptionals(value) {
|
|
157
|
+
return (
|
|
158
|
+
isPlainRecord(value) &&
|
|
159
|
+
[value.left, value.top, value.width, value.height, value.rotate].every(
|
|
160
|
+
(number) => number === undefined || isNumber(number),
|
|
161
|
+
) &&
|
|
162
|
+
[value.hFlip, value.vFlip, value.hidden].every(
|
|
163
|
+
(flag) => flag === undefined || isBoolean(flag),
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {unknown} value
|
|
170
|
+
* @param {"body" | "parent"} required The member each entry of the table names
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function isIconifyTable(value, required) {
|
|
174
|
+
return (
|
|
175
|
+
isPlainRecord(value) &&
|
|
176
|
+
Object.values(value).every(
|
|
177
|
+
(member) =>
|
|
178
|
+
hasIconifyOptionals(member) &&
|
|
179
|
+
isPlainRecord(member) &&
|
|
180
|
+
isString(member[required]),
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {unknown} value
|
|
187
|
+
* @returns {value is IconCollection}
|
|
188
|
+
*/
|
|
189
|
+
function isIconCollection(value) {
|
|
190
|
+
return (
|
|
191
|
+
hasIconifyOptionals(value) &&
|
|
192
|
+
isPlainRecord(value) &&
|
|
193
|
+
isString(value.prefix) &&
|
|
194
|
+
isIconifyTable(value.icons, "body") &&
|
|
195
|
+
(value.aliases === undefined || isIconifyTable(value.aliases, "parent")) &&
|
|
196
|
+
(value.lastModified === undefined || isNumber(value.lastModified))
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* An icon collection: the JSON file of an `@iconify-json/<prefix>` package.
|
|
202
|
+
*
|
|
203
|
+
* @param {unknown} content The file's JSON
|
|
204
|
+
* @returns {IconCollection}
|
|
205
|
+
* @throws {Error} When it is not an Iconify icon set
|
|
206
|
+
*/
|
|
207
|
+
export function iconCollectionOf(content) {
|
|
208
|
+
if (!isIconCollection(content))
|
|
209
|
+
throw new Error("the icon collection is not an Iconify icon set");
|
|
210
|
+
return content;
|
|
211
|
+
}
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { frontendRegistryOf } from "./generated-files.mjs";
|
|
4
5
|
import { publicOptionsOf } from "./public-options.mjs";
|
|
6
|
+
import { isRecord, isString } from "./type-guards.mjs";
|
|
5
7
|
|
|
6
8
|
const PROJECT_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
7
9
|
const REGISTRY_PATH = join(PROJECT_ROOT, "generated-frontend-modules.json");
|
|
8
|
-
const REGISTRY =
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const REGISTRY = frontendRegistryOf(
|
|
11
|
+
existsSync(REGISTRY_PATH)
|
|
12
|
+
? JSON.parse(readFileSync(REGISTRY_PATH, "utf8"))
|
|
13
|
+
: { modules: [] },
|
|
14
|
+
);
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {Readonly<Record<string, unknown>>} options
|
|
18
|
+
* @returns {string | undefined}
|
|
19
|
+
*/
|
|
12
20
|
function findHomepage(options) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
if (isString(options.homepage)) return options.homepage;
|
|
22
|
+
for (const child of Object.values(options)) {
|
|
23
|
+
const homepage = isRecord(child) ? findHomepage(child) : undefined;
|
|
24
|
+
if (homepage !== undefined) return homepage;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
const OPTIONS = publicOptionsOf(REGISTRY);
|
|
@@ -29,5 +38,8 @@ const OPTIONS = publicOptionsOf(REGISTRY);
|
|
|
29
38
|
*/
|
|
30
39
|
export const HOMEPAGE =
|
|
31
40
|
REGISTRY.modules
|
|
32
|
-
.map((module) =>
|
|
41
|
+
.map((module) => {
|
|
42
|
+
const options = OPTIONS[module.id];
|
|
43
|
+
return isRecord(options) ? findHomepage(options) : undefined;
|
|
44
|
+
})
|
|
33
45
|
.find(Boolean) ?? "/";
|
|
@@ -3,6 +3,10 @@ const HTML_ATTRIBUTE = /([^\s=]+)(?:="[^"]*")?/g;
|
|
|
3
3
|
/**
|
|
4
4
|
* The document's `<html>` tag: the template's attributes, overridden by those
|
|
5
5
|
* the render produced (`lang`, `data-scale`…), so none appears twice.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} templateAttributes
|
|
8
|
+
* @param {string} renderedAttributes
|
|
9
|
+
* @returns {string}
|
|
6
10
|
*/
|
|
7
11
|
export function htmlTag(templateAttributes, renderedAttributes) {
|
|
8
12
|
const rendered = new Set(
|