@antelopejs/dms-frontend 0.9.3 → 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/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 +2 -9
- 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/head-order.mjs +7 -0
- package/templates/vue/icon-scan.mjs +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 +2 -2
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- package/templates/vue/ui-request-state.mjs +2 -1
|
@@ -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(
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { getIcons } from "@iconify/utils";
|
|
6
6
|
import { ICON_API_PATH } from "../icon-api.mjs";
|
|
7
7
|
import { writeContent } from "./content.mjs";
|
|
8
|
+
import { iconCollectionOf } from "./generated-files.mjs";
|
|
8
9
|
|
|
9
10
|
const WORKSPACE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
10
11
|
const JSON_TYPE = "application/json";
|
|
@@ -24,14 +25,39 @@ const COLLECTION_PACKAGES = join("node_modules", "@iconify-json");
|
|
|
24
25
|
const ICON_CACHE_CONTROL =
|
|
25
26
|
"public, max-age=604800, stale-while-revalidate=86400";
|
|
26
27
|
const ERROR_CACHE_CONTROL = "no-store";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An Iconify icon collection: the JSON file of an `@iconify-json/<prefix>`
|
|
31
|
+
* package.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {import("./generated-files.mjs").IconCollection} IconCollection
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The answer to an icon query, to send.
|
|
38
|
+
*
|
|
39
|
+
* @typedef {{
|
|
40
|
+
* status: number,
|
|
41
|
+
* headers: Record<string, string>,
|
|
42
|
+
* body: string
|
|
43
|
+
* }} IconAnswer
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** @type {Map<string, Promise<IconCollection | undefined>>} */
|
|
27
47
|
const collections = new Map();
|
|
28
48
|
/**
|
|
29
49
|
* Kept by renderer module: a development server evaluates a new one after a
|
|
30
50
|
* layer changes, and each must find the collections loaded before it
|
|
31
51
|
* renders.
|
|
52
|
+
*
|
|
53
|
+
* @type {WeakMap<typeof import("../ssr-renderer.ts"), Promise<void>>}
|
|
32
54
|
*/
|
|
33
55
|
const rendererCollections = new WeakMap();
|
|
34
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} prefix
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
35
61
|
function collectionSpecifiers(prefix) {
|
|
36
62
|
return [
|
|
37
63
|
`@iconify-json/${prefix}/icons.json`,
|
|
@@ -42,6 +68,10 @@ function collectionSpecifiers(prefix) {
|
|
|
42
68
|
/**
|
|
43
69
|
* The file of an icon collection from the first source installed under its
|
|
44
70
|
* name; a source not installed is skipped and the next one tried.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} root
|
|
73
|
+
* @param {string} prefix
|
|
74
|
+
* @returns {string | undefined}
|
|
45
75
|
*/
|
|
46
76
|
function resolveCollectionFile(root, prefix) {
|
|
47
77
|
const require = createRequire(join(root, "package.json"));
|
|
@@ -53,15 +83,24 @@ function resolveCollectionFile(root, prefix) {
|
|
|
53
83
|
return undefined;
|
|
54
84
|
}
|
|
55
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} root
|
|
88
|
+
* @param {string} prefix
|
|
89
|
+
* @returns {Promise<IconCollection | undefined>}
|
|
90
|
+
*/
|
|
56
91
|
async function loadCollection(root, prefix) {
|
|
57
92
|
const file = resolveCollectionFile(root, prefix);
|
|
58
93
|
if (!file) return undefined;
|
|
59
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
94
|
+
return iconCollectionOf(JSON.parse(await readFile(file, "utf8")));
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
/**
|
|
63
98
|
* Parsed once per collection: a collection file is several megabytes, and
|
|
64
99
|
* one page asks for icons of the same few collections many times.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} root
|
|
102
|
+
* @param {string} prefix
|
|
103
|
+
* @returns {Promise<IconCollection | undefined>}
|
|
65
104
|
*/
|
|
66
105
|
function collection(root, prefix) {
|
|
67
106
|
const key = join(root, prefix);
|
|
@@ -77,6 +116,8 @@ function collection(root, prefix) {
|
|
|
77
116
|
* Every `@iconify-json/<prefix>` collection installed in the workspace under
|
|
78
117
|
* `root`, the ones the server render draws from. The workspace hoists every
|
|
79
118
|
* dependency, a module's collections included.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} root
|
|
80
121
|
*/
|
|
81
122
|
export async function installedCollections(root) {
|
|
82
123
|
const prefixes = await readdir(join(root, COLLECTION_PACKAGES)).catch(
|
|
@@ -94,6 +135,9 @@ export async function installedCollections(root) {
|
|
|
94
135
|
* Hand a server renderer the installed collections, once per renderer module,
|
|
95
136
|
* before its first render. A collection that fails to load leaves the render
|
|
96
137
|
* with the bundled icons only.
|
|
138
|
+
*
|
|
139
|
+
* @param {typeof import("../ssr-renderer.ts")} renderer
|
|
140
|
+
* @param {string} [root]
|
|
97
141
|
*/
|
|
98
142
|
export function provideIconCollections(renderer, root = WORKSPACE_ROOT) {
|
|
99
143
|
if (!rendererCollections.has(renderer))
|
|
@@ -101,16 +145,23 @@ export function provideIconCollections(renderer, root = WORKSPACE_ROOT) {
|
|
|
101
145
|
renderer,
|
|
102
146
|
installedCollections(root)
|
|
103
147
|
.then((loaded) => renderer.registerDmsIconCollections(loaded))
|
|
104
|
-
.catch(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
148
|
+
.catch(
|
|
149
|
+
/** @param {unknown} error */ (error) =>
|
|
150
|
+
console.error(
|
|
151
|
+
"DMS icon collections not loaded; server renders draw bundled icons only",
|
|
152
|
+
error,
|
|
153
|
+
),
|
|
109
154
|
),
|
|
110
155
|
);
|
|
111
156
|
return rendererCollections.get(renderer);
|
|
112
157
|
}
|
|
113
158
|
|
|
159
|
+
/**
|
|
160
|
+
* @param {number} status
|
|
161
|
+
* @param {string} cacheControl
|
|
162
|
+
* @param {IconCollection | null | { message: string }} payload
|
|
163
|
+
* @returns {IconAnswer}
|
|
164
|
+
*/
|
|
114
165
|
function jsonResponse(status, cacheControl, payload) {
|
|
115
166
|
return {
|
|
116
167
|
status,
|
|
@@ -119,10 +170,19 @@ function jsonResponse(status, cacheControl, payload) {
|
|
|
119
170
|
};
|
|
120
171
|
}
|
|
121
172
|
|
|
173
|
+
/**
|
|
174
|
+
* @param {number} status
|
|
175
|
+
* @param {string} message
|
|
176
|
+
* @returns {IconAnswer}
|
|
177
|
+
*/
|
|
122
178
|
function errorResponse(status, message) {
|
|
123
179
|
return jsonResponse(status, ERROR_CACHE_CONTROL, { message });
|
|
124
180
|
}
|
|
125
181
|
|
|
182
|
+
/**
|
|
183
|
+
* @param {URL} url
|
|
184
|
+
* @returns {string[] | undefined}
|
|
185
|
+
*/
|
|
126
186
|
function requestedIcons(url) {
|
|
127
187
|
const names = (url.searchParams.get("icons") ?? "")
|
|
128
188
|
.split(ICON_NAME_SEPARATOR)
|
|
@@ -132,7 +192,12 @@ function requestedIcons(url) {
|
|
|
132
192
|
return [...new Set(names)];
|
|
133
193
|
}
|
|
134
194
|
|
|
135
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Whether the generated server answers `pathname` with `iconResponse`.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} pathname
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
136
201
|
export function isIconRequest(pathname) {
|
|
137
202
|
return pathname.startsWith(ICON_API_PATH);
|
|
138
203
|
}
|
|
@@ -142,6 +207,11 @@ export function isIconRequest(pathname) {
|
|
|
142
207
|
* `root`: the subset of `@iconify-json/<prefix>` (or `@iconify/json`) holding
|
|
143
208
|
* the requested icons, and a `not_found` list for the others, as
|
|
144
209
|
* `api.iconify.design` does. Returns the status, headers and body to send.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} root
|
|
212
|
+
* @param {string | undefined} method
|
|
213
|
+
* @param {URL} url
|
|
214
|
+
* @returns {Promise<IconAnswer>}
|
|
145
215
|
*/
|
|
146
216
|
export async function iconResponse(root, method, url) {
|
|
147
217
|
if (!READ_METHODS.has(method)) {
|
|
@@ -163,6 +233,9 @@ export async function iconResponse(root, method, url) {
|
|
|
163
233
|
/**
|
|
164
234
|
* Serve an Iconify API query from the collections installed in the workspace,
|
|
165
235
|
* the same way under `ajs dms dev` and `ajs dms start`: both run this server.
|
|
236
|
+
*
|
|
237
|
+
* @param {import("node:http").IncomingMessage} request
|
|
238
|
+
* @param {import("node:http").ServerResponse} response
|
|
166
239
|
*/
|
|
167
240
|
export async function handleIcons(request, response) {
|
|
168
241
|
const url = new URL(request.url, "http://frontend.local");
|
|
@@ -9,6 +9,11 @@ const IN_PLACE_RENDERS = 2;
|
|
|
9
9
|
/**
|
|
10
10
|
* The key of a backend read a later render of the same request may reuse: a
|
|
11
11
|
* plain JSON `GET`, without hooks that would expect to see the response.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} path
|
|
14
|
+
* @param {import("ofetch").FetchOptions} options
|
|
15
|
+
* @param {Headers} headers
|
|
16
|
+
* @returns {string | undefined}
|
|
12
17
|
*/
|
|
13
18
|
function reusableReadKey(path, options, headers) {
|
|
14
19
|
const method = (options.method ?? "GET").toUpperCase();
|
|
@@ -23,6 +28,13 @@ function reusableReadKey(path, options, headers) {
|
|
|
23
28
|
]);
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The reads of a request's server renders, with the switch that lets a later
|
|
33
|
+
* render reuse the completed ones.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {((path: string, options?: import("ofetch").FetchOptions) => Promise<unknown>) & { reuseCompletedReads: () => void }} ServerReads
|
|
36
|
+
*/
|
|
37
|
+
|
|
26
38
|
/**
|
|
27
39
|
* The backend reads of a request's server renders, sent with the request's
|
|
28
40
|
* `backendHeaders()` and the read's own. After `reuseCompletedReads()`, a later
|
|
@@ -31,10 +43,18 @@ function reusableReadKey(path, options, headers) {
|
|
|
31
43
|
*
|
|
32
44
|
* A read returns a copy: the render that read it may still change the object it
|
|
33
45
|
* got. A body that cannot be copied is read again.
|
|
46
|
+
*
|
|
47
|
+
* @param {() => HeadersInit} backendHeaders
|
|
48
|
+
* @returns {ServerReads}
|
|
34
49
|
*/
|
|
35
50
|
export function serverReads(backendHeaders) {
|
|
51
|
+
/** @type {Map<string, unknown>} */
|
|
36
52
|
const completed = new Map();
|
|
37
53
|
let reuse = false;
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} path
|
|
56
|
+
* @param {import("ofetch").FetchOptions} [options]
|
|
57
|
+
*/
|
|
38
58
|
const read = (path, options = {}) => {
|
|
39
59
|
const headers = new Headers(backendHeaders());
|
|
40
60
|
new Headers(options.headers).forEach((value, name) => {
|
|
@@ -43,6 +63,7 @@ export function serverReads(backendHeaders) {
|
|
|
43
63
|
const key = reusableReadKey(path, options, headers);
|
|
44
64
|
if (reuse && key && completed.has(key))
|
|
45
65
|
return Promise.resolve(structuredClone(completed.get(key)));
|
|
66
|
+
/** @type {Promise<unknown>} */
|
|
46
67
|
const reading = ofetch(path, {
|
|
47
68
|
...options,
|
|
48
69
|
baseURL: process.env.DMS_API_BASE_URL,
|
|
@@ -67,6 +88,11 @@ export function serverReads(backendHeaders) {
|
|
|
67
88
|
* Whether a document request renders `location` in place of redirecting to
|
|
68
89
|
* it: a `GET` whose page only completes its query (`?workspace=`), on the
|
|
69
90
|
* same path. Any other redirect keeps its own response.
|
|
91
|
+
*
|
|
92
|
+
* @param {import("node:http").IncomingMessage} request
|
|
93
|
+
* @param {string | undefined} from
|
|
94
|
+
* @param {string} location
|
|
95
|
+
* @returns {boolean}
|
|
70
96
|
*/
|
|
71
97
|
function rendersInPlace(request, from, location) {
|
|
72
98
|
if (request.method !== "GET" || location.includes("#")) return false;
|
|
@@ -87,6 +113,15 @@ function rendersInPlace(request, from, location) {
|
|
|
87
113
|
*
|
|
88
114
|
* A page that completes its query as it renders is rendered at that query in
|
|
89
115
|
* the same request, rather than answered with a redirect.
|
|
116
|
+
*
|
|
117
|
+
* @template {{ url: string | undefined, props: unknown }} Page
|
|
118
|
+
* @template {{ redirect?: string }} Rendered
|
|
119
|
+
* @param {import("node:http").IncomingMessage} request
|
|
120
|
+
* @param {Page} page
|
|
121
|
+
* @param {ServerReads} fetch
|
|
122
|
+
* @param {(page: Page) => Promise<Rendered>} render
|
|
123
|
+
* @param {(props: Page["props"], url: URL) => Promise<Page["props"]>} propsAt
|
|
124
|
+
* @returns {Promise<{ rendered: Rendered, served: Page }>}
|
|
90
125
|
*/
|
|
91
126
|
export async function renderInPlace(request, page, fetch, render, propsAt) {
|
|
92
127
|
let rendered = await render(page);
|