@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.
Files changed (123) 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/config.js +3 -0
  8. package/dist/derived-outputs.js +3 -3
  9. package/dist/discovery.js +17 -6
  10. package/dist/entry-weight.js +15 -0
  11. package/dist/fs-sync.js +7 -4
  12. package/dist/index.js +2 -1
  13. package/dist/internal/built-workspace.d.ts +4 -0
  14. package/dist/internal/built-workspace.js +43 -0
  15. package/dist/internal/ensure.d.ts +1 -0
  16. package/dist/internal/ensure.js +8 -0
  17. package/dist/internal/locale-catalogs.d.ts +8 -3
  18. package/dist/internal/locale-catalogs.js +16 -6
  19. package/dist/internal/manifest-decoder.d.ts +7 -0
  20. package/dist/internal/manifest-decoder.js +81 -0
  21. package/dist/internal/own-release.d.ts +5 -0
  22. package/dist/internal/own-release.js +20 -0
  23. package/dist/internal/plain-object.d.ts +1 -0
  24. package/dist/internal/plain-object.js +6 -0
  25. package/dist/internal/text-list.d.ts +1 -0
  26. package/dist/internal/text-list.js +10 -0
  27. package/dist/internal/vite-manifest.d.ts +6 -0
  28. package/dist/internal/vite-manifest.js +24 -0
  29. package/dist/layer-watch.js +25 -24
  30. package/dist/layers.d.ts +1 -1
  31. package/dist/layers.js +3 -1
  32. package/dist/locale-files.d.ts +2 -1
  33. package/dist/locale-files.js +4 -0
  34. package/dist/manifest.d.ts +1 -1
  35. package/dist/manifest.js +44 -31
  36. package/dist/materialize.js +6 -2
  37. package/dist/module-set.js +2 -1
  38. package/dist/node-options.d.ts +1 -0
  39. package/dist/node-options.js +8 -0
  40. package/dist/output.d.ts +15 -9
  41. package/dist/output.js +30 -18
  42. package/dist/private-values.d.ts +1 -1
  43. package/dist/private-values.js +5 -3
  44. package/dist/project-modules.js +13 -8
  45. package/dist/renderer-range.js +5 -3
  46. package/dist/server-process.js +1 -1
  47. package/dist/verify-source-build.js +10 -2
  48. package/dist/verify-source-client-bundle.d.ts +1 -0
  49. package/dist/verify-source-client-bundle.js +30 -0
  50. package/dist/verify-source-runner.js +44 -60
  51. package/dist/verify-source-sources.js +49 -11
  52. package/dist/workspace-setup.js +1 -1
  53. package/package.json +6 -4
  54. package/skills/dms-frontend/SKILL.md +1 -1
  55. package/templates/vue/app-runtime.ts +90 -16
  56. package/templates/vue/client-server-guard.mjs +16 -0
  57. package/templates/vue/compress-assets.mjs +9 -1
  58. package/templates/vue/entry-weight.mjs +22 -0
  59. package/templates/vue/frontend-module.ts +1 -0
  60. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  61. package/templates/vue/frontend-runtime/state.ts +2 -2
  62. package/templates/vue/frontend-runtime/types.ts +8 -2
  63. package/templates/vue/globals.d.ts +5 -0
  64. package/templates/vue/head-order.mjs +7 -0
  65. package/templates/vue/icon-bundle.ts +31 -0
  66. package/templates/vue/icon-scan.mjs +4 -0
  67. package/templates/vue/message-subset.ts +65 -0
  68. package/templates/vue/network-error.ts +4 -0
  69. package/templates/vue/server/auth/backend.mjs +41 -4
  70. package/templates/vue/server/auth/client-ip.mjs +52 -4
  71. package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
  72. package/templates/vue/server/auth/session.mjs +81 -2
  73. package/templates/vue/server/auth-hooks.mjs +15 -5
  74. package/templates/vue/server/backend-proxies.mjs +65 -9
  75. package/templates/vue/server/backend-request.mjs +61 -0
  76. package/templates/vue/server/backend-response.mjs +23 -6
  77. package/templates/vue/server/built-files.mjs +7 -0
  78. package/templates/vue/server/cli-report.mjs +53 -11
  79. package/templates/vue/server/client-manifest.mjs +23 -1
  80. package/templates/vue/server/content.mjs +18 -1
  81. package/templates/vue/server/dev-private-files.mjs +17 -1
  82. package/templates/vue/server/dev-styles.mjs +38 -0
  83. package/templates/vue/server/email.mjs +9 -3
  84. package/templates/vue/server/favicon.mjs +3 -0
  85. package/templates/vue/server/generated-files.mjs +211 -0
  86. package/templates/vue/server/homepage.mjs +21 -9
  87. package/templates/vue/server/html-tag.mjs +4 -0
  88. package/templates/vue/server/icons.mjs +80 -7
  89. package/templates/vue/server/in-place-render.mjs +35 -0
  90. package/templates/vue/server/inertia.mjs +61 -2
  91. package/templates/vue/server/module-event.mjs +98 -17
  92. package/templates/vue/server/module-route-types.mjs +78 -0
  93. package/templates/vue/server/module-routes.mjs +138 -31
  94. package/templates/vue/server/module-server.mjs +98 -27
  95. package/templates/vue/server/module-testing.mjs +14 -5
  96. package/templates/vue/server/page-props.mjs +92 -2
  97. package/templates/vue/server/page-redirects.mjs +37 -0
  98. package/templates/vue/server/path-patterns.mjs +13 -1
  99. package/templates/vue/server/proxy-compression.mjs +22 -0
  100. package/templates/vue/server/proxy-headers.mjs +11 -1
  101. package/templates/vue/server/proxy-redirects.mjs +20 -5
  102. package/templates/vue/server/proxy-request.mjs +57 -0
  103. package/templates/vue/server/proxy-transfers.mjs +33 -5
  104. package/templates/vue/server/public-endpoints.mjs +138 -23
  105. package/templates/vue/server/public-options.mjs +20 -6
  106. package/templates/vue/server/request-failure.mjs +23 -0
  107. package/templates/vue/server/request-routing.mjs +26 -9
  108. package/templates/vue/server/request-target.mjs +9 -3
  109. package/templates/vue/server/route-rules.mjs +39 -15
  110. package/templates/vue/server/ssr-module.mjs +32 -0
  111. package/templates/vue/server/static-files.mjs +24 -1
  112. package/templates/vue/server/type-guards.mjs +71 -0
  113. package/templates/vue/server/vite-errors.mjs +32 -5
  114. package/templates/vue/server/web-body.mjs +3 -0
  115. package/templates/vue/server-module.ts +10 -1
  116. package/templates/vue/server.mjs +132 -148
  117. package/templates/vue/sfc-global-types.mjs +112 -13
  118. package/templates/vue/ssr-renderer.ts +12 -3
  119. package/templates/vue/ui-inertia-page.mjs +16 -1
  120. package/templates/vue/ui-kbd.mjs +2 -1
  121. package/templates/vue/ui-lazy-icons.mjs +44 -0
  122. package/templates/vue/ui-request-state.mjs +2 -1
  123. package/templates/vue/vite.config.ts +2 -0
