@antelopejs/dms-frontend 0.9.3 → 0.10.1

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 (140) hide show
  1. package/README.md +46 -29
  2. package/dist/commands/build-action.d.ts +3 -0
  3. package/dist/commands/build-action.js +77 -8
  4. package/dist/commands/build.js +7 -0
  5. package/dist/commands/clean-action.js +1 -1
  6. package/dist/commands/clean.js +1 -1
  7. package/dist/commands/dev-action.js +4 -3
  8. package/dist/commands/dev.js +1 -1
  9. package/dist/commands/prepare-action.js +1 -1
  10. package/dist/commands/start-action.js +3 -24
  11. package/dist/commands/workspace-task.js +2 -1
  12. package/dist/config.js +1 -0
  13. package/dist/derived-outputs.d.ts +1 -0
  14. package/dist/derived-outputs.js +6 -4
  15. package/dist/discovery.js +17 -6
  16. package/dist/entry-weight.js +15 -0
  17. package/dist/fs-sync.js +7 -4
  18. package/dist/help.js +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/internal/built-workspace.d.ts +4 -0
  21. package/dist/internal/built-workspace.js +43 -0
  22. package/dist/internal/ensure.d.ts +1 -0
  23. package/dist/internal/ensure.js +8 -0
  24. package/dist/internal/locale-catalogs.d.ts +8 -3
  25. package/dist/internal/locale-catalogs.js +16 -6
  26. package/dist/internal/manifest-decoder.d.ts +7 -0
  27. package/dist/internal/manifest-decoder.js +81 -0
  28. package/dist/internal/own-release.d.ts +5 -0
  29. package/dist/internal/own-release.js +20 -0
  30. package/dist/internal/plain-object.d.ts +1 -0
  31. package/dist/internal/plain-object.js +6 -0
  32. package/dist/internal/prepared-workspace.d.ts +12 -0
  33. package/dist/internal/prepared-workspace.js +74 -0
  34. package/dist/internal/text-list.d.ts +1 -0
  35. package/dist/internal/text-list.js +10 -0
  36. package/dist/internal/vite-manifest.d.ts +6 -0
  37. package/dist/internal/vite-manifest.js +24 -0
  38. package/dist/layer-watch.js +25 -24
  39. package/dist/layers.d.ts +1 -1
  40. package/dist/layers.js +3 -1
  41. package/dist/locale-files.d.ts +6 -3
  42. package/dist/locale-files.js +15 -9
  43. package/dist/manifest.d.ts +1 -1
  44. package/dist/manifest.js +46 -32
  45. package/dist/materialize.js +6 -2
  46. package/dist/module-set.js +2 -1
  47. package/dist/node-options.d.ts +1 -0
  48. package/dist/node-options.js +8 -0
  49. package/dist/output.d.ts +15 -9
  50. package/dist/output.js +30 -18
  51. package/dist/ports.d.ts +3 -1
  52. package/dist/ports.js +1 -1
  53. package/dist/private-values.d.ts +1 -1
  54. package/dist/private-values.js +5 -3
  55. package/dist/project-modules.d.ts +5 -1
  56. package/dist/project-modules.js +23 -15
  57. package/dist/renderer-range.js +5 -3
  58. package/dist/server-process.js +1 -1
  59. package/dist/verify-source-build.js +10 -2
  60. package/dist/verify-source-client-bundle.d.ts +1 -0
  61. package/dist/verify-source-client-bundle.js +30 -0
  62. package/dist/verify-source-runner.js +36 -94
  63. package/dist/verify-source-sources.js +49 -11
  64. package/dist/verify-source-workspace.d.ts +2 -0
  65. package/dist/verify-source-workspace.js +111 -0
  66. package/dist/workspace-setup.d.ts +1 -0
  67. package/dist/workspace-setup.js +4 -3
  68. package/package.json +11 -7
  69. package/skills/dms-frontend/SKILL.md +1 -1
  70. package/templates/vue/app-runtime.ts +2 -9
  71. package/templates/vue/backend-boundary.mjs +132 -0
  72. package/templates/vue/client-chunking.mjs +2 -0
  73. package/templates/vue/client-server-guard.mjs +16 -0
  74. package/templates/vue/compress-assets.mjs +9 -1
  75. package/templates/vue/entry-weight.mjs +29 -3
  76. package/templates/vue/frontend-build.ts +4 -3
  77. package/templates/vue/frontend-module.ts +1 -0
  78. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  79. package/templates/vue/frontend-runtime/state.ts +2 -2
  80. package/templates/vue/frontend-runtime/types.ts +8 -2
  81. package/templates/vue/head-order.mjs +10 -2
  82. package/templates/vue/icon-api.mjs +2 -0
  83. package/templates/vue/icon-scan.mjs +4 -0
  84. package/templates/vue/server/auth/backend.mjs +42 -5
  85. package/templates/vue/server/auth/client-ip.mjs +52 -4
  86. package/templates/vue/server/auth/establish-endpoints.mjs +11 -7
  87. package/templates/vue/server/auth/session.mjs +86 -4
  88. package/templates/vue/server/auth-declarations.mjs +46 -0
  89. package/templates/vue/server/auth-hooks.mjs +55 -7
  90. package/templates/vue/server/backend-proxies.mjs +78 -18
  91. package/templates/vue/server/backend-request.mjs +61 -0
  92. package/templates/vue/server/backend-response.mjs +23 -6
  93. package/templates/vue/server/built-files.mjs +7 -0
  94. package/templates/vue/server/cli-report.mjs +75 -12
  95. package/templates/vue/server/client-error.mjs +5 -0
  96. package/templates/vue/server/client-manifest.mjs +23 -1
  97. package/templates/vue/server/content.mjs +40 -10
  98. package/templates/vue/server/dev-private-files.mjs +17 -1
  99. package/templates/vue/server/dev-styles.mjs +38 -0
  100. package/templates/vue/server/email-bundle-watch.mjs +9 -3
  101. package/templates/vue/server/email.mjs +14 -6
  102. package/templates/vue/server/favicon.mjs +3 -0
  103. package/templates/vue/server/generated-files.mjs +221 -0
  104. package/templates/vue/server/homepage.mjs +21 -9
  105. package/templates/vue/server/html-tag.mjs +4 -0
  106. package/templates/vue/server/icons.mjs +93 -16
  107. package/templates/vue/server/in-place-render.mjs +35 -0
  108. package/templates/vue/server/inertia.mjs +76 -3
  109. package/templates/vue/server/module-event.mjs +118 -25
  110. package/templates/vue/server/module-route-types.mjs +78 -0
  111. package/templates/vue/server/module-routes.mjs +151 -96
  112. package/templates/vue/server/module-server.mjs +121 -40
  113. package/templates/vue/server/module-testing.mjs +36 -12
  114. package/templates/vue/server/page-props.mjs +94 -3
  115. package/templates/vue/server/page-redirects.mjs +37 -0
  116. package/templates/vue/server/path-patterns.mjs +35 -12
  117. package/templates/vue/server/proxy-compression.mjs +22 -0
  118. package/templates/vue/server/proxy-headers.mjs +12 -2
  119. package/templates/vue/server/proxy-redirects.mjs +21 -5
  120. package/templates/vue/server/proxy-request.mjs +58 -0
  121. package/templates/vue/server/proxy-transfers.mjs +44 -10
  122. package/templates/vue/server/public-endpoints.mjs +139 -28
  123. package/templates/vue/server/public-options.mjs +20 -6
  124. package/templates/vue/server/request-failure.mjs +24 -1
  125. package/templates/vue/server/request-routing.mjs +32 -10
  126. package/templates/vue/server/request-target.mjs +30 -11
  127. package/templates/vue/server/route-rules.mjs +48 -18
  128. package/templates/vue/server/ssr-module.mjs +32 -0
  129. package/templates/vue/server/static-files.mjs +35 -7
  130. package/templates/vue/server/type-guards.mjs +71 -0
  131. package/templates/vue/server/vite-errors.mjs +45 -7
  132. package/templates/vue/server/web-body.mjs +5 -1
  133. package/templates/vue/server-module.ts +12 -2
  134. package/templates/vue/server.mjs +162 -157
  135. package/templates/vue/sfc-global-types.mjs +112 -13
  136. package/templates/vue/ssr-renderer.ts +2 -2
  137. package/templates/vue/ui-inertia-page.mjs +16 -1
  138. package/templates/vue/ui-kbd.mjs +2 -1
  139. package/templates/vue/ui-request-state.mjs +2 -1
  140. package/templates/vue/vite.config.ts +9 -0
