@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.
Files changed (116) hide show
  1. package/README.md +18 -24
  2. package/dist/commands/build-action.js +3 -2
  3. package/dist/commands/dev-action.js +2 -1
  4. package/dist/commands/prepare-action.js +1 -1
  5. package/dist/commands/start-action.js +3 -24
  6. package/dist/commands/workspace-task.js +2 -1
  7. package/dist/derived-outputs.js +3 -3
  8. package/dist/discovery.js +17 -6
  9. package/dist/entry-weight.js +15 -0
  10. package/dist/fs-sync.js +7 -4
  11. package/dist/index.js +2 -1
  12. package/dist/internal/built-workspace.d.ts +4 -0
  13. package/dist/internal/built-workspace.js +43 -0
  14. package/dist/internal/ensure.d.ts +1 -0
  15. package/dist/internal/ensure.js +8 -0
  16. package/dist/internal/locale-catalogs.d.ts +8 -3
  17. package/dist/internal/locale-catalogs.js +16 -6
  18. package/dist/internal/manifest-decoder.d.ts +7 -0
  19. package/dist/internal/manifest-decoder.js +81 -0
  20. package/dist/internal/own-release.d.ts +5 -0
  21. package/dist/internal/own-release.js +20 -0
  22. package/dist/internal/plain-object.d.ts +1 -0
  23. package/dist/internal/plain-object.js +6 -0
  24. package/dist/internal/text-list.d.ts +1 -0
  25. package/dist/internal/text-list.js +10 -0
  26. package/dist/internal/vite-manifest.d.ts +6 -0
  27. package/dist/internal/vite-manifest.js +24 -0
  28. package/dist/layer-watch.js +25 -24
  29. package/dist/layers.d.ts +1 -1
  30. package/dist/layers.js +3 -1
  31. package/dist/locale-files.d.ts +2 -1
  32. package/dist/locale-files.js +4 -0
  33. package/dist/manifest.d.ts +1 -1
  34. package/dist/manifest.js +44 -31
  35. package/dist/materialize.js +6 -2
  36. package/dist/module-set.js +2 -1
  37. package/dist/node-options.d.ts +1 -0
  38. package/dist/node-options.js +8 -0
  39. package/dist/output.d.ts +15 -9
  40. package/dist/output.js +30 -18
  41. package/dist/private-values.d.ts +1 -1
  42. package/dist/private-values.js +5 -3
  43. package/dist/project-modules.js +13 -8
  44. package/dist/renderer-range.js +5 -3
  45. package/dist/server-process.js +1 -1
  46. package/dist/verify-source-build.js +10 -2
  47. package/dist/verify-source-client-bundle.d.ts +1 -0
  48. package/dist/verify-source-client-bundle.js +30 -0
  49. package/dist/verify-source-runner.js +44 -60
  50. package/dist/verify-source-sources.js +49 -11
  51. package/dist/workspace-setup.js +1 -1
  52. package/package.json +6 -4
  53. package/skills/dms-frontend/SKILL.md +1 -1
  54. package/templates/vue/app-runtime.ts +2 -9
  55. package/templates/vue/client-server-guard.mjs +16 -0
  56. package/templates/vue/compress-assets.mjs +9 -1
  57. package/templates/vue/entry-weight.mjs +22 -0
  58. package/templates/vue/frontend-module.ts +1 -0
  59. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  60. package/templates/vue/frontend-runtime/state.ts +2 -2
  61. package/templates/vue/frontend-runtime/types.ts +8 -2
  62. package/templates/vue/head-order.mjs +7 -0
  63. package/templates/vue/icon-scan.mjs +4 -0
  64. package/templates/vue/server/auth/backend.mjs +41 -4
  65. package/templates/vue/server/auth/client-ip.mjs +52 -4
  66. package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
  67. package/templates/vue/server/auth/session.mjs +81 -2
  68. package/templates/vue/server/auth-hooks.mjs +15 -5
  69. package/templates/vue/server/backend-proxies.mjs +65 -9
  70. package/templates/vue/server/backend-request.mjs +61 -0
  71. package/templates/vue/server/backend-response.mjs +23 -6
  72. package/templates/vue/server/built-files.mjs +7 -0
  73. package/templates/vue/server/cli-report.mjs +53 -11
  74. package/templates/vue/server/client-manifest.mjs +23 -1
  75. package/templates/vue/server/content.mjs +18 -1
  76. package/templates/vue/server/dev-private-files.mjs +17 -1
  77. package/templates/vue/server/dev-styles.mjs +38 -0
  78. package/templates/vue/server/email.mjs +9 -3
  79. package/templates/vue/server/favicon.mjs +3 -0
  80. package/templates/vue/server/generated-files.mjs +211 -0
  81. package/templates/vue/server/homepage.mjs +21 -9
  82. package/templates/vue/server/html-tag.mjs +4 -0
  83. package/templates/vue/server/icons.mjs +80 -7
  84. package/templates/vue/server/in-place-render.mjs +35 -0
  85. package/templates/vue/server/inertia.mjs +61 -2
  86. package/templates/vue/server/module-event.mjs +98 -17
  87. package/templates/vue/server/module-route-types.mjs +78 -0
  88. package/templates/vue/server/module-routes.mjs +138 -31
  89. package/templates/vue/server/module-server.mjs +98 -27
  90. package/templates/vue/server/module-testing.mjs +14 -5
  91. package/templates/vue/server/page-props.mjs +92 -2
  92. package/templates/vue/server/page-redirects.mjs +37 -0
  93. package/templates/vue/server/path-patterns.mjs +13 -1
  94. package/templates/vue/server/proxy-compression.mjs +22 -0
  95. package/templates/vue/server/proxy-headers.mjs +11 -1
  96. package/templates/vue/server/proxy-redirects.mjs +20 -5
  97. package/templates/vue/server/proxy-request.mjs +57 -0
  98. package/templates/vue/server/proxy-transfers.mjs +33 -5
  99. package/templates/vue/server/public-endpoints.mjs +138 -23
  100. package/templates/vue/server/public-options.mjs +20 -6
  101. package/templates/vue/server/request-failure.mjs +23 -0
  102. package/templates/vue/server/request-routing.mjs +26 -9
  103. package/templates/vue/server/request-target.mjs +9 -3
  104. package/templates/vue/server/route-rules.mjs +39 -15
  105. package/templates/vue/server/ssr-module.mjs +32 -0
  106. package/templates/vue/server/static-files.mjs +24 -1
  107. package/templates/vue/server/type-guards.mjs +71 -0
  108. package/templates/vue/server/vite-errors.mjs +32 -5
  109. package/templates/vue/server/web-body.mjs +3 -0
  110. package/templates/vue/server-module.ts +10 -1
  111. package/templates/vue/server.mjs +132 -148
  112. package/templates/vue/sfc-global-types.mjs +112 -13
  113. package/templates/vue/ssr-renderer.ts +2 -2
  114. package/templates/vue/ui-inertia-page.mjs +16 -1
  115. package/templates/vue/ui-kbd.mjs +2 -1
  116. 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 = existsSync(REGISTRY_PATH)
9
- ? JSON.parse(readFileSync(REGISTRY_PATH, "utf8"))
10
- : { modules: [] };
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 (!options || typeof options !== "object") return undefined;
14
- if (typeof options.homepage === "string") return options.homepage;
15
- return Object.values(options)
16
- .map(findHomepage)
17
- .find((homepage) => homepage !== undefined);
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) => findHomepage(OPTIONS[module.id]))
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((error) =>
105
- console.error(
106
- "DMS icon collections not loaded; server renders draw bundled icons only",
107
- error,
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
- /** Whether the generated server answers `pathname` with `iconResponse`. */
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);