@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
@@ -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";
@@ -50,20 +46,26 @@ import {
50
46
  handleModuleRoute,
51
47
  loadServerModules,
52
48
  } from "./server/module-server.mjs";
53
- import { HOMEPAGE } from "./server/homepage.mjs";
49
+ import {
50
+ redirectSignedOutPrivatePage,
51
+ redirectToHomepage,
52
+ } from "./server/page-redirects.mjs";
54
53
  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";
54
+ import { renderInPlace } from "./server/in-place-render.mjs";
55
+ import {
56
+ loadPageProps,
57
+ pageRoute,
58
+ propsForQuery,
59
+ } from "./server/page-props.mjs";
57
60
  import { htmlTag } from "./server/html-tag.mjs";
61
+ import { isString } from "./server/type-guards.mjs";
58
62
  import { listenHost } from "./server/listen-host.mjs";
59
63
  import * as icons from "./server/icons.mjs";
64
+ import { ssrRenderer } from "./server/ssr-module.mjs";
60
65
  import * as requestFailure from "./server/request-failure.mjs";
61
- 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";
66
+ import { proxy } from "./server/proxy-request.mjs";
67
+ import { refuseForeignTarget } from "./server/request-target.mjs";
68
+ import { SERVER_OPTIONS } from "./server/proxy-transfers.mjs";
67
69
  import {
68
70
  createInertiaPage,
69
71
  htmlHeaders,
@@ -75,21 +77,18 @@ import {
75
77
  } from "./server/inertia.mjs";
76
78
  export * from "./server/inertia.mjs";
77
79
 
80
+ /** @typedef {import("node:http").IncomingMessage} IncomingMessage */
81
+ /** @typedef {import("node:http").ServerResponse} ServerResponse */
82
+ /** @typedef {import("vite").ViteDevServer} ViteDevServer */
83
+ /** @typedef {import("./server/inertia.mjs").InertiaPage} InertiaPage */
84
+ /** @typedef {import("./server/in-place-render.mjs").ServerReads} ServerReads */
85
+
78
86
  const INERTIA_HEADER = "x-inertia";
79
87
  const JSON_TYPE = "application/json";
80
88
  const PROJECT_ROOT = fileURLToPath(new URL(".", import.meta.url));
81
- const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
82
89
  const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
83
90
  const CLIENT_ROOT = join(PROJECT_ROOT, "dist/client");
84
91
  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
92
 
94
93
  /**
95
94
  * The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
@@ -102,16 +101,20 @@ function defineVueCompileFlags() {
102
101
  }
103
102
 
104
103
  defineVueCompileFlags();
105
- let productionSsrRenderer;
104
+ /** @type {ViteDevServer | undefined} */
106
105
  let vite;
106
+ /** @type {Promise<ViteDevServer> | undefined} */
107
107
  let vitePromise;
108
+ /** @type {import("node:http").Server | undefined} */
108
109
  let frontendHttpServer;
109
110
 
111
+ /** @returns {Promise<ViteDevServer | undefined>} */
110
112
  async function developmentServer() {
111
113
  if (process.env.DMS_DEV !== "true") return undefined;
112
114
  if (vite) return vite;
113
115
  vitePromise ??= import("vite")
114
116
  .then(({ createServer: createViteServer }) => {
117
+ /** @type {import("vite").ServerOptions} */
115
118
  const serverOptions = {
116
119
  middlewareMode: true,
117
120
  fs: { deny: DEV_FS_DENY },
@@ -127,57 +130,24 @@ async function developmentServer() {
127
130
  vite = server;
128
131
  return server;
129
132
  })
130
- .catch((error) => {
131
- vitePromise = undefined;
132
- throw error;
133
- });
133
+ .catch(
134
+ /** @param {unknown} error */ (error) => {
135
+ vitePromise = undefined;
136
+ throw error;
137
+ },
138
+ );
134
139
  return vitePromise;
135
140
  }
136
141
 
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
142
  /** The session logic of the module that owns sign-in. */
177
143
  async function currentAuth() {
178
144
  return (await loadServerModules(await developmentServer())).serverAuth;
179
145
  }
180
146
 
147
+ /**
148
+ * @param {IncomingMessage} request
149
+ * @returns {import("./server/page-props.mjs").PageBackend}
150
+ */
181
151
  function pageBackend(request) {
182
152
  return {
183
153
  fetchJson: (path) => backendJson(path, request),
@@ -188,76 +158,23 @@ function pageBackend(request) {
188
158
  };
189
159
  }
190
160
 
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
161
  /**
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.
162
+ * @param {IncomingMessage} request
163
+ * @param {import("./server/page-props.mjs").PageAuth} auth
227
164
  */
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
- });
165
+ function pageProps(request, auth) {
166
+ return loadPageProps(request, pageBackend(request), auth);
255
167
  }
256
168
 
257
169
  /**
258
170
  * Send the built file `pathname` names, or `false` when the build has none
259
171
  * there. `publicBuildFile` decides which file that is; `serveStaticFile` how
260
172
  * it goes out.
173
+ *
174
+ * @param {string} pathname
175
+ * @param {IncomingMessage} request
176
+ * @param {ServerResponse} response
177
+ * @returns {boolean}
261
178
  */
262
179
  function serveAsset(pathname, request, response) {
263
180
  const file = publicBuildFile(CLIENT_ROOT, pathname);
@@ -268,14 +185,10 @@ function serveAsset(pathname, request, response) {
268
185
  return true;
269
186
  }
270
187
 
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
-
188
+ /**
189
+ * @param {InertiaPage} page
190
+ * @returns {import("./ssr-renderer.ts").DmsSsrResult}
191
+ */
279
192
  function clientRenderedShell(page) {
280
193
  return {
281
194
  body: inertiaAppHtml(page),
@@ -285,6 +198,12 @@ function clientRenderedShell(page) {
285
198
  };
286
199
  }
287
200
 
201
+ /**
202
+ * @param {InertiaPage} page
203
+ * @param {ServerReads} serverFetch
204
+ * @param {string | undefined} requestCookies
205
+ * @param {ViteDevServer | undefined} devServer
206
+ */
288
207
  async function serverRendered(page, serverFetch, requestCookies, devServer) {
289
208
  try {
290
209
  const renderer = await ssrRenderer(devServer);
@@ -301,6 +220,13 @@ async function serverRendered(page, serverFetch, requestCookies, devServer) {
301
220
  /**
302
221
  * Renders a page document. With `{ ssr: false }` (a route rule), the client
303
222
  * renders the page from the shell instead.
223
+ *
224
+ * @param {InertiaPage} page
225
+ * @param {string | undefined} requestUrl
226
+ * @param {ServerReads} serverFetch
227
+ * @param {string | undefined} requestCookies
228
+ * @param {{ ssr?: false }} [options]
229
+ * @returns {Promise<{ html: string, status: number, redirect?: string }>}
304
230
  */
305
231
  export async function renderHtml(
306
232
  page,
@@ -330,7 +256,7 @@ export async function renderHtml(
330
256
  .replace("<title>Antelope DMS</title>", () =>
331
257
  withFaviconPlaceholder(rendered.head.headTags),
332
258
  )
333
- .replace(/<html([^>]*)>/, (_, attributes) =>
259
+ .replace(/<html([^>]*)>/, (_, /** @type {string} */ attributes) =>
334
260
  htmlTag(attributes, rendered.head.htmlAttrs),
335
261
  )
336
262
  .replace("<body", `<body ${rendered.head.bodyAttrs}`)
@@ -343,6 +269,12 @@ export async function renderHtml(
343
269
  return { html, status: rendered.error?.statusCode ?? 200 };
344
270
  }
345
271
 
272
+ /**
273
+ * @param {InertiaPage} page
274
+ * @param {IncomingMessage} request
275
+ * @param {Pick<import("./server/route-rules.mjs").RouteRule, "ssr">} [rule]
276
+ * @param {ServerReads} [reads]
277
+ */
346
278
  function renderRequestHtml(page, request, rule = {}, reads) {
347
279
  const fetch = reads ?? serverComponentFetch(request);
348
280
  return renderHtml(page, request.url, fetch, request.headers.cookie, {
@@ -353,6 +285,11 @@ function renderRequestHtml(page, request, rule = {}, reads) {
353
285
  /**
354
286
  * Renders a page document, at the query the page completes as it renders:
355
287
  * see `renderInPlace`.
288
+ *
289
+ * @param {InertiaPage} page
290
+ * @param {IncomingMessage} request
291
+ * @param {import("./server/route-rules.mjs").MergedRouteRule} rule
292
+ * @param {import("./server/page-props.mjs").PageAuth} auth
356
293
  */
357
294
  function renderDocument(page, request, rule, auth) {
358
295
  const fetch = serverComponentFetch(request);
@@ -365,9 +302,14 @@ function renderDocument(page, request, rule, auth) {
365
302
  );
366
303
  }
367
304
 
305
+ /**
306
+ * @param {unknown} error
307
+ * @param {IncomingMessage} request
308
+ * @param {ServerResponse} response
309
+ */
368
310
  async function writeBackendError(error, request, response) {
369
311
  const auth = await currentAuth();
370
- const status = Number.isInteger(error?.status) ? error.status : 502;
312
+ const status = requestFailure.failureStatus(error);
371
313
  const pathname = new URL(request.url, "http://frontend.local").pathname;
372
314
  if (
373
315
  error instanceof BackendResponseError &&
@@ -410,6 +352,12 @@ async function writeBackendError(error, request, response) {
410
352
  response.end(JSON.stringify(payload));
411
353
  }
412
354
 
355
+ /**
356
+ * @param {ViteDevServer} devServer
357
+ * @param {IncomingMessage} request
358
+ * @param {ServerResponse} response
359
+ * @returns {Promise<boolean>}
360
+ */
413
361
  async function answeredByVite(devServer, request, response) {
414
362
  const result = await runViteMiddlewares(devServer, request, response);
415
363
  if (result.status === "failed") writeViteError(response, result.error);
@@ -422,6 +370,10 @@ async function answeredByVite(devServer, request, response) {
422
370
  * production server serves. Only a path with an extension names one.
423
371
  *
424
372
  * The path is decoded as Vite and the asset server read it: `%2E` is a dot.
373
+ *
374
+ * @param {ViteDevServer | undefined} devServer
375
+ * @param {string} requested
376
+ * @returns {boolean}
425
377
  */
426
378
  function hasRendererFile(devServer, requested) {
427
379
  let pathname;
@@ -445,7 +397,15 @@ function hasRendererFile(devServer, requested) {
445
397
  });
446
398
  }
447
399
 
448
- /** Vite's files in development, the build's in production; else a 404. */
400
+ /**
401
+ * Vite's files in development, the build's in production; else a 404.
402
+ *
403
+ * @param {ViteDevServer | undefined} devServer
404
+ * @param {IncomingMessage} request
405
+ * @param {ServerResponse} response
406
+ * @param {string} pathname
407
+ * @returns {Promise<void>}
408
+ */
449
409
  async function serveRendererFile(devServer, request, response, pathname) {
450
410
  const answered = devServer
451
411
  ? await answeredByVite(devServer, request, response)
@@ -455,11 +415,16 @@ async function serveRendererFile(devServer, request, response, pathname) {
455
415
  response.end("Not found");
456
416
  }
457
417
 
418
+ /** @param {ServerResponse} response */
458
419
  function writeNotFound(response) {
459
420
  response.writeHead(404, { "content-type": JSON_TYPE });
460
421
  response.end(JSON.stringify({ message: "Not found" }));
461
422
  }
462
423
 
424
+ /**
425
+ * @param {IncomingMessage} request
426
+ * @param {ServerResponse} response
427
+ */
463
428
  export async function handleRequest(request, response) {
464
429
  const url = new URL(request.url, "http://frontend.local");
465
430
  const pathname = url.pathname;
@@ -467,6 +432,7 @@ export async function handleRequest(request, response) {
467
432
  if (localRoute) return localRoute[1](request, response);
468
433
  const devServer = await developmentServer();
469
434
  const modules = await loadServerModules(devServer);
435
+ /** @param {string} path */
470
436
  const hasFile = (path) => hasRendererFile(devServer, path);
471
437
  const owner = requestOwner(request.method, pathname, modules, hasFile);
472
438
  if (devServer) response.setHeader("x-dms-route-owner", ownerLabel(owner));
@@ -474,6 +440,7 @@ export async function handleRequest(request, response) {
474
440
  return serveRendererFile(devServer, request, response, pathname);
475
441
  if (owner.kind === "module")
476
442
  return handleModuleRoute(request, response, pathname, modules);
443
+ /** @param {string} path */
477
444
  const isProxiedPath = (path) => reachesBackend(path, modules, hasFile);
478
445
  if (owner.kind === "backend")
479
446
  return proxy(request, response, owner.proxy, isProxiedPath);
@@ -487,8 +454,9 @@ export async function handleRequest(request, response) {
487
454
  if (redirectSignedOutPrivatePage(request, response, props, auth)) return;
488
455
  const page = createInertiaPage(request.url, props, undefined, onceProps);
489
456
  const rule = routeRuleFor(rules, pathname);
457
+ /** @param {number} status */
490
458
  const headersFor = (status) =>
491
- ruleHeaders(rule, request, props.page?.route, status);
459
+ ruleHeaders(rule, request, pageRoute(props), status);
492
460
  if (request.headers[INERTIA_HEADER]) {
493
461
  return writeContent(
494
462
  request,
@@ -513,6 +481,11 @@ export async function handleRequest(request, response) {
513
481
  );
514
482
  }
515
483
 
484
+ /**
485
+ * @param {unknown} error
486
+ * @param {IncomingMessage & { dmsSessionRetry?: boolean }} request
487
+ * @param {ServerResponse} response
488
+ */
516
489
  async function writeRequestFailure(error, request, response) {
517
490
  if (requestFailure.isResponseGone(error, response)) return;
518
491
  if (
@@ -531,14 +504,21 @@ async function writeRequestFailure(error, request, response) {
531
504
  await writeBackendError(error, request, response);
532
505
  }
533
506
 
534
- /** An error page failing too must not reject: that would end the process. */
507
+ /**
508
+ * An error page failing too must not reject: that would end the process.
509
+ *
510
+ * @param {IncomingMessage} request
511
+ * @param {ServerResponse} response
512
+ * @returns {Promise<void>}
513
+ */
535
514
  export async function handleRequestSafely(request, response) {
536
515
  try {
537
516
  if (refuseForeignTarget(request, response)) return;
538
517
  await handleRequest(request, response);
539
518
  } catch (error) {
540
- await writeRequestFailure(error, request, response).catch((failure) =>
541
- requestFailure.abandonResponse(failure, response),
519
+ await writeRequestFailure(error, request, response).catch(
520
+ /** @param {unknown} failure */ (failure) =>
521
+ requestFailure.abandonResponse(failure, response),
542
522
  );
543
523
  }
544
524
  }
@@ -553,6 +533,7 @@ if (
553
533
  );
554
534
  const port = Number(process.env.PORT ?? 3001);
555
535
  const host = listenHost();
536
+ /** @param {NodeJS.ErrnoException} error */
556
537
  const onListenError = (error) => void reportListenError(error, host, port);
557
538
  frontendHttpServer.once("error", onListenError);
558
539
  frontendHttpServer.listen(port, host, () => {
@@ -587,7 +568,10 @@ async function announceReady() {
587
568
  await reportStartError(error);
588
569
  return;
589
570
  }
590
- const { address, port } = frontendHttpServer.address();
571
+ const listening = frontendHttpServer.address();
572
+ if (listening === null || isString(listening))
573
+ throw new TypeError("The DMS server is not listening on a port");
574
+ const { address, port } = listening;
591
575
  await reportReady(address, port);
592
576
  await reportRouting();
593
577
  if (process.env.DMS_DEV === "true") watchEmailBundle();
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { isFunction, isObjectLike, isString } from "./server/type-guards.mjs";
4
5
 
5
6
  /**
6
7
  * The very unimport the auto-import plugin scans with, so both read the
@@ -12,14 +13,83 @@ const unimportUrl = pathToFileURL(
12
13
  createRequire(import.meta.url).resolve("unplugin-auto-import/package.json"),
13
14
  ).resolve("unimport"),
14
15
  );
15
- const { builtinPresets, resolvePreset, scanDirExports } = await import(
16
- unimportUrl.href
17
- );
16
+ /** @type {unknown} */
17
+ const unimport = await import(unimportUrl.href);
18
+
19
+ /**
20
+ * An export of unimport, which this package does not depend on and so has no
21
+ * types for.
22
+ *
23
+ * @param {string} name
24
+ * @returns {unknown}
25
+ */
26
+ function unimportExport(name) {
27
+ return isObjectLike(unimport) ? unimport[name] : undefined;
28
+ }
29
+
30
+ /**
31
+ * The function unimport exports as `name`.
32
+ *
33
+ * @param {string} name
34
+ * @returns {Function}
35
+ */
36
+ function unimportFunction(name) {
37
+ const exported = unimportExport(name);
38
+ if (!isFunction(exported))
39
+ throw new Error(`unimport exports no function ${name}`);
40
+ return exported;
41
+ }
42
+
43
+ /**
44
+ * A name unimport scanned or resolved: where it comes from, what it is called
45
+ * and whether it is only a type.
46
+ *
47
+ * @typedef {object} ImportedName
48
+ * @property {string} name
49
+ * @property {string} from
50
+ * @property {string} [as]
51
+ */
52
+
53
+ /**
54
+ * @param {unknown} entry
55
+ * @returns {entry is ImportedName}
56
+ */
57
+ function isImportedName(entry) {
58
+ return (
59
+ isObjectLike(entry) &&
60
+ isString(entry.name) &&
61
+ isString(entry.from) &&
62
+ (entry.as === undefined || isString(entry.as))
63
+ );
64
+ }
65
+
66
+ /**
67
+ * The names of type declarations among `entries`, as unimport lists them.
68
+ *
69
+ * @param {unknown} entries
70
+ * @returns {ImportedName[]}
71
+ */
72
+ function typeNames(entries) {
73
+ if (!Array.isArray(entries)) throw new Error("unimport listed no names");
74
+ /** @type {unknown[]} */
75
+ const listed = entries;
76
+ /** @type {ImportedName[]} */
77
+ const names = [];
78
+ for (const entry of listed) {
79
+ if (!isObjectLike(entry)) throw new Error("unimport listed a non-name");
80
+ if (!entry.type) continue;
81
+ if (!isImportedName(entry))
82
+ throw new Error("unimport listed a type without a name and a source");
83
+ names.push(entry);
84
+ }
85
+ return names;
86
+ }
18
87
 
19
88
  const HEADER =
20
89
  "// Generated by @antelopejs/dms-frontend from the auto-imported types, for\n" +
21
90
  "// Vue's SFC compiler (script.globalTypeFiles). Not read by TypeScript.\n";
22
91
 
92
+ /** @param {string} path */
23
93
  const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "");
24
94
 
25
95
  /**
@@ -45,13 +115,22 @@ const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "");
45
115
  * @param {(string | { glob: string, types?: boolean })[]} dirs the auto-import
46
116
  * directories, as handed to the auto-import plugin
47
117
  * @param {string} cwd the workspace
118
+ * @returns {Promise<string>}
48
119
  */
49
120
  export async function sfcGlobalTypes(dirs, cwd) {
50
- const vue = await resolvePreset(builtinPresets.vue);
51
- const scanned = await scanDirExports(dirs, { cwd, types: true });
121
+ const presets = unimportExport("builtinPresets");
122
+ /** @type {unknown} */
123
+ const vue = await unimportFunction("resolvePreset")(
124
+ isObjectLike(presets) ? presets.vue : undefined,
125
+ );
126
+ /** @type {unknown} */
127
+ const scanned = await unimportFunction("scanDirExports")(dirs, {
128
+ cwd,
129
+ types: true,
130
+ });
131
+ /** @type {Map<string, string>} */
52
132
  const aliases = new Map();
53
- for (const entry of [...vue, ...scanned]) {
54
- if (!entry.type) continue;
133
+ for (const entry of [...typeNames(vue), ...typeNames(scanned)]) {
55
134
  const name = entry.as ?? entry.name;
56
135
  const source = JSON.stringify(withoutExtension(entry.from));
57
136
  aliases.set(
@@ -66,8 +145,13 @@ export async function sfcGlobalTypes(dirs, cwd) {
66
145
  * Written through a temporary file: a build starting meanwhile reads the
67
146
  * previous declarations or the new ones, never a half-written file the
68
147
  * compiler would then keep in its cache.
148
+ *
149
+ * @param {string} file
150
+ * @param {string} content
151
+ * @returns {boolean}
69
152
  */
70
153
  function writeIfChanged(file, content) {
154
+ /** @type {string | undefined} */
71
155
  let current;
72
156
  try {
73
157
  current = readFileSync(file, "utf8");
@@ -81,7 +165,14 @@ function writeIfChanged(file, content) {
81
165
  return true;
82
166
  }
83
167
 
84
- /** Writes the declarations once, before Vite's config is returned. */
168
+ /**
169
+ * Writes the declarations once, before Vite's config is returned.
170
+ *
171
+ * @param {(string | { glob: string, types?: boolean })[]} dirs
172
+ * @param {string} cwd
173
+ * @param {string} file
174
+ * @returns {Promise<void>}
175
+ */
85
176
  export async function writeSfcGlobalTypes(dirs, cwd, file) {
86
177
  writeIfChanged(file, await sfcGlobalTypes(dirs, cwd));
87
178
  }
@@ -94,24 +185,32 @@ export async function writeSfcGlobalTypes(dirs, cwd, file) {
94
185
  * The file is under the workspace, which Vite watches: rewriting it is the
95
186
  * change the Vue plugin reloads the dependent components on. A failed scan or
96
187
  * write keeps the previous declarations, since it must not end the dev server.
188
+ *
189
+ * @param {(string | { glob: string, types?: boolean })[]} dirs
190
+ * @param {string} cwd
191
+ * @param {string} file
192
+ * @returns {import("vite").Plugin}
97
193
  */
98
194
  export function sfcGlobalTypesWatcher(dirs, cwd, file) {
99
195
  return {
100
196
  name: "dms-sfc-global-types",
101
197
  apply: "serve",
102
198
  configureServer(server) {
199
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
103
200
  let pending;
201
+ /** @param {string} path */
104
202
  const refresh = (path) => {
105
203
  if (!/\.[cm]?[jt]sx?$/.test(path) || path === file) return;
106
204
  clearTimeout(pending);
107
205
  pending = setTimeout(() => {
108
206
  sfcGlobalTypes(dirs, cwd)
109
207
  .then((content) => writeIfChanged(file, content))
110
- .catch((error) =>
111
- server.config.logger.warn(
112
- `Could not update ${file}, the types Vue's SFC compiler ` +
113
- `reads: ${error instanceof Error ? error.message : error}`,
114
- ),
208
+ .catch(
209
+ /** @param {unknown} error */ (error) =>
210
+ server.config.logger.warn(
211
+ `Could not update ${file}, the types Vue's SFC compiler ` +
212
+ `reads: ${error instanceof Error ? error.message : String(error)}`,
213
+ ),
115
214
  );
116
215
  }, 100);
117
216
  };