@@ -1,6 +1,10 @@
1
1
  /** The statuses that send the client to their `Location`. */
2
2
  export const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
3
3
 
4
+ /**
5
+ * @param {string | URL | undefined} url
6
+ * @returns {string | undefined}
7
+ */
4
8
  function originOf(url) {
5
9
  try {
6
10
  return new URL(url).origin;
@@ -9,7 +13,13 @@ function originOf(url) {
9
13
  }
10
14
  }
11
15
 
12
- /** Whether `url` is on the backend's origin. */
16
+ /**
17
+ * Whether `url` is on the backend's origin.
18
+ *
19
+ * @param {string} url
20
+ * @param {string} [backendBaseUrl]
21
+ * @returns {boolean}
22
+ */
13
23
  export function isBackendUrl(
14
24
  url,
15
25
  backendBaseUrl = process.env.DMS_API_BASE_URL,
@@ -34,16 +44,18 @@ export function isBackendUrl(
34
44
  *
35
45
  * `//host` would name another host, so `/.` keeps the location a path.
36
46
  *
37
- * @param location The backend's `Location`
38
- * @param fetchedUrl The backend URL the answer came from
39
- * @param isProxiedPath `(pathname) => boolean`, whether the frontend server
40
- * sends that path to the backend
47
+ * @param {string} location The backend's `Location`
48
+ * @param {string | URL} fetchedUrl The backend URL the answer came from
49
+ * @param {(pathname: string) => boolean} [isProxiedPath] Whether the frontend
50
+ * server sends that path to the backend
51
+ * @returns {string}
41
52
  */
42
53
  export function relayedLocation(
43
54
  location,
44
55
  fetchedUrl,
45
56
  isProxiedPath = () => false,
46
57
  ) {
58
+ /** @type {URL} */
47
59
  let target;
48
60
  try {
49
61
  target = new URL(location, fetchedUrl);
@@ -60,6 +72,9 @@ export function relayedLocation(
60
72
  * The cookies of a `Response` a module route returns. A fetch that followed a
61
73
  * redirect to another site would set that site's cookies on the frontend's
62
74
  * origin: those are dropped.
75
+ *
76
+ * @param {Response} answer
77
+ * @returns {string[]}
63
78
  */
64
79
  export function relayableCookies(answer) {
65
80
  if (answer.redirected && !isBackendUrl(answer.url)) return [];
@@ -0,0 +1,57 @@
1
+ import { backendHeaders } from "./backend-request.mjs";
2
+ import { PLAIN_ANSWER_HEADERS, relayAnswer } from "./proxy-compression.mjs";
3
+ import { sendWithinLimit } from "./proxy-transfers.mjs";
4
+ import { proxyAccess, refuseCrossOrigin } from "./public-endpoints.mjs";
5
+ import { backendUrl } from "./request-target.mjs";
6
+
7
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
8
+
9
+ /**
10
+ * Sends a request to the backend through the proxy it reached.
11
+ * `isProxiedPath` says which backend paths a relayed `Location` may name on
12
+ * the frontend's origin.
13
+ *
14
+ * A client that left while the request was routed closed the response already,
15
+ * and would never abort the request below.
16
+ *
17
+ * Cancelling the body alone misses a client that leaves before the backend
18
+ * answers: its stream, an SSE feed above all, then stayed open for good.
19
+ *
20
+ * A backend redirect is the browser's to follow: Media sends a file's link to
21
+ * the storage's signed URL, on the public origin the server itself may not
22
+ * reach, which turned every download into a 502.
23
+ *
24
+ * @param {import("node:http").IncomingMessage} request
25
+ * @param {import("node:http").ServerResponse} response
26
+ * @param {import("./backend-proxies.mjs").BackendProxy} backendProxy
27
+ * @param {(path: string) => boolean} isProxiedPath
28
+ * @returns {Promise<true | undefined>}
29
+ */
30
+ export async function proxy(request, response, backendProxy, isProxiedPath) {
31
+ const access = proxyAccess(request, SAFE_METHODS, backendProxy);
32
+ if (access.isRefused) {
33
+ refuseCrossOrigin(response);
34
+ return true;
35
+ }
36
+ if (response.closed) return;
37
+ const abandoned = new AbortController();
38
+ response.once("close", () => abandoned.abort());
39
+ const init = {
40
+ signal: abandoned.signal,
41
+ method: request.method,
42
+ headers: { ...backendHeaders(request, access), ...PLAIN_ANSWER_HEADERS },
43
+ duplex: "half",
44
+ redirect: "manual",
45
+ };
46
+ const upstream = await sendWithinLimit(
47
+ request,
48
+ response,
49
+ backendProxy.maxBodyBytes,
50
+ (body) => fetch(backendUrl(request.url), { ...init, body }),
51
+ );
52
+ if (!upstream) return true;
53
+ await relayAnswer(request, response, upstream, {
54
+ fetched: true,
55
+ isProxiedPath,
56
+ });
57
+ }
@@ -26,6 +26,11 @@ export const SERVER_OPTIONS = Object.freeze({
26
26
 
27
27
  class BodyTooLargeError extends Error {}
28
28
 
29
+ /**
30
+ * @param {import("node:http").IncomingMessage} request
31
+ * @param {number} maxBodyBytes
32
+ * @returns {AsyncGenerator<Buffer>}
33
+ */
29
34
  async function* limitedChunks(request, maxBodyBytes) {
30
35
  let received = 0;
31
36
  for await (const chunk of request.iterator(BODY_ITERATION)) {
@@ -35,6 +40,10 @@ async function* limitedChunks(request, maxBodyBytes) {
35
40
  }
36
41
  }
37
42
 
43
+ /**
44
+ * @param {import("node:http").IncomingMessage} request
45
+ * @param {number | undefined} maxBodyBytes
46
+ */
38
47
  function requestBody(request, maxBodyBytes) {
39
48
  if (BODILESS_METHODS.has(request.method)) return undefined;
40
49
  return maxBodyBytes === undefined
@@ -42,6 +51,11 @@ function requestBody(request, maxBodyBytes) {
42
51
  : limitedChunks(request, maxBodyBytes);
43
52
  }
44
53
 
54
+ /**
55
+ * @param {import("node:http").IncomingMessage} request
56
+ * @param {number | undefined} maxBodyBytes
57
+ * @returns {boolean}
58
+ */
45
59
  function declaresTooLargeBody(request, maxBodyBytes) {
46
60
  return (
47
61
  maxBodyBytes !== undefined &&
@@ -49,6 +63,10 @@ function declaresTooLargeBody(request, maxBodyBytes) {
49
63
  );
50
64
  }
51
65
 
66
+ /**
67
+ * @param {unknown} error
68
+ * @returns {boolean}
69
+ */
52
70
  function isBodyTooLarge(error) {
53
71
  return (
54
72
  error instanceof BodyTooLargeError ||
@@ -65,6 +83,9 @@ function isBodyTooLarge(error) {
65
83
  * the browser's upload failing without a reason. The rest of the body is read
66
84
  * and dropped instead (RFC 9112, section 9.6, "tear-down"), for `LINGER_MS`
67
85
  * at most; the response ends with it and the connection then closes cleanly.
86
+ *
87
+ * @param {import("node:http").IncomingMessage} request
88
+ * @param {import("node:http").ServerResponse} response
68
89
  */
69
90
  function refuseTooLargeBody(request, response) {
70
91
  response.writeHead(PAYLOAD_TOO_LARGE, {
@@ -96,11 +117,18 @@ function refuseTooLargeBody(request, response) {
96
117
  * before a byte is passed on, a chunked body is cut off as soon as it goes
97
118
  * over.
98
119
  *
99
- * @param send `(body) => Promise<Response>`, the backend request with the
100
- * body to stream: none for a GET or a HEAD, the request itself, or its
101
- * bytes cut off past the limit
102
- * @returns The backend's answer, or `undefined` once the body went over and
103
- * `response` got the 413
120
+ * @param {import("node:http").IncomingMessage} request
121
+ * @param {import("node:http").ServerResponse} response
122
+ * @param {number | undefined} maxBodyBytes
123
+ * @param {(
124
+ * body: import("node:http").IncomingMessage | AsyncGenerator<Buffer> | undefined
125
+ * ) => Promise<Response>} send The backend request with the body to stream:
126
+ * none for a GET or a HEAD, the request itself, or its bytes cut off past
127
+ * the limit
128
+ * @returns {Promise<Response | import("node:http").ServerResponse | undefined>}
129
+ * The backend's answer, or once the body went over and `response` got the
130
+ * 413: `undefined`, or `response` itself when the request had been read
131
+ * already (`response.end()`'s own return)
104
132
  */
105
133
  export async function sendWithinLimit(request, response, maxBodyBytes, send) {
106
134
  if (declaresTooLargeBody(request, maxBodyBytes))
@@ -1,6 +1,33 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { json } from "./auth/backend.mjs";
3
3
  import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./auth/client-ip.mjs";
4
+ import { PROXY_AUTH_MODES } from "./backend-proxies.mjs";
5
+ import {
6
+ isNonNullObject,
7
+ isNumber,
8
+ isObjectLike,
9
+ isRecord,
10
+ isString,
11
+ } from "./type-guards.mjs";
12
+
13
+ /**
14
+ * A public endpoint, compiled from a declaration that passed every check.
15
+ *
16
+ * @typedef {object} PublicEndpoint
17
+ * @property {string} path
18
+ * @property {string[]} methods
19
+ * @property {string[]} headers
20
+ * @property {RegExp} matcher
21
+ */
22
+
23
+ /**
24
+ * A declaration as the manifest or the environment holds it, before any check.
25
+ *
26
+ * @typedef {object} PublicEndpointDeclaration
27
+ * @property {unknown} [path]
28
+ * @property {unknown} [methods]
29
+ * @property {unknown} [headers]
30
+ */
4
31
 
5
32
  /**
6
33
  * Written at the workspace root by the loader, from the endpoints each
@@ -41,39 +68,101 @@ const HEADER_NAME = /^x-[a-z0-9-]+$/;
41
68
  const RESERVED_HEADER =
42
69
  /^x-(?:forwarded-|real-ip$|dms-|inertia|http-method|method-override|original-|rewrite-)/;
43
70
 
71
+ /** @type {unknown[] | undefined} */
44
72
  let declaredEndpoints;
73
+ /** @type {import("./backend-proxies.mjs").ManifestProxy[] | undefined} */
45
74
  let declaredProxies;
46
75
 
47
76
  /**
48
77
  * The public endpoints the modules of this deployment declared, unchecked.
49
78
  *
50
- * @param file Generated file to read
51
- * @returns The declared entries, or none when the file is absent or broken
79
+ * @param {URL | string} [file] Generated file to read
80
+ * @returns {unknown[]} The declared entries, or none when the file is absent or broken
52
81
  */
53
82
  export function readDeclaredPublicEndpoints(file = DECLARED_ENDPOINTS_FILE) {
54
83
  try {
84
+ /** @type {unknown} */
55
85
  const parsed = JSON.parse(readFileSync(file, "utf8"));
56
- return Array.isArray(parsed?.endpoints) ? parsed.endpoints : [];
86
+ const endpoints = isObjectLike(parsed) ? parsed.endpoints : undefined;
87
+ return Array.isArray(endpoints) ? endpoints : [];
57
88
  } catch {
58
89
  return [];
59
90
  }
60
91
  }
61
92
 
93
+ /**
94
+ * Whether `entry` names a path and, when it says so, an auth mode and a body
95
+ * limit of the right type. The path pattern and the limit's range are judged
96
+ * when the proxies are resolved.
97
+ *
98
+ * @param {unknown} entry
99
+ * @returns {entry is { path: string, auth?: "session" | "none", maxBodyBytes?: number }}
100
+ */
101
+ function isProxyDeclaration(entry) {
102
+ return (
103
+ isRecord(entry) &&
104
+ !Array.isArray(entry) &&
105
+ isString(entry.path) &&
106
+ (entry.auth === undefined ||
107
+ PROXY_AUTH_MODES.some((mode) => mode === entry.auth)) &&
108
+ (entry.maxBodyBytes === undefined || isNumber(entry.maxBodyBytes))
109
+ );
110
+ }
111
+
112
+ /**
113
+ * @param {unknown[]} entries
114
+ * @param {string} source Where they were declared, for the warning
115
+ * @param {(entry: unknown) => boolean} isUsable
116
+ */
117
+ function warnUnusableProxies(entries, source, isUsable) {
118
+ const unusable = entries.filter((entry) => !isUsable(entry)).length;
119
+ if (unusable)
120
+ console.warn(
121
+ `Ignoring ${unusable} ${unusable === 1 ? "entry" : "entries"} of ${source}: not a backend proxy declaration (a path, an auth of session or none, a body limit in bytes)`,
122
+ );
123
+ }
124
+
125
+ /**
126
+ * @param {unknown} entry
127
+ * @returns {entry is import("./backend-proxies.mjs").ManifestProxy}
128
+ */
129
+ function isDeclaredProxy(entry) {
130
+ return (
131
+ isRecord(entry) &&
132
+ isProxyDeclaration(entry) &&
133
+ (entry.module === undefined || isString(entry.module))
134
+ );
135
+ }
136
+
62
137
  /**
63
138
  * The backend proxies the backend modules declared in the manifest, as
64
- * `{ module, path, auth?, maxBodyBytes? }`, unchecked.
139
+ * `{ module, path, auth?, maxBodyBytes? }`. An entry that is not one is
140
+ * reported and left out.
65
141
  *
66
142
  * A missing or broken file declares no proxy.
67
143
  *
68
- * @param file Generated file to read
144
+ * @param {URL | string} [file] Generated file to read
145
+ * @returns {import("./backend-proxies.mjs").ManifestProxy[]}
69
146
  */
70
147
  export function readDeclaredBackendProxies(file = DECLARED_ENDPOINTS_FILE) {
71
148
  if (file === DECLARED_ENDPOINTS_FILE && declaredProxies)
72
149
  return declaredProxies;
150
+ /** @type {import("./backend-proxies.mjs").ManifestProxy[]} */
73
151
  let proxies = [];
74
152
  try {
153
+ /** @type {unknown} */
75
154
  const parsed = JSON.parse(readFileSync(file, "utf8"));
76
- if (Array.isArray(parsed?.proxies)) proxies = parsed.proxies;
155
+ const listed = isObjectLike(parsed) ? parsed.proxies : undefined;
156
+ if (Array.isArray(listed)) {
157
+ /** @type {unknown[]} */
158
+ const entries = listed;
159
+ warnUnusableProxies(
160
+ entries,
161
+ "the generated manifest file",
162
+ isDeclaredProxy,
163
+ );
164
+ proxies = entries.filter(isDeclaredProxy);
165
+ }
77
166
  } catch {}
78
167
  if (file === DECLARED_ENDPOINTS_FILE) declaredProxies = proxies;
79
168
  return proxies;
@@ -82,29 +171,40 @@ export function readDeclaredBackendProxies(file = DECLARED_ENDPOINTS_FILE) {
82
171
  /**
83
172
  * The backend proxies `DMS_BACKEND_PROXIES` adds: a JSON array of
84
173
  * `{ path, auth?, maxBodyBytes? }`, as the manifest's `backendProxies`, for an
85
- * operator. A value that is not a JSON array is reported and ignored.
174
+ * operator. A value that is not a JSON array is reported and ignored, and so
175
+ * is each entry that is not a declaration.
86
176
  *
87
- * @param declaration The variable's value
177
+ * @param {string} [declaration] The variable's value
178
+ * @returns {import("./backend-proxies.mjs").ManifestProxy[]}
88
179
  */
89
180
  export function environmentBackendProxies(
90
181
  declaration = process.env.DMS_BACKEND_PROXIES,
91
182
  ) {
92
183
  if (!declaration?.trim()) return [];
93
184
  try {
185
+ /** @type {unknown} */
94
186
  const parsed = JSON.parse(declaration);
95
- if (Array.isArray(parsed))
96
- return parsed.map((entry) => ({
97
- ...entry,
98
- module: "DMS_BACKEND_PROXIES",
99
- }));
187
+ if (Array.isArray(parsed)) {
188
+ /** @type {unknown[]} */
189
+ const entries = parsed;
190
+ warnUnusableProxies(entries, "DMS_BACKEND_PROXIES", isProxyDeclaration);
191
+ return entries
192
+ .filter(isProxyDeclaration)
193
+ .map((entry) => ({ ...entry, module: "DMS_BACKEND_PROXIES" }));
194
+ }
100
195
  } catch {}
101
196
  console.warn("Ignoring DMS_BACKEND_PROXIES: not a JSON array");
102
197
  return [];
103
198
  }
104
199
 
200
+ /**
201
+ * @param {string | undefined} declaration
202
+ * @returns {unknown[]}
203
+ */
105
204
  function environmentEndpoints(declaration) {
106
205
  if (!declaration?.trim()) return [];
107
206
  try {
207
+ /** @type {unknown} */
108
208
  const parsed = JSON.parse(declaration);
109
209
  return Array.isArray(parsed) ? parsed : [];
110
210
  } catch {
@@ -113,6 +213,7 @@ function environmentEndpoints(declaration) {
113
213
  }
114
214
  }
115
215
 
216
+ /** @param {unknown} methods */
116
217
  function normalizedMethods(methods) {
117
218
  if (methods === undefined) return DEFAULT_METHODS;
118
219
  if (!Array.isArray(methods) || methods.length === 0) return undefined;
@@ -122,6 +223,7 @@ function normalizedMethods(methods) {
122
223
  : undefined;
123
224
  }
124
225
 
226
+ /** @param {unknown} headers */
125
227
  function normalizedHeaders(headers) {
126
228
  if (headers === undefined) return [];
127
229
  if (!Array.isArray(headers)) return undefined;
@@ -133,6 +235,7 @@ function normalizedHeaders(headers) {
133
235
  : undefined;
134
236
  }
135
237
 
238
+ /** @param {string} path */
136
239
  function pathMatcher(path) {
137
240
  const pattern = path
138
241
  .split("/")
@@ -148,11 +251,13 @@ function pathMatcher(path) {
148
251
  /**
149
252
  * A declaration made usable, or `undefined` when any part of it is malformed:
150
253
  * a bad entry is dropped whole, never widened to what it might have meant.
254
+ *
255
+ * @param {PublicEndpointDeclaration | null | undefined} entry
256
+ * @returns {PublicEndpoint | undefined}
151
257
  */
152
258
  export function compilePublicEndpoint(entry) {
153
- if (typeof entry !== "object" || entry === null) return undefined;
154
- if (typeof entry.path !== "string" || !PUBLIC_PATH.test(entry.path))
155
- return undefined;
259
+ if (!isNonNullObject(entry)) return undefined;
260
+ if (!isString(entry.path) || !PUBLIC_PATH.test(entry.path)) return undefined;
156
261
  const methods = normalizedMethods(entry.methods);
157
262
  const headers = normalizedHeaders(entry.headers);
158
263
  if (!methods || !headers) return undefined;
@@ -168,6 +273,10 @@ export function compilePublicEndpoint(entry) {
168
273
  * The backend endpoints this deployment serves to anyone: what the backend's
169
274
  * modules declared, plus what `DMS_PUBLIC_ENDPOINTS` adds (a JSON array of the
170
275
  * same `{ path, methods?, headers? }` entries).
276
+ *
277
+ * @param {string} [declaration]
278
+ * @param {unknown[]} [declared]
279
+ * @returns {PublicEndpoint[]}
171
280
  */
172
281
  export function allowedPublicEndpoints(
173
282
  declaration = process.env.DMS_PUBLIC_ENDPOINTS,
@@ -189,6 +298,8 @@ export function allowedPublicEndpoints(
189
298
  /**
190
299
  * Compiled once per value of the environment variable, so a malformed entry
191
300
  * is reported once rather than on every request.
301
+ *
302
+ * @type {{ declaration: string | undefined, endpoints: PublicEndpoint[] } | undefined}
192
303
  */
193
304
  let compiled;
194
305
 
@@ -202,9 +313,9 @@ function currentPublicEndpoints() {
202
313
  /**
203
314
  * The public endpoint a request reaches, if any.
204
315
  *
205
- * @param method The request's method
206
- * @param pathname The request's normalized path, without its query string
207
- * @param endpoints The endpoints to match, this deployment's by default
316
+ * @param {string} method The request's method
317
+ * @param {string} pathname The request's normalized path, without its query string
318
+ * @param {PublicEndpoint[]} [endpoints] The endpoints to match, this deployment's by default
208
319
  */
209
320
  export function matchPublicEndpoint(
210
321
  method,
@@ -227,9 +338,9 @@ export function matchPublicEndpoint(
227
338
  * it reaches a public endpoint, which is why the proxy never sends a public
228
339
  * endpoint the session: a cross-site form posting to it acts as nobody.
229
340
  *
230
- * @param request The incoming request
231
- * @param safeMethods Methods that change nothing and need no Origin
232
- * @param proxy The backend proxy the request reached
341
+ * @param {import("node:http").IncomingMessage} request The incoming request
342
+ * @param {ReadonlySet<string>} safeMethods Methods that change nothing and need no Origin
343
+ * @param {import("./backend-proxies.mjs").BackendProxy} proxy The backend proxy the request reached
233
344
  * @returns `{ anonymous, headers, isRefused }`
234
345
  */
235
346
  export function proxyAccess(request, safeMethods, proxy) {
@@ -252,7 +363,11 @@ export function proxyAccess(request, safeMethods, proxy) {
252
363
  };
253
364
  }
254
365
 
255
- /** Answers the 403 of a request `proxyAccess` refused. */
366
+ /**
367
+ * Answers the 403 of a request `proxyAccess` refused.
368
+ *
369
+ * @param {import("node:http").ServerResponse} response
370
+ */
256
371
  export function refuseCrossOrigin(response) {
257
372
  json(response, 403, CROSS_ORIGIN_ERROR);
258
373
  }
@@ -1,15 +1,25 @@
1
+ import { isRecord } from "./type-guards.mjs";
2
+
1
3
  const VARIABLE = "DMS_PUBLIC_OPTIONS";
2
4
  export const PUBLIC_OPTIONS_ELEMENT_ID = "dms-public-options";
3
5
 
6
+ /**
7
+ * The registry the workspace was materialized with.
8
+ *
9
+ * @typedef {object} FrontendRegistry
10
+ * @property {{ id: string, options?: unknown }[]} modules The modules, highest priority first
11
+ */
12
+
13
+ /**
14
+ * @param {string | undefined} raw
15
+ * @returns {Record<string, unknown> | undefined}
16
+ */
4
17
  function parse(raw) {
5
18
  if (!raw) return undefined;
6
19
  try {
20
+ /** @type {unknown} */
7
21
  const parsed = JSON.parse(raw);
8
- return typeof parsed === "object" &&
9
- parsed !== null &&
10
- !Array.isArray(parsed)
11
- ? parsed
12
- : undefined;
22
+ return isRecord(parsed) && !Array.isArray(parsed) ? parsed : undefined;
13
23
  } catch {
14
24
  console.warn(`${VARIABLE} is not a JSON object of options by module id`);
15
25
  return undefined;
@@ -27,7 +37,11 @@ function parse(raw) {
27
37
  */
28
38
  export const RUNTIME_PUBLIC_OPTIONS = parse(process.env[VARIABLE]);
29
39
 
30
- /** The options of each module of the registry, the runtime ones first. */
40
+ /**
41
+ * The options of each module of the registry, the runtime ones first.
42
+ *
43
+ * @param {FrontendRegistry} registry
44
+ */
31
45
  export function publicOptionsOf(registry) {
32
46
  return Object.fromEntries(
33
47
  registry.modules.map((module) => [
@@ -1,9 +1,13 @@
1
+ import { isNumber, isObjectLike } from "./type-guards.mjs";
2
+
1
3
  const FRONTEND_ORIGIN = "http://frontend.local";
2
4
  const TEXT_TYPE = "text/plain; charset=utf-8";
3
5
 
4
6
  /**
5
7
  * Whether the path of `url` holds a percent-escape no decoder accepts, such as
6
8
  * `/projects/%E0%A4%A`. Vite and the router decode the path, and throw on it.
9
+ *
10
+ * @param {string} url
7
11
  */
8
12
  export function hasMalformedPath(url) {
9
13
  try {
@@ -14,9 +18,23 @@ export function hasMalformedPath(url) {
14
18
  }
15
19
  }
16
20
 
21
+ /**
22
+ * The status an error asks to be answered with: its integer `status`, else 502.
23
+ *
24
+ * @param {unknown} error
25
+ * @returns {number}
26
+ */
27
+ export function failureStatus(error) {
28
+ const status = isObjectLike(error) ? error.status : undefined;
29
+ return isNumber(status) && Number.isInteger(status) ? status : 502;
30
+ }
31
+
17
32
  /**
18
33
  * Whether a failed request can no longer be answered: the client went away, or
19
34
  * the response already started.
35
+ *
36
+ * @param {unknown} error
37
+ * @param {import("node:http").ServerResponse} response
20
38
  */
21
39
  export function isResponseGone(error, response) {
22
40
  return (
@@ -30,6 +48,8 @@ export function isResponseGone(error, response) {
30
48
  /**
31
49
  * Answers for requests the server cannot serve at all: an unreadable URL, or
32
50
  * a failure while it was already rendering the error page.
51
+ *
52
+ * @param {import("node:http").ServerResponse} response
33
53
  */
34
54
  export function writeBadRequest(response) {
35
55
  response.writeHead(400, { "content-type": TEXT_TYPE });
@@ -40,6 +60,9 @@ export function writeBadRequest(response) {
40
60
  * The last resort once even the error page failed: a bare 500, or a dropped
41
61
  * connection when the response already started. The server keeps running
42
62
  * either way.
63
+ *
64
+ * @param {unknown} error
65
+ * @param {import("node:http").ServerResponse} response
43
66
  */
44
67
  export function abandonResponse(error, response) {
45
68
  console.error("DMS error response failed", error);
@@ -1,6 +1,19 @@
1
1
  import { matchBackendProxy } from "./backend-proxies.mjs";
2
2
  import { isRendererFilePath, moduleRouteMethods } from "./module-routes.mjs";
3
3
 
4
+ /**
5
+ * @typedef {object} RoutingModules
6
+ * @property {import("./module-route-types.mjs").ServerRoute[]} routes
7
+ * @property {import("./backend-proxies.mjs").BackendProxy[]} [proxies]
8
+ */
9
+ /**
10
+ * @typedef {{ kind: "renderer" }
11
+ * | { kind: "module", id: string }
12
+ * | { kind: "backend", proxy: import("./backend-proxies.mjs").BackendProxy }
13
+ * | { kind: "page" }
14
+ * | { kind: "not-found" }} RequestOwner
15
+ */
16
+
4
17
  const PAGE_METHODS = new Set(["GET", "HEAD"]);
5
18
 
6
19
  /**
@@ -14,12 +27,12 @@ const PAGE_METHODS = new Set(["GET", "HEAD"]);
14
27
  * `backendProxies`); 4. the pages, for everything else: a method other than
15
28
  * `GET` and `HEAD` on a page path is a `404`.
16
29
  *
17
- * @param method The request's method
18
- * @param pathname The request's normalized path
19
- * @param modules What `loadServerModules` returned: `routes` and `proxies`
20
- * @param hasRendererFile `(pathname) => boolean`, whether the renderer has a
30
+ * @param {string} method The request's method
31
+ * @param {string} pathname The request's normalized path
32
+ * @param {RoutingModules} modules What `loadServerModules` returned: `routes` and `proxies`
33
+ * @param {(pathname: string) => boolean} hasRendererFile `(pathname) => boolean`, whether the renderer has a
21
34
  * file at that path
22
- * @returns `{ kind: "renderer" }`, `{ kind: "module", id }`,
35
+ * @returns {RequestOwner} `{ kind: "renderer" }`, `{ kind: "module", id }`,
23
36
  * `{ kind: "backend", proxy }`, `{ kind: "page" }` or
24
37
  * `{ kind: "not-found" }`
25
38
  */
@@ -41,9 +54,9 @@ export function requestOwner(method, pathname, modules, hasRendererFile) {
41
54
  * `Location` on such a path can become a frontend path (see
42
55
  * `proxy-redirects.mjs`).
43
56
  *
44
- * @param pathname A normalized path
45
- * @param modules What `loadServerModules` returned: `routes` and `proxies`
46
- * @param hasRendererFile `(pathname) => boolean`, whether the renderer has a
57
+ * @param {string} pathname A normalized path
58
+ * @param {RoutingModules} modules What `loadServerModules` returned: `routes` and `proxies`
59
+ * @param {(pathname: string) => boolean} [hasRendererFile] `(pathname) => boolean`, whether the renderer has a
47
60
  * file at that path
48
61
  */
49
62
  export function reachesBackend(
@@ -56,7 +69,11 @@ export function reachesBackend(
56
69
  );
57
70
  }
58
71
 
59
- /** The `x-dms-route-owner` value of an owner, for the development server. */
72
+ /**
73
+ * The `x-dms-route-owner` value of an owner, for the development server.
74
+ *
75
+ * @param {RequestOwner} owner
76
+ */
60
77
  export function ownerLabel(owner) {
61
78
  if (owner.kind === "module") return `module:${owner.id}`;
62
79
  if (owner.kind === "backend") return `backend:${owner.proxy.path}`;
@@ -1,4 +1,5 @@
1
1
  import { writeBadRequest } from "./request-failure.mjs";
2
+ import { isString } from "./type-guards.mjs";
2
3
 
3
4
  const FRONTEND_ORIGIN = "http://frontend.local";
4
5
 
@@ -7,9 +8,11 @@ const FRONTEND_ORIGIN = "http://frontend.local";
7
8
  * query, that stays on this server once resolved. Anything else (an absolute
8
9
  * or authority form, `*`, a path a URL parser reads as another host) is not a
9
10
  * request a browser sends to the frontend.
11
+ *
12
+ * @param {string | undefined} target
10
13
  */
11
14
  export function isOriginForm(target) {
12
- if (typeof target !== "string" || !target.startsWith("/")) return false;
15
+ if (!isString(target) || !target.startsWith("/")) return false;
13
16
  try {
14
17
  return new URL(target, FRONTEND_ORIGIN).origin === FRONTEND_ORIGIN;
15
18
  } catch {
@@ -22,8 +25,8 @@ export function isOriginForm(target) {
22
25
  * and query on the backend's origin. The path is set, not resolved, so a path
23
26
  * normalizing to `//host` (`/x/..//host`) stays a path on the backend.
24
27
  *
25
- * @param target The request target, origin-form
26
- * @param backendBaseUrl The backend's base URL
28
+ * @param {string} target The request target, origin-form
29
+ * @param {string} [backendBaseUrl] The backend's base URL
27
30
  */
28
31
  export function backendUrl(
29
32
  target,
@@ -45,6 +48,9 @@ export function backendUrl(
45
48
  * form `http://host/x` all name another host once resolved, which turned the
46
49
  * proxy into an open one. Every routing decision and the URL sent to the
47
50
  * backend read the same normalized path from here.
51
+ *
52
+ * @param {import("node:http").IncomingMessage} request
53
+ * @param {import("node:http").ServerResponse} response
48
54
  */
49
55
  export function refuseForeignTarget(request, response) {
50
56
  if (isOriginForm(request.url)) return false;