@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,6 +2,21 @@ import { resolveBackendProxies } from "./backend-proxies.mjs";
2
2
  import { compilePathPattern } from "./path-patterns.mjs";
3
3
  import { matchPublicEndpoint } from "./public-endpoints.mjs";
4
4
  import { ruleRejection } from "./route-rules.mjs";
5
+ import {
6
+ isBoolean,
7
+ isFunction,
8
+ isNonNullObject,
9
+ isString,
10
+ } from "./type-guards.mjs";
11
+
12
+ /** @typedef {(message: string) => void} Warn */
13
+ /**
14
+ * @import {
15
+ * AuthProvider, DeclaredModule, ModuleDeclared, RouteRegistration,
16
+ * ServerDeclarations, ServerModuleDeclaration, ServerRoute
17
+ * } from "./module-route-types.mjs"
18
+ * @import { PublicEndpoint } from "./public-endpoints.mjs"
19
+ */
5
20
 
6
21
  const METHODS = new Set([
7
22
  "GET",
@@ -29,6 +44,8 @@ const MODULE_API_PREFIX = /^\/api\/_[A-Za-z0-9-]+(?:\/|$)/;
29
44
  * Whether `pathname` names one of the files the renderer serves: Vite's URLs,
30
45
  * the built assets and the dependencies. No server route is ever there, and a
31
46
  * development page requests hundreds of them.
47
+ *
48
+ * @param {string} pathname
32
49
  */
33
50
  export function isRendererFilePath(pathname) {
34
51
  return ["/@", "/assets/", "/node_modules/"].some((prefix) =>
@@ -36,20 +53,25 @@ export function isRendererFilePath(pathname) {
36
53
  );
37
54
  }
38
55
 
39
- /** Whether a module route may serve `pathname`: never the renderer's own. */
56
+ /**
57
+ * Whether a module route may serve `pathname`: never the renderer's own.
58
+ *
59
+ * @param {string} pathname
60
+ */
40
61
  export function mayHaveModuleRoute(pathname) {
41
62
  return !RESERVED_PREFIXES.some(
42
63
  (prefix) => pathname === prefix.slice(0, -1) || pathname.startsWith(prefix),
43
64
  );
44
65
  }
45
66
 
67
+ /** @param {string} path */
46
68
  function normalizedPath(path) {
47
69
  return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
48
70
  }
49
71
 
72
+ /** @param {string} path */
50
73
  function pathRejection(path) {
51
- if (typeof path !== "string" || !path.startsWith("/"))
52
- return "a path starts with /";
74
+ if (!isString(path) || !path.startsWith("/")) return "a path starts with /";
53
75
  const normalized = normalizedPath(path);
54
76
  if (!mayHaveModuleRoute(normalized))
55
77
  return `${RESERVED_PREFIXES.join(", ")} are the renderer's own paths`;
@@ -71,14 +93,11 @@ function pathRejection(path) {
71
93
  return undefined;
72
94
  }
73
95
 
96
+ /** @param {RouteRegistration["options"]} options */
74
97
  function optionsRejection(options) {
75
98
  if (options === undefined) return undefined;
76
- if (typeof options !== "object" || options === null)
77
- return "its options are not an object";
78
- if (
79
- options.sameOrigin !== undefined &&
80
- typeof options.sameOrigin !== "boolean"
81
- )
99
+ if (!isNonNullObject(options)) return "its options are not an object";
100
+ if (options.sameOrigin !== undefined && !isBoolean(options.sameOrigin))
82
101
  return "sameOrigin is not a boolean";
83
102
  if (
84
103
  options.bodyLimit !== undefined &&
@@ -90,11 +109,14 @@ function optionsRejection(options) {
90
109
 
91
110
  /**
92
111
  * Two declarations of one path whatever their parameters are named share a
93
- * shape.
112
+ * path identity.
113
+ *
114
+ * @param {string} path
94
115
  */
95
116
  function compilePath(path) {
96
117
  const normalized = normalizedPath(path);
97
118
  const segments = normalized === "/" ? [] : normalized.slice(1).split("/");
119
+ /** @type {string[]} */
98
120
  const params = [];
99
121
  const pattern = segments
100
122
  .map((segment) => {
@@ -106,7 +128,7 @@ function compilePath(path) {
106
128
  .join("");
107
129
  return {
108
130
  path: normalized,
109
- shape: segments
131
+ pathIdentity: segments
110
132
  .map((segment) => (PARAM_SEGMENT.test(segment) ? ":" : segment))
111
133
  .join("/"),
112
134
  params,
@@ -114,8 +136,14 @@ function compilePath(path) {
114
136
  };
115
137
  }
116
138
 
117
- /** What a module's setup declares, by kind, in declaration order. */
139
+ /**
140
+ * What a module's setup declares, by kind, in declaration order.
141
+ *
142
+ * @param {ServerModuleDeclaration["setup"]} setup
143
+ * @returns {ModuleDeclared}
144
+ */
118
145
  function collectDeclarations(setup) {
146
+ /** @type {ModuleDeclared} */
119
147
  const declared = { routes: [], auth: [], rules: [], proxies: [] };
120
148
  setup({
121
149
  registerRoute: (method, path, handler, options) =>
@@ -128,27 +156,37 @@ function collectDeclarations(setup) {
128
156
  return declared;
129
157
  }
130
158
 
159
+ /** @param {unknown} method */
131
160
  function methodsOf(method) {
161
+ /** @type {readonly unknown[]} */
132
162
  const methods = Array.isArray(method) ? method : [method];
133
163
  return methods.length && methods.every((name) => METHODS.has(name))
134
164
  ? methods
135
165
  : undefined;
136
166
  }
137
167
 
168
+ /** @param {RouteRegistration} registration */
138
169
  function registrationRejection(registration) {
139
170
  if (!methodsOf(registration.method))
140
171
  return `the method is not one of ${[...METHODS].join(", ")}`;
141
- if (typeof registration.handler !== "function")
142
- return "its handler is not a function";
172
+ if (!isFunction(registration.handler)) return "its handler is not a function";
143
173
  return (
144
174
  pathRejection(registration.path) ?? optionsRejection(registration.options)
145
175
  );
146
176
  }
147
177
 
178
+ /**
179
+ * @param {RouteRegistration["method"]} method
180
+ * @param {string} path
181
+ */
148
182
  function describeRoute(method, path) {
149
183
  return `"${[method].flat().join(", ")} ${path}"`;
150
184
  }
151
185
 
186
+ /**
187
+ * @param {string} id
188
+ * @param {ServerRoute} route
189
+ */
152
190
  function backendShadowWarning(id, route) {
153
191
  if (
154
192
  !BACKEND_PREFIX.test(route.path) ||
@@ -163,6 +201,11 @@ function backendShadowWarning(id, route) {
163
201
  );
164
202
  }
165
203
 
204
+ /**
205
+ * @param {string} id
206
+ * @param {ServerRoute} route
207
+ * @param {PublicEndpoint[]} publicEndpoints
208
+ */
166
209
  function publicEndpointWarning(id, route, publicEndpoints) {
167
210
  const sample = route.path.replaceAll(/\/:[^/]+/g, "/x");
168
211
  if (!matchPublicEndpoint(route.method, sample, publicEndpoints))
@@ -173,7 +216,12 @@ function publicEndpointWarning(id, route, publicEndpoints) {
173
216
  );
174
217
  }
175
218
 
176
- /** Runs a module's setup; a setup that throws declares nothing. */
219
+ /**
220
+ * Runs a module's setup; a setup that throws declares nothing.
221
+ *
222
+ * @param {ServerModuleDeclaration} declaration
223
+ * @param {Warn} warn
224
+ */
177
225
  function declarationsOfModule(declaration, warn) {
178
226
  try {
179
227
  return collectDeclarations(declaration.setup);
@@ -185,6 +233,11 @@ function declarationsOfModule(declaration, warn) {
185
233
  }
186
234
  }
187
235
 
236
+ /**
237
+ * @param {DeclaredModule} module
238
+ * @param {Warn} warn
239
+ * @returns {ServerRoute[]}
240
+ */
188
241
  function routesOfModule(module, warn) {
189
242
  const { id, priority = 0 } = module;
190
243
  return module.declared.routes.flatMap((registration, order) => {
@@ -210,6 +263,10 @@ function routesOfModule(module, warn) {
210
263
  });
211
264
  }
212
265
 
266
+ /**
267
+ * @param {{ id: string, priority?: number }} left
268
+ * @param {{ id: string, priority?: number }} right
269
+ */
213
270
  function byPriority(left, right) {
214
271
  return (
215
272
  (right.priority ?? 0) - (left.priority ?? 0) ||
@@ -217,6 +274,10 @@ function byPriority(left, right) {
217
274
  );
218
275
  }
219
276
 
277
+ /**
278
+ * @param {ServerRoute} kept
279
+ * @param {ServerRoute} shadowed
280
+ */
220
281
  function shadowWarning(kept, shadowed) {
221
282
  const settled =
222
283
  kept.priority === shadowed.priority
@@ -228,6 +289,7 @@ function shadowWarning(kept, shadowed) {
228
289
  );
229
290
  }
230
291
 
292
+ /** @param {ServerRoute} route */
231
293
  function parameterCount(route) {
232
294
  return route.params.length;
233
295
  }
@@ -235,10 +297,15 @@ function parameterCount(route) {
235
297
  /**
236
298
  * Whether one request path can match both routes: as many segments, and at
237
299
  * each position the same literal or a parameter on either side.
300
+ *
301
+ * @param {ServerRoute} left
302
+ * @param {ServerRoute} right
238
303
  */
239
304
  function routesOverlap(left, right) {
240
- const leftSegments = left.shape === "" ? [] : left.shape.split("/");
241
- const rightSegments = right.shape === "" ? [] : right.shape.split("/");
305
+ const leftSegments =
306
+ left.pathIdentity === "" ? [] : left.pathIdentity.split("/");
307
+ const rightSegments =
308
+ right.pathIdentity === "" ? [] : right.pathIdentity.split("/");
242
309
  return (
243
310
  leftSegments.length === rightSegments.length &&
244
311
  leftSegments.every(
@@ -250,7 +317,12 @@ function routesOverlap(left, right) {
250
317
  );
251
318
  }
252
319
 
253
- /** `first` is tried before `second`: it answers what both can match. */
320
+ /**
321
+ * `first` is tried before `second`: it answers what both can match.
322
+ *
323
+ * @param {ServerRoute} first
324
+ * @param {ServerRoute} second
325
+ */
254
326
  function overlapWarning(first, second) {
255
327
  const settled =
256
328
  parameterCount(first) < parameterCount(second)
@@ -265,6 +337,7 @@ function overlapWarning(first, second) {
265
337
  );
266
338
  }
267
339
 
340
+ /** @param {ServerRoute} route */
268
341
  function rootParameterWarning(route) {
269
342
  return (
270
343
  `Frontend module ${route.id} serves ${describeRoute(route.method, route.path)}, whose first ` +
@@ -276,10 +349,13 @@ function rootParameterWarning(route) {
276
349
  /**
277
350
  * Reports what the order of `routes` settles silently: two modules' routes
278
351
  * that can match one request, and a route that catches every first segment.
352
+ *
353
+ * @param {ServerRoute[]} routes
354
+ * @param {Warn} warn
279
355
  */
280
356
  function warnOverlaps(routes, warn) {
281
357
  for (const [index, route] of routes.entries()) {
282
- if (route.shape.startsWith(":")) warn(rootParameterWarning(route));
358
+ if (route.pathIdentity.startsWith(":")) warn(rootParameterWarning(route));
283
359
  for (const later of routes.slice(index + 1))
284
360
  if (
285
361
  later.method === route.method &&
@@ -290,11 +366,17 @@ function warnOverlaps(routes, warn) {
290
366
  }
291
367
  }
292
368
 
369
+ /**
370
+ * @param {DeclaredModule[]} modules
371
+ * @param {Warn} warn
372
+ * @param {PublicEndpoint[]} publicEndpoints
373
+ */
293
374
  function resolveRoutes(modules, warn, publicEndpoints) {
375
+ /** @type {Map<string, ServerRoute>} */
294
376
  const kept = new Map();
295
377
  for (const module of modules) {
296
378
  for (const route of routesOfModule(module, warn)) {
297
- const key = `${route.method} ${route.shape}`;
379
+ const key = `${route.method} ${route.pathIdentity}`;
298
380
  const current = kept.get(key);
299
381
  if (current) {
300
382
  warn(shadowWarning(current, route));
@@ -319,14 +401,15 @@ function resolveRoutes(modules, warn, publicEndpoints) {
319
401
 
320
402
  const AUTH_HOOKS = ["refreshSession", "publicSession"];
321
403
 
404
+ /** @param {unknown} auth */
322
405
  function authRejection(auth) {
323
- if (typeof auth !== "object" || auth === null) return "it is not an object";
406
+ if (!isNonNullObject(auth)) return "it is not an object";
324
407
  for (const hook of AUTH_HOOKS)
325
- if (auth[hook] !== undefined && typeof auth[hook] !== "function")
408
+ if (auth[hook] !== undefined && !isFunction(auth[hook]))
326
409
  return `${hook} is not a function`;
327
410
  if (
328
411
  auth.signInPath !== undefined &&
329
- (typeof auth.signInPath !== "string" ||
412
+ (!isString(auth.signInPath) ||
330
413
  !compilePathPattern(auth.signInPath) ||
331
414
  auth.signInPath.includes("*"))
332
415
  )
@@ -345,8 +428,12 @@ function authRejection(auth) {
345
428
  /**
346
429
  * The auth hooks: those of the highest-priority module that registers them.
347
430
  * A module registering them twice, or after another one, is reported.
431
+ *
432
+ * @param {DeclaredModule[]} modules
433
+ * @param {Warn} warn
348
434
  */
349
435
  function resolveAuth(modules, warn) {
436
+ /** @type {AuthProvider | undefined} */
350
437
  let provider;
351
438
  for (const module of modules) {
352
439
  for (const auth of module.declared.auth) {
@@ -379,12 +466,16 @@ function resolveAuth(modules, warn) {
379
466
  /**
380
467
  * The route rules: of two declarations of the same pattern, the module of
381
468
  * higher priority keeps it, with a warning.
469
+ *
470
+ * @param {DeclaredModule[]} modules
471
+ * @param {Warn} warn
382
472
  */
383
473
  function resolveRules(modules, warn) {
474
+ /** @type {Map<string, import("./route-rules.mjs").ResolvedRouteRule>} */
384
475
  const kept = new Map();
385
476
  for (const module of modules) {
386
477
  for (const rules of module.declared.rules) {
387
- if (typeof rules !== "object" || rules === null) {
478
+ if (!isNonNullObject(rules)) {
388
479
  warn(
389
480
  `Frontend module ${module.id} declares route rules that are ignored: they are not an object.`,
390
481
  );
@@ -435,12 +526,13 @@ function resolveRules(modules, warn) {
435
526
  * request reaches. Pure: the server hands it the declarations it loaded, and
436
527
  * `module-event.mjs` runs the handler a request reaches.
437
528
  *
438
- * @param declarations `{ id, priority, setup }` per module with a dms.server.ts
439
- * @param warn Reports a declaration problem
440
- * @param publicEndpoints The deployment's public endpoints (#108), to report a
529
+ * @param {readonly ServerModuleDeclaration[]} declarations `{ id, priority, setup }` per module with a dms.server.ts
530
+ * @param {Warn} warn Reports a declaration problem
531
+ * @param {PublicEndpoint[]} [publicEndpoints] The deployment's public endpoints (#108), to report a
441
532
  * module route answering one
442
- * @param manifestProxies `{ module, path, auth? }`, the backend proxies the
533
+ * @param {readonly import("./backend-proxies.mjs").ManifestProxy[]} [manifestProxies] `{ module, path, auth? }`, the backend proxies the
443
534
  * backend modules declare in the manifest
535
+ * @returns {ServerDeclarations}
444
536
  */
445
537
  export function resolveServerDeclarations(
446
538
  declarations,
@@ -466,7 +558,12 @@ export function resolveServerDeclarations(
466
558
  };
467
559
  }
468
560
 
561
+ /**
562
+ * @param {ServerRoute} route
563
+ * @param {RegExpExecArray} match
564
+ */
469
565
  function decodedParams(route, match) {
566
+ /** @type {Record<string, string>} */
470
567
  const params = {};
471
568
  for (const [index, name] of route.params.entries()) {
472
569
  try {
@@ -483,9 +580,9 @@ function decodedParams(route, match) {
483
580
  *
484
581
  * A `HEAD` no route declares is the `GET`'s, without its body.
485
582
  *
486
- * @param routes What `resolveServerDeclarations` returned
487
- * @param method The request's method
488
- * @param pathname The request's normalized path, without its query string
583
+ * @param {ServerRoute[]} routes What `resolveServerDeclarations` returned
584
+ * @param {string} method The request's method
585
+ * @param {string} pathname The request's normalized path, without its query string
489
586
  */
490
587
  export function matchModuleRoute(routes, method, pathname) {
491
588
  const reached = matchMethod(routes, method, pathname);
@@ -493,6 +590,12 @@ export function matchModuleRoute(routes, method, pathname) {
493
590
  return matchMethod(routes, "GET", pathname);
494
591
  }
495
592
 
593
+ /**
594
+ * @param {ServerRoute[]} routes
595
+ * @param {string} method
596
+ * @param {string} pathname
597
+ * @returns {{ route: ServerRoute, params: Record<string, string> } | undefined}
598
+ */
496
599
  function matchMethod(routes, method, pathname) {
497
600
  for (const route of routes) {
498
601
  if (route.method !== method) continue;
@@ -507,8 +610,12 @@ function matchMethod(routes, method, pathname) {
507
610
  /**
508
611
  * The methods the module routes answer on `pathname`: a path a module route
509
612
  * serves is that module's for every method, the others answered `405`.
613
+ *
614
+ * @param {ServerRoute[]} routes
615
+ * @param {string} pathname
510
616
  */
511
617
  export function moduleRouteMethods(routes, pathname) {
618
+ /** @type {Set<string>} */
512
619
  const methods = new Set();
513
620
  for (const route of routes)
514
621
  if (route.matcher.test(pathname)) methods.add(route.method);
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { allowedEstablishEndpoints } from "./auth/establish-endpoints.mjs";
4
4
  import { serverAuth } from "./auth-hooks.mjs";
5
5
  import { answerModuleRoute } from "./module-event.mjs";
6
+ import { isNonNullObject, isString } from "./type-guards.mjs";
6
7
  import {
7
8
  matchModuleRoute,
8
9
  mayHaveModuleRoute,
@@ -16,7 +17,30 @@ import {
16
17
  } from "./public-endpoints.mjs";
17
18
  import { reachesBackend } from "./request-routing.mjs";
18
19
 
20
+ import { frontendRegistryOf, serverOptionsOf } from "./generated-files.mjs";
19
21
  import { publicOptionsOf } from "./public-options.mjs";
22
+ /** @typedef {import("./module-route-types.mjs").ServerModuleDeclaration} ServerModuleDeclaration */
23
+ /**
24
+ * What a handler of a module reads besides its request.
25
+ *
26
+ * @typedef {object} ModuleContext
27
+ * @property {{ id: string, options: unknown, privateOptions: unknown }} module
28
+ * @property {{ authEstablishEndpoints: readonly string[] }} app
29
+ * @property {string | undefined} authOwner The module that owns sign-in
30
+ */
31
+ /**
32
+ * A deployment's data, which the server hands to a set of server modules.
33
+ *
34
+ * @typedef {object} Deployment
35
+ * @property {Record<string, unknown>} [publicOptions] Each module's public options by id
36
+ * @property {Record<string, unknown>} [privateOptions] Each module's private options by id
37
+ * @property {() => readonly string[]} [authEstablishEndpoints]
38
+ * @property {import("./public-endpoints.mjs").PublicEndpoint[]} [publicEndpoints]
39
+ * @property {import("./backend-proxies.mjs").ManifestProxy[]} [backendProxies]
40
+ * @property {(message: string) => void} warn
41
+ */
42
+ /** @typedef {ReturnType<typeof createServerModules>} ServerModules */
43
+
20
44
  const WORKSPACE_ROOT = new URL("../", import.meta.url);
21
45
  const SOURCE_LOADER = "/server-modules.generated.ts";
22
46
  const BUILT_LOADER = new URL(
@@ -32,11 +56,20 @@ const SERVER_OPTIONS_FILE = new URL(
32
56
  WORKSPACE_ROOT,
33
57
  );
34
58
 
59
+ /** @type {Promise<ServerModuleDeclaration[]> | undefined} */
35
60
  let builtDeclarations;
61
+ /** @type {ServerModuleDeclaration[]} */
36
62
  let lastDeclarations = [];
63
+ /** @type {string | undefined} */
37
64
  let lastLoadError;
65
+ /** @type {WeakMap<ServerModuleDeclaration[], ServerModules>} */
38
66
  const resolved = new WeakMap();
39
67
 
68
+ /**
69
+ * @param {URL} url
70
+ * @param {unknown} fallback
71
+ * @returns {unknown} The file's JSON, the fallback when it cannot be read
72
+ */
40
73
  function readJsonFile(url, fallback) {
41
74
  try {
42
75
  return JSON.parse(readFileSync(url, "utf8"));
@@ -47,18 +80,29 @@ function readJsonFile(url, fallback) {
47
80
 
48
81
  const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
49
82
 
50
- /** The object holding `path`'s last key, when every key up to it is own. */
83
+ /**
84
+ * The object holding `path`'s last key, when every key up to it is own.
85
+ *
86
+ * @param {Record<string, unknown>} target
87
+ * @param {string[]} path
88
+ */
51
89
  function holderOf(target, path) {
52
90
  let node = target;
53
91
  for (const key of path.slice(0, -1)) {
54
92
  if (UNSAFE_KEYS.has(key) || !Object.hasOwn(node, key)) return undefined;
55
93
  node = node[key];
56
- if (typeof node !== "object" || node === null) return undefined;
94
+ if (!isNonNullObject(node)) return undefined;
57
95
  }
58
96
  return UNSAFE_KEYS.has(path.at(-1)) ? undefined : node;
59
97
  }
60
98
 
61
- /** Sets `value` at `path` when the object holding it exists but lacks it. */
99
+ /**
100
+ * Sets `value` at `path` when the object holding it exists but lacks it.
101
+ *
102
+ * @param {Record<string, unknown>} target
103
+ * @param {string[]} path
104
+ * @param {string} value
105
+ */
62
106
  function fillPath(target, path, value) {
63
107
  const holder = holderOf(target, path);
64
108
  const key = path.at(-1);
@@ -66,7 +110,13 @@ function fillPath(target, path, value) {
66
110
  holder[key] = value;
67
111
  }
68
112
 
69
- /** Sets `value` at `path` only where the build left an empty string. */
113
+ /**
114
+ * Sets `value` at `path` only where the build left an empty string.
115
+ *
116
+ * @param {Record<string, unknown>} target
117
+ * @param {string[]} path
118
+ * @param {string} value
119
+ */
70
120
  function fillEmptied(target, path, value) {
71
121
  const holder = holderOf(target, path);
72
122
  const key = path.at(-1);
@@ -79,10 +129,10 @@ function fillEmptied(target, path, value) {
79
129
  * the environment variable of a secret (`DMS_HTML_RENDER_SECRET`,
80
130
  * `DMS_OAUTH_RELAY_SECRET`) only fills in a section that carries none.
81
131
  *
82
- * @param modules The private options by module id, as the manifest published
132
+ * @param {Record<string, Record<string, unknown>>} modules The private options by module id, as the manifest published
83
133
  * them
84
- * @param environment The variables by the path of the secret they stand for
85
- * @param env The process environment
134
+ * @param {Record<string, string[]>} environment The variables by the path of the secret they stand for
135
+ * @param {NodeJS.ProcessEnv} env The process environment
86
136
  */
87
137
  export function privateOptionsWithFallbacks(modules, environment, env) {
88
138
  for (const options of Object.values(modules))
@@ -91,10 +141,16 @@ export function privateOptionsWithFallbacks(modules, environment, env) {
91
141
  return modules;
92
142
  }
93
143
 
94
- /** Each string leaf of `value`, array items included, with its path. */
144
+ /**
145
+ * Each string leaf of `value`, array items included, with its path.
146
+ *
147
+ * @param {unknown} value
148
+ * @param {string[]} [path]
149
+ * @returns {Generator<[string[], string]>}
150
+ */
95
151
  function* stringLeaves(value, path = []) {
96
- if (typeof value === "string") yield [path, value];
97
- else if (typeof value === "object" && value !== null)
152
+ if (isString(value)) yield [path, value];
153
+ else if (isNonNullObject(value))
98
154
  for (const [key, child] of Object.entries(value))
99
155
  yield* stringLeaves(child, [...path, key]);
100
156
  }
@@ -107,19 +163,20 @@ function* stringLeaves(value, path = []) {
107
163
  * a value the build emptied is filled, array items included: a value the
108
164
  * manifest carries wins, and no option or section is added or replaced.
109
165
  *
110
- * @param modules The private options by module id
111
- * @param json The variable's value; nothing is filled when unset
112
- * @param warn Reports a value that is not a JSON object, never its content
166
+ * @param {Record<string, Record<string, unknown>>} modules The private options by module id
167
+ * @param {string | undefined} json The variable's value; nothing is filled when unset
168
+ * @param {(message: string) => void} warn Reports a value that is not a JSON object, never its content
113
169
  */
114
170
  export function privateOptionsFromJson(modules, json, warn) {
115
171
  if (!json) return modules;
172
+ /** @type {unknown} */
116
173
  let given;
117
174
  try {
118
175
  given = JSON.parse(json);
119
176
  } catch {
120
177
  given = undefined;
121
178
  }
122
- if (typeof given !== "object" || given === null || Array.isArray(given)) {
179
+ if (!isNonNullObject(given) || Array.isArray(given)) {
123
180
  warn("DMS_PRIVATE_OPTIONS is not a JSON object of options by module id");
124
181
  return modules;
125
182
  }
@@ -131,10 +188,10 @@ export function privateOptionsFromJson(modules, json, warn) {
131
188
  return modules;
132
189
  }
133
190
 
191
+ /** @param {NodeJS.ProcessEnv} [env] */
134
192
  function readPrivateOptions(env = process.env) {
135
- const { modules = {}, environment = {} } = readJsonFile(
136
- SERVER_OPTIONS_FILE,
137
- {},
193
+ const { modules, environment } = serverOptionsOf(
194
+ readJsonFile(SERVER_OPTIONS_FILE, {}),
138
195
  );
139
196
  return privateOptionsFromJson(
140
197
  privateOptionsWithFallbacks(modules, environment, env),
@@ -144,14 +201,22 @@ function readPrivateOptions(env = process.env) {
144
201
  }
145
202
 
146
203
  function readPublicOptions() {
147
- return publicOptionsOf(readJsonFile(REGISTRY_FILE, { modules: [] }));
204
+ return publicOptionsOf(
205
+ frontendRegistryOf(readJsonFile(REGISTRY_FILE, { modules: [] })),
206
+ );
148
207
  }
149
208
 
209
+ /**
210
+ * @param {import("vite").ViteDevServer | undefined} devServer
211
+ * @returns {Promise<ServerModuleDeclaration[]>}
212
+ */
150
213
  async function loadDeclarations(devServer) {
151
214
  if (devServer)
215
+ // oxlint-disable-next-line typescript/no-unsafe-return -- `ssrLoadModule` types every module `Record<string, any>`; the loader is the generated `server-modules.generated.ts`, which exports `serverModules`.
152
216
  return (await devServer.ssrLoadModule(SOURCE_LOADER)).serverModules;
153
217
  if (!existsSync(fileURLToPath(BUILT_LOADER))) return [];
154
218
  builtDeclarations ??= import(BUILT_LOADER.href).then(
219
+ // oxlint-disable-next-line typescript/no-unsafe-return -- the build of that same generated module is imported by a URL computed at run time, so it is untyped.
155
220
  (loaded) => loaded.serverModules,
156
221
  );
157
222
  return builtDeclarations;
@@ -161,6 +226,8 @@ async function loadDeclarations(devServer) {
161
226
  * The declarations to serve. A module entry that fails to load in development
162
227
  * (a syntax error being typed) is reported once, and the last routes that
163
228
  * loaded keep answering meanwhile.
229
+ *
230
+ * @param {import("vite").ViteDevServer | undefined} devServer
164
231
  */
165
232
  async function currentDeclarations(devServer) {
166
233
  try {
@@ -181,8 +248,8 @@ async function currentDeclarations(devServer) {
181
248
  *
182
249
  * `authOwner` is the module whose sealed cookies are the `dms_*` ones.
183
250
  *
184
- * @param declarations `{ id, priority, setup }` per module with a dms.server.ts
185
- * @param deployment `{ publicOptions, privateOptions, authEstablishEndpoints,
251
+ * @param {readonly ServerModuleDeclaration[]} declarations `{ id, priority, setup }` per module with a dms.server.ts
252
+ * @param {Deployment} deployment `{ publicOptions, privateOptions, authEstablishEndpoints,
186
253
  * publicEndpoints, backendProxies, warn }`: each module's options by id,
187
254
  * the session-opening endpoints as a function (the environment can change
188
255
  * them), the public endpoints, the backend proxies the manifest declares
@@ -198,7 +265,7 @@ export function createServerModules(declarations, deployment) {
198
265
  deployment.publicEndpoints ?? [],
199
266
  deployment.backendProxies ?? [],
200
267
  );
201
- const context = (id) => ({
268
+ const context = (/** @type {string} */ id) => ({
202
269
  module: {
203
270
  id,
204
271
  options: deployment.publicOptions?.[id],
@@ -216,7 +283,11 @@ export function createServerModules(declarations, deployment) {
216
283
  };
217
284
  }
218
285
 
219
- /** Resolved once per set of declarations: in development, once per edit. */
286
+ /**
287
+ * Resolved once per set of declarations: in development, once per edit.
288
+ *
289
+ * @param {ServerModuleDeclaration[]} declarations
290
+ */
220
291
  function serverModules(declarations) {
221
292
  let modules = resolved.get(declarations);
222
293
  if (!modules) {
@@ -245,7 +316,7 @@ function serverModules(declarations) {
245
316
  * next request; in production from the bundle `ajs dms build` makes
246
317
  * (`dist/server-modules`). A workspace built before the feature has none.
247
318
  *
248
- * @param devServer Vite in development, `undefined` in production
319
+ * @param {import("vite").ViteDevServer | undefined} devServer Vite in development, `undefined` in production
249
320
  */
250
321
  export async function loadServerModules(devServer) {
251
322
  return serverModules(await currentDeclarations(devServer));
@@ -256,10 +327,10 @@ export async function loadServerModules(devServer) {
256
327
  * module route serves is the module's for every method: another method is
257
328
  * answered `405`.
258
329
  *
259
- * @param request The Node request
260
- * @param response The Node response
261
- * @param pathname The request's normalized path
262
- * @param modules What `loadServerModules` or `createServerModules` returned
330
+ * @param {import("node:http").IncomingMessage} request The Node request
331
+ * @param {import("node:http").ServerResponse} response The Node response
332
+ * @param {string} pathname The request's normalized path
333
+ * @param {ServerModules} modules What `loadServerModules` or `createServerModules` returned
263
334
  * @returns Whether a module route answered
264
335
  */
265
336
  export async function handleModuleRoute(request, response, pathname, modules) {