@@ -101,7 +101,16 @@ export interface DmsServerEvent {
101
101
  * overwrites another's, and naming a `dms_*` cookie throws.
102
102
  */
103
103
  cookies: {
104
- readSealed<T = unknown>(name: string): T | undefined;
104
+ /**
105
+ * The value a sealed cookie holds, when `accepts` takes it. A cookie that
106
+ * is absent, does not open, or holds a value `accepts` refuses reads as
107
+ * `undefined`. `accepts` receives the JSON the cookie was written from and
108
+ * is never called for an absent or unreadable cookie.
109
+ */
110
+ readSealed<T>(
111
+ name: string,
112
+ accepts: (value: unknown) => value is T,
113
+ ): T | undefined;
105
114
  writeSealed(
106
115
  name: string,
107
116
  value: unknown,
@@ -177,7 +186,8 @@ export interface DmsServerRouteOptions {
177
186
  * The session logic the frontend server runs outside any route, which the
178
187
  * module owning sign-in provides. Without `publicSession`, pages carry no
179
188
  * `user` and `session`; without `refreshSession`, a session the backend
180
- * refuses is not renewed; `signInPath` is `/auth` by default.
189
+ * refuses is not renewed; `signInPath` is `/auth` by default. The server
190
+ * calls each hook with the registered object as `this`.
181
191
  */
182
192
  export interface DmsServerAuth {
183
193
  /** Where a signed-out visit to a private page goes, with `?redirect=`. */
@@ -2,18 +2,14 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { createServer, maxHeaderSize } from "node:http";
3
3
  import { extname, join, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { RequestBodyError, UpstreamError } from "./server/auth/backend.mjs";
5
6
  import {
6
- CLIENT_HEADERS,
7
- relayedHeaders,
8
- RequestBodyError,
9
- UpstreamError,
10
- } from "./server/auth/backend.mjs";
11
- import { forwardedHeaders } from "./server/auth/client-ip.mjs";
12
- import { proxyAccess, refuseCrossOrigin } from "./server/public-endpoints.mjs";
7
+ backendJson,
8
+ serverComponentFetch,
9
+ } from "./server/backend-request.mjs";
13
10
  import { readSession } from "./server/auth/session.mjs";
14
11
  import {
15
12
  BackendResponseError,
16
- backendResponseError,
17
13
  redirectAccessRefusal,
18
14
  UNEXPECTED_ERROR_BODY,
19
15
  } from "./server/backend-response.mjs";
@@ -21,6 +17,7 @@ import { handleClientError } from "./server/client-error.mjs";
21
17
  import {
22
18
  reportListenError,
23
19
  reportReady,
20
+ reportRouting,
24
21
  reportStartError,
25
22
  } from "./server/cli-report.mjs";
26
23
  import { publicBuildFile } from "./server/built-files.mjs";
@@ -50,20 +47,29 @@ import {
50
47
  handleModuleRoute,
51
48
  loadServerModules,
52
49
  } from "./server/module-server.mjs";
53
- import { HOMEPAGE } from "./server/homepage.mjs";
50
+ import {
51
+ redirectSignedOutPrivatePage,
52
+ redirectToHomepage,
53
+ } from "./server/page-redirects.mjs";
54
54
  import { publicOptionsScript } from "./server/public-options.mjs";
55
- import { renderInPlace, serverReads } from "./server/in-place-render.mjs";
56
- import { loadPageProps, propsForQuery } from "./server/page-props.mjs";
55
+ import { renderInPlace } from "./server/in-place-render.mjs";
56
+ import {
57
+ loadPageProps,
58
+ pageRoute,
59
+ propsForQuery,
60
+ } from "./server/page-props.mjs";
57
61
  import { htmlTag } from "./server/html-tag.mjs";
62
+ import { isString } from "./server/type-guards.mjs";
58
63
  import { listenHost } from "./server/listen-host.mjs";
59
64
  import * as icons from "./server/icons.mjs";
65
+ import { ssrRenderer } from "./server/ssr-module.mjs";
60
66
  import * as requestFailure from "./server/request-failure.mjs";
67
+ import { proxy } from "./server/proxy-request.mjs";
61
68
  import {
62
- PLAIN_ANSWER_HEADERS,
63
- relayAnswer,
64
- } from "./server/proxy-compression.mjs";
65
- import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
66
- import { sendWithinLimit, SERVER_OPTIONS } from "./server/proxy-transfers.mjs";
69
+ hasOriginFormTarget,
70
+ isServerRequest,
71
+ } from "./server/request-target.mjs";
72
+ import { SERVER_OPTIONS } from "./server/proxy-transfers.mjs";
67
73
  import {
68
74
  createInertiaPage,
69
75
  htmlHeaders,
@@ -72,24 +78,29 @@ import {
72
78
  isFrontendVisit,
73
79
  redirectFrontendVisit,
74
80
  handleAssetVersionMismatch,
81
+ isSsrPage,
75
82
  } from "./server/inertia.mjs";
76
83
  export * from "./server/inertia.mjs";
77
84
 
85
+ /** @typedef {import("node:http").IncomingMessage} IncomingMessage */
86
+ /** @typedef {import("./server/request-target.mjs").ServerRequest} ServerRequest */
87
+ /** @typedef {import("node:http").ServerResponse} ServerResponse */
88
+ /** @typedef {import("vite").ViteDevServer} ViteDevServer */
89
+ /** @typedef {import("./server/inertia.mjs").InertiaPage} InertiaPage */
90
+ /** @typedef {import("./server/in-place-render.mjs").ServerReads} ServerReads */
91
+
78
92
  const INERTIA_HEADER = "x-inertia";
79
93
  const JSON_TYPE = "application/json";
80
94
  const PROJECT_ROOT = fileURLToPath(new URL(".", import.meta.url));
81
- const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
95
+ /**
96
+ * @type {[
97
+ * (pathname: string) => boolean,
98
+ * (request: ServerRequest, response: ServerResponse) => Promise<void>
99
+ * ][]}
100
+ */
82
101
  const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
83
102
  const CLIENT_ROOT = join(PROJECT_ROOT, "dist/client");
84
103
  const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
85
- /**
86
- * A URL, not a path: on Windows, `import()` reads `C:\...` as a URL whose
87
- * scheme is `c:` and refuses it.
88
- */
89
- const BUILT_SSR_RENDERER_URL = new URL(
90
- "dist/ssr/ssr-renderer.js",
91
- import.meta.url,
92
- );
93
104
 
94
105
  /**
95
106
  * The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
@@ -98,20 +109,26 @@ const BUILT_SSR_RENDERER_URL = new URL(
98
109
  * NODE_ENV=production throws a ReferenceError on the first render.
99
110
  */
100
111
  function defineVueCompileFlags() {
101
- globalThis.__VUE_PROD_DEVTOOLS__ ??= false;
112
+ /** @type {typeof globalThis & { __VUE_PROD_DEVTOOLS__?: boolean }} */
113
+ const vueGlobals = globalThis;
114
+ vueGlobals.__VUE_PROD_DEVTOOLS__ ??= false;
102
115
  }
103
116
 
104
117
  defineVueCompileFlags();
105
- let productionSsrRenderer;
118
+ /** @type {ViteDevServer | undefined} */
106
119
  let vite;
120
+ /** @type {Promise<ViteDevServer> | undefined} */
107
121
  let vitePromise;
122
+ /** @type {import("node:http").Server | undefined} */
108
123
  let frontendHttpServer;
109
124
 
125
+ /** @returns {Promise<ViteDevServer | undefined>} */
110
126
  async function developmentServer() {
111
127
  if (process.env.DMS_DEV !== "true") return undefined;
112
128
  if (vite) return vite;
113
129
  vitePromise ??= import("vite")
114
130
  .then(({ createServer: createViteServer }) => {
131
+ /** @type {import("vite").ServerOptions} */
115
132
  const serverOptions = {
116
133
  middlewareMode: true,
117
134
  fs: { deny: DEV_FS_DENY },
@@ -127,57 +144,24 @@ async function developmentServer() {
127
144
  vite = server;
128
145
  return server;
129
146
  })
130
- .catch((error) => {
131
- vitePromise = undefined;
132
- throw error;
133
- });
147
+ .catch(
148
+ /** @param {unknown} error */ (error) => {
149
+ vitePromise = undefined;
150
+ throw error;
151
+ },
152
+ );
134
153
  return vitePromise;
135
154
  }
136
155
 
137
- /**
138
- * The headers of a backend request: the relayed client headers, the client's
139
- * Origin and Referer as received, the `X-Forwarded-*` headers (set last, so no
140
- * caller writes them), and the session's access token unless the request goes
141
- * anonymously.
142
- */
143
- function backendHeaders(request, access = { anonymous: false, headers: [] }) {
144
- const headers = {
145
- ...relayedHeaders(request, [
146
- "accept",
147
- "content-type",
148
- "if-match",
149
- "origin",
150
- "referer",
151
- ...CLIENT_HEADERS,
152
- ...access.headers,
153
- ]),
154
- ...forwardedHeaders(request),
155
- };
156
- const session = access.anonymous ? undefined : readSession(request);
157
- if (session?.accessToken)
158
- headers.authorization = `Bearer ${session.accessToken}`;
159
- return headers;
160
- }
161
-
162
- function serverComponentFetch(request) {
163
- return serverReads(() => backendHeaders(request));
164
- }
165
-
166
- async function backendJson(path, request) {
167
- const response = await fetch(new URL(path, process.env.DMS_API_BASE_URL), {
168
- headers: backendHeaders(request),
169
- });
170
- if (!response.ok) {
171
- throw await backendResponseError(response);
172
- }
173
- return response.json();
174
- }
175
-
176
156
  /** The session logic of the module that owns sign-in. */
177
157
  async function currentAuth() {
178
158
  return (await loadServerModules(await developmentServer())).serverAuth;
179
159
  }
180
160
 
161
+ /**
162
+ * @param {ServerRequest} request
163
+ * @returns {import("./server/page-props.mjs").PageBackend}
164
+ */
181
165
  function pageBackend(request) {
182
166
  return {
183
167
  fetchJson: (path) => backendJson(path, request),
@@ -188,76 +172,23 @@ function pageBackend(request) {
188
172
  };
189
173
  }
190
174
 
191
- function pageProps(request, auth) {
192
- return loadPageProps(request, pageBackend(request), auth);
193
- }
194
-
195
- function redirectToHomepage(request, response, pathname) {
196
- if (pathname !== "/" || HOMEPAGE === "/") return false;
197
- redirectFrontendVisit(request, response, HOMEPAGE);
198
- return true;
199
- }
200
-
201
- function redirectSignedOutPrivatePage(request, response, props, auth) {
202
- const route = props.page?.route;
203
- if (
204
- route?.publicAccess === true ||
205
- route?.hasAccess !== false ||
206
- readSession(request)
207
- )
208
- return false;
209
- redirectFrontendVisit(request, response, auth.signInLocation(request.url));
210
- return true;
211
- }
212
-
213
175
  /**
214
- * Sends a request to the backend through the proxy it reached.
215
- * `isProxiedPath` says which backend paths a relayed `Location` may name on
216
- * the frontend's origin.
217
- *
218
- * A client that left while the request was routed closed the response already,
219
- * and would never abort the request below.
220
- *
221
- * Cancelling the body alone misses a client that leaves before the backend
222
- * answers: its stream, an SSE feed above all, then stayed open for good.
223
- *
224
- * A backend redirect is the browser's to follow: Media sends a file's link to
225
- * the storage's signed URL, on the public origin the server itself may not
226
- * reach, which turned every download into a 502.
176
+ * @param {ServerRequest} request
177
+ * @param {import("./server/page-props.mjs").PageAuth} auth
227
178
  */
228
- async function proxy(request, response, backendProxy, isProxiedPath) {
229
- const access = proxyAccess(request, SAFE_METHODS, backendProxy);
230
- if (access.isRefused) {
231
- refuseCrossOrigin(response);
232
- return true;
233
- }
234
- if (response.closed) return;
235
- const abandoned = new AbortController();
236
- response.once("close", () => abandoned.abort());
237
- const init = {
238
- signal: abandoned.signal,
239
- method: request.method,
240
- headers: { ...backendHeaders(request, access), ...PLAIN_ANSWER_HEADERS },
241
- duplex: "half",
242
- redirect: "manual",
243
- };
244
- const upstream = await sendWithinLimit(
245
- request,
246
- response,
247
- backendProxy.maxBodyBytes,
248
- (body) => fetch(backendUrl(request.url), { ...init, body }),
249
- );
250
- if (!upstream) return true;
251
- await relayAnswer(request, response, upstream, {
252
- fetched: true,
253
- isProxiedPath,
254
- });
179
+ function pageProps(request, auth) {
180
+ return loadPageProps(request, pageBackend(request), auth);
255
181
  }
256
182
 
257
183
  /**
258
184
  * Send the built file `pathname` names, or `false` when the build has none
259
185
  * there. `publicBuildFile` decides which file that is; `serveStaticFile` how
260
186
  * it goes out.
187
+ *
188
+ * @param {string} pathname
189
+ * @param {IncomingMessage} request
190
+ * @param {ServerResponse} response
191
+ * @returns {boolean}
261
192
  */
262
193
  function serveAsset(pathname, request, response) {
263
194
  const file = publicBuildFile(CLIENT_ROOT, pathname);
@@ -268,14 +199,10 @@ function serveAsset(pathname, request, response) {
268
199
  return true;
269
200
  }
270
201
 
271
- async function ssrRenderer(devServer) {
272
- const renderer = devServer
273
- ? await devServer.ssrLoadModule("/ssr-renderer.ts")
274
- : await (productionSsrRenderer ??= import(BUILT_SSR_RENDERER_URL.href));
275
- await icons.provideIconCollections(renderer);
276
- return renderer;
277
- }
278
-
202
+ /**
203
+ * @param {InertiaPage} page
204
+ * @returns {import("./ssr-renderer.ts").DmsSsrResult}
205
+ */
279
206
  function clientRenderedShell(page) {
280
207
  return {
281
208
  body: inertiaAppHtml(page),
@@ -285,9 +212,17 @@ function clientRenderedShell(page) {
285
212
  };
286
213
  }
287
214
 
215
+ /**
216
+ * @param {InertiaPage} page
217
+ * @param {ServerReads} serverFetch
218
+ * @param {string | undefined} requestCookies
219
+ * @param {ViteDevServer | undefined} devServer
220
+ */
288
221
  async function serverRendered(page, serverFetch, requestCookies, devServer) {
289
222
  try {
290
223
  const renderer = await ssrRenderer(devServer);
224
+ if (!isSsrPage(page))
225
+ throw new TypeError("The page has no path and no page props to render");
291
226
  return await renderer.renderDmsPage(page, serverFetch, requestCookies);
292
227
  } catch (error) {
293
228
  console.error(
@@ -301,6 +236,13 @@ async function serverRendered(page, serverFetch, requestCookies, devServer) {
301
236
  /**
302
237
  * Renders a page document. With `{ ssr: false }` (a route rule), the client
303
238
  * renders the page from the shell instead.
239
+ *
240
+ * @param {InertiaPage} page
241
+ * @param {string} requestUrl
242
+ * @param {ServerReads} serverFetch
243
+ * @param {string | undefined} requestCookies
244
+ * @param {{ ssr?: false }} [options]
245
+ * @returns {Promise<{ html: string, status: number, redirect?: string }>}
304
246
  */
305
247
  export async function renderHtml(
306
248
  page,
@@ -330,7 +272,7 @@ export async function renderHtml(
330
272
  .replace("<title>Antelope DMS</title>", () =>
331
273
  withFaviconPlaceholder(rendered.head.headTags),
332
274
  )
333
- .replace(/<html([^>]*)>/, (_, attributes) =>
275
+ .replace(/<html([^>]*)>/, (_, /** @type {string} */ attributes) =>
334
276
  htmlTag(attributes, rendered.head.htmlAttrs),
335
277
  )
336
278
  .replace("<body", `<body ${rendered.head.bodyAttrs}`)
@@ -343,6 +285,12 @@ export async function renderHtml(
343
285
  return { html, status: rendered.error?.statusCode ?? 200 };
344
286
  }
345
287
 
288
+ /**
289
+ * @param {InertiaPage} page
290
+ * @param {ServerRequest} request
291
+ * @param {Pick<import("./server/route-rules.mjs").RouteRule, "ssr">} [rule]
292
+ * @param {ServerReads} [reads]
293
+ */
346
294
  function renderRequestHtml(page, request, rule = {}, reads) {
347
295
  const fetch = reads ?? serverComponentFetch(request);
348
296
  return renderHtml(page, request.url, fetch, request.headers.cookie, {
@@ -353,6 +301,11 @@ function renderRequestHtml(page, request, rule = {}, reads) {
353
301
  /**
354
302
  * Renders a page document, at the query the page completes as it renders:
355
303
  * see `renderInPlace`.
304
+ *
305
+ * @param {InertiaPage} page
306
+ * @param {ServerRequest} request
307
+ * @param {import("./server/route-rules.mjs").MergedRouteRule} rule
308
+ * @param {import("./server/page-props.mjs").PageAuth} auth
356
309
  */
357
310
  function renderDocument(page, request, rule, auth) {
358
311
  const fetch = serverComponentFetch(request);
@@ -365,9 +318,14 @@ function renderDocument(page, request, rule, auth) {
365
318
  );
366
319
  }
367
320
 
321
+ /**
322
+ * @param {unknown} error
323
+ * @param {ServerRequest} request
324
+ * @param {ServerResponse} response
325
+ */
368
326
  async function writeBackendError(error, request, response) {
369
327
  const auth = await currentAuth();
370
- const status = Number.isInteger(error?.status) ? error.status : 502;
328
+ const status = requestFailure.failureStatus(error);
371
329
  const pathname = new URL(request.url, "http://frontend.local").pathname;
372
330
  if (
373
331
  error instanceof BackendResponseError &&
@@ -410,6 +368,12 @@ async function writeBackendError(error, request, response) {
410
368
  response.end(JSON.stringify(payload));
411
369
  }
412
370
 
371
+ /**
372
+ * @param {ViteDevServer} devServer
373
+ * @param {IncomingMessage} request
374
+ * @param {ServerResponse} response
375
+ * @returns {Promise<boolean>}
376
+ */
413
377
  async function answeredByVite(devServer, request, response) {
414
378
  const result = await runViteMiddlewares(devServer, request, response);
415
379
  if (result.status === "failed") writeViteError(response, result.error);
@@ -422,6 +386,10 @@ async function answeredByVite(devServer, request, response) {
422
386
  * production server serves. Only a path with an extension names one.
423
387
  *
424
388
  * The path is decoded as Vite and the asset server read it: `%2E` is a dot.
389
+ *
390
+ * @param {ViteDevServer | undefined} devServer
391
+ * @param {string} requested
392
+ * @returns {boolean}
425
393
  */
426
394
  function hasRendererFile(devServer, requested) {
427
395
  let pathname;
@@ -445,7 +413,15 @@ function hasRendererFile(devServer, requested) {
445
413
  });
446
414
  }
447
415
 
448
- /** Vite's files in development, the build's in production; else a 404. */
416
+ /**
417
+ * Vite's files in development, the build's in production; else a 404.
418
+ *
419
+ * @param {ViteDevServer | undefined} devServer
420
+ * @param {IncomingMessage} request
421
+ * @param {ServerResponse} response
422
+ * @param {string} pathname
423
+ * @returns {Promise<void>}
424
+ */
449
425
  async function serveRendererFile(devServer, request, response, pathname) {
450
426
  const answered = devServer
451
427
  ? await answeredByVite(devServer, request, response)
@@ -455,11 +431,16 @@ async function serveRendererFile(devServer, request, response, pathname) {
455
431
  response.end("Not found");
456
432
  }
457
433
 
434
+ /** @param {ServerResponse} response */
458
435
  function writeNotFound(response) {
459
436
  response.writeHead(404, { "content-type": JSON_TYPE });
460
437
  response.end(JSON.stringify({ message: "Not found" }));
461
438
  }
462
439
 
440
+ /**
441
+ * @param {ServerRequest} request
442
+ * @param {ServerResponse} response
443
+ */
463
444
  export async function handleRequest(request, response) {
464
445
  const url = new URL(request.url, "http://frontend.local");
465
446
  const pathname = url.pathname;
@@ -467,6 +448,7 @@ export async function handleRequest(request, response) {
467
448
  if (localRoute) return localRoute[1](request, response);
468
449
  const devServer = await developmentServer();
469
450
  const modules = await loadServerModules(devServer);
451
+ /** @param {string} path */
470
452
  const hasFile = (path) => hasRendererFile(devServer, path);
471
453
  const owner = requestOwner(request.method, pathname, modules, hasFile);
472
454
  if (devServer) response.setHeader("x-dms-route-owner", ownerLabel(owner));
@@ -474,6 +456,7 @@ export async function handleRequest(request, response) {
474
456
  return serveRendererFile(devServer, request, response, pathname);
475
457
  if (owner.kind === "module")
476
458
  return handleModuleRoute(request, response, pathname, modules);
459
+ /** @param {string} path */
477
460
  const isProxiedPath = (path) => reachesBackend(path, modules, hasFile);
478
461
  if (owner.kind === "backend")
479
462
  return proxy(request, response, owner.proxy, isProxiedPath);
@@ -487,8 +470,9 @@ export async function handleRequest(request, response) {
487
470
  if (redirectSignedOutPrivatePage(request, response, props, auth)) return;
488
471
  const page = createInertiaPage(request.url, props, undefined, onceProps);
489
472
  const rule = routeRuleFor(rules, pathname);
473
+ /** @param {number} status */
490
474
  const headersFor = (status) =>
491
- ruleHeaders(rule, request, props.page?.route, status);
475
+ ruleHeaders(rule, request, pageRoute(props), status);
492
476
  if (request.headers[INERTIA_HEADER]) {
493
477
  return writeContent(
494
478
  request,
@@ -513,8 +497,15 @@ export async function handleRequest(request, response) {
513
497
  );
514
498
  }
515
499
 
500
+ /**
501
+ * @param {unknown} error
502
+ * @param {IncomingMessage & { dmsSessionRetry?: boolean }} request
503
+ * @param {ServerResponse} response
504
+ */
516
505
  async function writeRequestFailure(error, request, response) {
517
506
  if (requestFailure.isResponseGone(error, response)) return;
507
+ if (!isServerRequest(request))
508
+ return requestFailure.abandonResponse(error, response);
518
509
  if (
519
510
  error instanceof BackendResponseError &&
520
511
  error.status === 401 &&
@@ -531,14 +522,24 @@ async function writeRequestFailure(error, request, response) {
531
522
  await writeBackendError(error, request, response);
532
523
  }
533
524
 
534
- /** An error page failing too must not reject: that would end the process. */
525
+ /**
526
+ * An error page failing too must not reject: that would end the process.
527
+ *
528
+ * @param {IncomingMessage} request
529
+ * @param {ServerResponse} response
530
+ * @returns {Promise<void>}
531
+ */
535
532
  export async function handleRequestSafely(request, response) {
536
533
  try {
537
- if (refuseForeignTarget(request, response)) return;
534
+ if (!hasOriginFormTarget(request)) {
535
+ requestFailure.writeBadRequest(response);
536
+ return;
537
+ }
538
538
  await handleRequest(request, response);
539
539
  } catch (error) {
540
- await writeRequestFailure(error, request, response).catch((failure) =>
541
- requestFailure.abandonResponse(failure, response),
540
+ await writeRequestFailure(error, request, response).catch(
541
+ /** @param {unknown} failure */ (failure) =>
542
+ requestFailure.abandonResponse(failure, response),
542
543
  );
543
544
  }
544
545
  }
@@ -548,25 +549,26 @@ if (
548
549
  import.meta.url === new URL(`file://${process.argv[1]}`).href
549
550
  ) {
550
551
  frontendHttpServer = createServer(SERVER_OPTIONS, handleRequestSafely);
551
- frontendHttpServer.on("clientError", (error, socket) =>
552
+ const server = frontendHttpServer;
553
+ server.on("clientError", (error, socket) =>
552
554
  handleClientError(error, socket, maxHeaderSize),
553
555
  );
554
556
  const port = Number(process.env.PORT ?? 3001);
555
557
  const host = listenHost();
558
+ /** @param {NodeJS.ErrnoException} error */
556
559
  const onListenError = (error) => void reportListenError(error, host, port);
557
- frontendHttpServer.once("error", onListenError);
558
- frontendHttpServer.listen(port, host, () => {
559
- frontendHttpServer.off("error", onListenError);
560
+ server.once("error", onListenError);
561
+ server.listen(port, host, () => {
562
+ server.off("error", onListenError);
560
563
  void announceReady();
561
564
  });
562
565
  }
563
566
 
564
567
  /** Prints who owns which paths, once the server is up. */
565
- async function reportRouting() {
568
+ async function reportServerModules() {
566
569
  try {
567
570
  const modules = await loadServerModules(await developmentServer());
568
- for (const line of routingReport(modules.proxies, modules.routes.length))
569
- console.log(line);
571
+ reportRouting(routingReport(modules.proxies, modules.routes.length));
570
572
  } catch (error) {
571
573
  console.error("DMS server modules failed to load", error);
572
574
  }
@@ -587,8 +589,11 @@ async function announceReady() {
587
589
  await reportStartError(error);
588
590
  return;
589
591
  }
590
- const { address, port } = frontendHttpServer.address();
592
+ const listening = frontendHttpServer?.address();
593
+ if (!listening || isString(listening))
594
+ throw new TypeError("The DMS server is not listening on a port");
595
+ const { address, port } = listening;
591
596
  await reportReady(address, port);
592
- await reportRouting();
597
+ await reportServerModules();
593
598
  if (process.env.DMS_DEV === "true") watchEmailBundle();
594
599
  }