@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.
- package/README.md +18 -24
- package/dist/commands/build-action.js +3 -2
- package/dist/commands/dev-action.js +2 -1
- package/dist/commands/prepare-action.js +1 -1
- package/dist/commands/start-action.js +3 -24
- package/dist/commands/workspace-task.js +2 -1
- package/dist/derived-outputs.js +3 -3
- package/dist/discovery.js +17 -6
- package/dist/entry-weight.js +15 -0
- package/dist/fs-sync.js +7 -4
- package/dist/index.js +2 -1
- package/dist/internal/built-workspace.d.ts +4 -0
- package/dist/internal/built-workspace.js +43 -0
- package/dist/internal/ensure.d.ts +1 -0
- package/dist/internal/ensure.js +8 -0
- package/dist/internal/locale-catalogs.d.ts +8 -3
- package/dist/internal/locale-catalogs.js +16 -6
- package/dist/internal/manifest-decoder.d.ts +7 -0
- package/dist/internal/manifest-decoder.js +81 -0
- package/dist/internal/own-release.d.ts +5 -0
- package/dist/internal/own-release.js +20 -0
- package/dist/internal/plain-object.d.ts +1 -0
- package/dist/internal/plain-object.js +6 -0
- package/dist/internal/text-list.d.ts +1 -0
- package/dist/internal/text-list.js +10 -0
- package/dist/internal/vite-manifest.d.ts +6 -0
- package/dist/internal/vite-manifest.js +24 -0
- package/dist/layer-watch.js +25 -24
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +3 -1
- package/dist/locale-files.d.ts +2 -1
- package/dist/locale-files.js +4 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +44 -31
- package/dist/materialize.js +6 -2
- package/dist/module-set.js +2 -1
- package/dist/node-options.d.ts +1 -0
- package/dist/node-options.js +8 -0
- package/dist/output.d.ts +15 -9
- package/dist/output.js +30 -18
- package/dist/private-values.d.ts +1 -1
- package/dist/private-values.js +5 -3
- package/dist/project-modules.js +13 -8
- package/dist/renderer-range.js +5 -3
- package/dist/server-process.js +1 -1
- package/dist/verify-source-build.js +10 -2
- package/dist/verify-source-client-bundle.d.ts +1 -0
- package/dist/verify-source-client-bundle.js +30 -0
- package/dist/verify-source-runner.js +44 -60
- package/dist/verify-source-sources.js +49 -11
- package/dist/workspace-setup.js +1 -1
- package/package.json +6 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/templates/vue/app-runtime.ts +2 -9
- package/templates/vue/client-server-guard.mjs +16 -0
- package/templates/vue/compress-assets.mjs +9 -1
- package/templates/vue/entry-weight.mjs +22 -0
- package/templates/vue/frontend-module.ts +1 -0
- package/templates/vue/frontend-runtime/module-registration.ts +2 -2
- package/templates/vue/frontend-runtime/state.ts +2 -2
- package/templates/vue/frontend-runtime/types.ts +8 -2
- package/templates/vue/head-order.mjs +7 -0
- package/templates/vue/icon-scan.mjs +4 -0
- package/templates/vue/server/auth/backend.mjs +41 -4
- package/templates/vue/server/auth/client-ip.mjs +52 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
- package/templates/vue/server/auth/session.mjs +81 -2
- package/templates/vue/server/auth-hooks.mjs +15 -5
- package/templates/vue/server/backend-proxies.mjs +65 -9
- package/templates/vue/server/backend-request.mjs +61 -0
- package/templates/vue/server/backend-response.mjs +23 -6
- package/templates/vue/server/built-files.mjs +7 -0
- package/templates/vue/server/cli-report.mjs +53 -11
- package/templates/vue/server/client-manifest.mjs +23 -1
- package/templates/vue/server/content.mjs +18 -1
- package/templates/vue/server/dev-private-files.mjs +17 -1
- package/templates/vue/server/dev-styles.mjs +38 -0
- package/templates/vue/server/email.mjs +9 -3
- package/templates/vue/server/favicon.mjs +3 -0
- package/templates/vue/server/generated-files.mjs +211 -0
- package/templates/vue/server/homepage.mjs +21 -9
- package/templates/vue/server/html-tag.mjs +4 -0
- package/templates/vue/server/icons.mjs +80 -7
- package/templates/vue/server/in-place-render.mjs +35 -0
- package/templates/vue/server/inertia.mjs +61 -2
- package/templates/vue/server/module-event.mjs +98 -17
- package/templates/vue/server/module-route-types.mjs +78 -0
- package/templates/vue/server/module-routes.mjs +138 -31
- package/templates/vue/server/module-server.mjs +98 -27
- package/templates/vue/server/module-testing.mjs +14 -5
- package/templates/vue/server/page-props.mjs +92 -2
- package/templates/vue/server/page-redirects.mjs +37 -0
- package/templates/vue/server/path-patterns.mjs +13 -1
- package/templates/vue/server/proxy-compression.mjs +22 -0
- package/templates/vue/server/proxy-headers.mjs +11 -1
- package/templates/vue/server/proxy-redirects.mjs +20 -5
- package/templates/vue/server/proxy-request.mjs +57 -0
- package/templates/vue/server/proxy-transfers.mjs +33 -5
- package/templates/vue/server/public-endpoints.mjs +138 -23
- package/templates/vue/server/public-options.mjs +20 -6
- package/templates/vue/server/request-failure.mjs +23 -0
- package/templates/vue/server/request-routing.mjs +26 -9
- package/templates/vue/server/request-target.mjs +9 -3
- package/templates/vue/server/route-rules.mjs +39 -15
- package/templates/vue/server/ssr-module.mjs +32 -0
- package/templates/vue/server/static-files.mjs +24 -1
- package/templates/vue/server/type-guards.mjs +71 -0
- package/templates/vue/server/vite-errors.mjs +32 -5
- package/templates/vue/server/web-body.mjs +3 -0
- package/templates/vue/server-module.ts +10 -1
- package/templates/vue/server.mjs +132 -148
- package/templates/vue/sfc-global-types.mjs +112 -13
- package/templates/vue/ssr-renderer.ts +2 -2
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- 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
|
-
/**
|
|
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 (
|
|
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 (
|
|
77
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
-
/**
|
|
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 =
|
|
241
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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 (
|
|
406
|
+
if (!isNonNullObject(auth)) return "it is not an object";
|
|
324
407
|
for (const hook of AUTH_HOOKS)
|
|
325
|
-
if (auth[hook] !== undefined &&
|
|
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
|
-
(
|
|
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 (
|
|
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
|
-
/**
|
|
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 (
|
|
94
|
+
if (!isNonNullObject(node)) return undefined;
|
|
57
95
|
}
|
|
58
96
|
return UNSAFE_KEYS.has(path.at(-1)) ? undefined : node;
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
97
|
-
else if (
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
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) {
|