@antelopejs/dms-frontend 0.5.6 → 0.6.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 +376 -18
- package/dist/app-configs.d.ts +9 -0
- package/dist/app-configs.js +52 -0
- package/dist/asset-roots.d.ts +7 -0
- package/dist/asset-roots.js +34 -0
- package/dist/commands/build-action.js +9 -0
- package/dist/commands/dev-action.js +88 -19
- package/dist/commands/start-action.js +2 -0
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +8 -3
- package/dist/derived-outputs.d.ts +2 -0
- package/dist/derived-outputs.js +38 -31
- package/dist/help.js +10 -3
- package/dist/layer-watch.js +123 -131
- package/dist/layers.js +2 -0
- package/dist/manifest-secrets.js +5 -5
- package/dist/manifest.js +7 -1
- package/dist/materialize.js +21 -1
- package/dist/module-set.d.ts +16 -0
- package/dist/module-set.js +80 -0
- package/dist/server-process.d.ts +1 -0
- package/dist/server-process.js +28 -2
- package/dist/verify-source-runner.js +12 -0
- package/dist/workspace.d.ts +9 -0
- package/package.json +2 -1
- package/templates/vue/app-runtime.ts +58 -16
- package/templates/vue/client-server-guard.mjs +73 -0
- package/templates/vue/email-renderer.ts +17 -4
- package/templates/vue/email-runtime.ts +1 -1
- package/templates/vue/frontend-module.ts +1817 -259
- package/templates/vue/globals.d.ts +1 -1
- package/templates/vue/icon-hydration.ts +106 -1
- package/templates/vue/index.html +0 -1
- package/templates/vue/layer-app-config.ts +92 -0
- package/templates/vue/main.ts +10 -2
- package/templates/vue/package.json +4 -1
- package/templates/vue/server/auth/backend.mjs +13 -19
- package/templates/vue/server/auth/client-ip.mjs +27 -0
- package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
- package/templates/vue/server/auth/session.mjs +6 -97
- package/templates/vue/server/auth-hooks.mjs +84 -0
- package/templates/vue/server/backend-proxies.mjs +110 -0
- package/templates/vue/server/dev-private-files.mjs +99 -0
- package/templates/vue/server/email.mjs +11 -33
- package/templates/vue/server/favicon.mjs +20 -0
- package/templates/vue/server/inertia.mjs +48 -3
- package/templates/vue/server/listen-host.mjs +19 -0
- package/templates/vue/server/module-event.mjs +265 -0
- package/templates/vue/server/module-routes.mjs +513 -0
- package/templates/vue/server/module-server.mjs +225 -0
- package/templates/vue/server/module-testing.mjs +65 -0
- package/templates/vue/server/page-props.mjs +151 -0
- package/templates/vue/server/path-patterns.mjs +38 -0
- package/templates/vue/server/public-endpoints.mjs +69 -11
- package/templates/vue/server/request-routing.mjs +47 -0
- package/templates/vue/server/route-rules.mjs +129 -0
- package/templates/vue/server-module.ts +294 -0
- package/templates/vue/server.mjs +177 -161
- package/templates/vue/ssr-renderer.ts +60 -19
- package/templates/vue/tsconfig.json +2 -1
- package/templates/vue/ui-app-config.ts +18 -6
- package/templates/vue/ui-inertia-page.mjs +37 -0
- package/templates/vue/vite.config.ts +31 -10
- package/templates/vue/vite.email.config.ts +1 -1
- package/templates/vue/vite.server.config.ts +57 -0
- package/templates/vue/server/auth/oauth.mjs +0 -233
- package/templates/vue/server/auth/routes.mjs +0 -444
- package/templates/vue/server/render-token.mjs +0 -81
- package/templates/vue/server/tester.mjs +0 -227
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
// The routes frontend modules declare in their `dms.server.ts`: what each
|
|
2
|
+
// declaration is checked against, how conflicts are settled, and which route a
|
|
3
|
+
// request reaches. Pure: the server hands it the declarations it loaded, and
|
|
4
|
+
// `module-event.mjs` runs the handler a request reaches.
|
|
5
|
+
|
|
6
|
+
import { resolveBackendProxies } from "./backend-proxies.mjs";
|
|
7
|
+
import { compilePathPattern } from "./path-patterns.mjs";
|
|
8
|
+
import { matchPublicEndpoint } from "./public-endpoints.mjs";
|
|
9
|
+
import { ruleRejection } from "./route-rules.mjs";
|
|
10
|
+
|
|
11
|
+
const METHODS = new Set([
|
|
12
|
+
"GET",
|
|
13
|
+
"HEAD",
|
|
14
|
+
"POST",
|
|
15
|
+
"PUT",
|
|
16
|
+
"PATCH",
|
|
17
|
+
"DELETE",
|
|
18
|
+
"OPTIONS",
|
|
19
|
+
]);
|
|
20
|
+
const LITERAL_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
21
|
+
const PARAM_SEGMENT = /^:([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
22
|
+
const DEFAULT_BODY_LIMIT = 64 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* The renderer's own paths: its icon endpoint, Vite's URLs and the built
|
|
25
|
+
* assets. No module route is ever served there.
|
|
26
|
+
*/
|
|
27
|
+
const RESERVED_PREFIXES = ["/api/_dms/", "/@", "/assets/", "/node_modules/"];
|
|
28
|
+
/** The backend's namespace, which the proxy forwards. */
|
|
29
|
+
const BACKEND_PREFIX = /^\/(?:api|dms)(?:\/|$)/;
|
|
30
|
+
/** A module's own namespace under `/api/`, which the backend does not use. */
|
|
31
|
+
const MODULE_API_PREFIX = /^\/api\/_[A-Za-z0-9-]+(?:\/|$)/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether `pathname` names one of the files the renderer serves: Vite's URLs,
|
|
35
|
+
* the built assets and the dependencies. No server route is ever there, and a
|
|
36
|
+
* development page requests hundreds of them.
|
|
37
|
+
*/
|
|
38
|
+
export function isRendererFilePath(pathname) {
|
|
39
|
+
return ["/@", "/assets/", "/node_modules/"].some((prefix) =>
|
|
40
|
+
pathname.startsWith(prefix),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether a module route may serve `pathname`: never the renderer's own. */
|
|
45
|
+
export function mayHaveModuleRoute(pathname) {
|
|
46
|
+
return !RESERVED_PREFIXES.some(
|
|
47
|
+
(prefix) => pathname === prefix.slice(0, -1) || pathname.startsWith(prefix),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizedPath(path) {
|
|
52
|
+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pathRejection(path) {
|
|
56
|
+
if (typeof path !== "string" || !path.startsWith("/"))
|
|
57
|
+
return "a path starts with /";
|
|
58
|
+
const normalized = normalizedPath(path);
|
|
59
|
+
if (!mayHaveModuleRoute(normalized))
|
|
60
|
+
return `${RESERVED_PREFIXES.join(", ")} are the renderer's own paths`;
|
|
61
|
+
if (normalized === "/") return undefined;
|
|
62
|
+
const segments = normalized.slice(1).split("/");
|
|
63
|
+
const names = new Set();
|
|
64
|
+
for (const segment of segments) {
|
|
65
|
+
const param = segment.match(PARAM_SEGMENT)?.[1];
|
|
66
|
+
if (param) {
|
|
67
|
+
if (names.has(param)) return `the parameter :${param} is repeated`;
|
|
68
|
+
names.add(param);
|
|
69
|
+
} else if (
|
|
70
|
+
!LITERAL_SEGMENT.test(segment) ||
|
|
71
|
+
segment === "." ||
|
|
72
|
+
segment === ".."
|
|
73
|
+
)
|
|
74
|
+
return `"${segment}" is neither a literal segment nor a :name parameter`;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function optionsRejection(options) {
|
|
80
|
+
if (options === undefined) return undefined;
|
|
81
|
+
if (typeof options !== "object" || options === null)
|
|
82
|
+
return "its options are not an object";
|
|
83
|
+
if (
|
|
84
|
+
options.sameOrigin !== undefined &&
|
|
85
|
+
typeof options.sameOrigin !== "boolean"
|
|
86
|
+
)
|
|
87
|
+
return "sameOrigin is not a boolean";
|
|
88
|
+
if (
|
|
89
|
+
options.bodyLimit !== undefined &&
|
|
90
|
+
!(Number.isSafeInteger(options.bodyLimit) && options.bodyLimit >= 0)
|
|
91
|
+
)
|
|
92
|
+
return "bodyLimit is not a byte count";
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compilePath(path) {
|
|
97
|
+
const normalized = normalizedPath(path);
|
|
98
|
+
const segments = normalized === "/" ? [] : normalized.slice(1).split("/");
|
|
99
|
+
const params = [];
|
|
100
|
+
const pattern = segments
|
|
101
|
+
.map((segment) => {
|
|
102
|
+
const param = segment.match(PARAM_SEGMENT)?.[1];
|
|
103
|
+
if (!param) return `/${segment.replaceAll(".", "\\.")}`;
|
|
104
|
+
params.push(param);
|
|
105
|
+
return "/([^/]+)";
|
|
106
|
+
})
|
|
107
|
+
.join("");
|
|
108
|
+
return {
|
|
109
|
+
path: normalized,
|
|
110
|
+
// Two declarations of one path whatever their parameters are named.
|
|
111
|
+
shape: segments
|
|
112
|
+
.map((segment) => (PARAM_SEGMENT.test(segment) ? ":" : segment))
|
|
113
|
+
.join("/"),
|
|
114
|
+
params,
|
|
115
|
+
matcher: new RegExp(`^${pattern || ""}/?$`),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** What a module's setup declares, by kind, in declaration order. */
|
|
120
|
+
function collectDeclarations(setup) {
|
|
121
|
+
const declared = { routes: [], auth: [], rules: [], proxies: [] };
|
|
122
|
+
setup({
|
|
123
|
+
registerRoute: (method, path, handler, options) =>
|
|
124
|
+
declared.routes.push({ method, path, handler, options }),
|
|
125
|
+
registerAuth: (auth) => declared.auth.push(auth),
|
|
126
|
+
registerRouteRules: (rules) => declared.rules.push(rules),
|
|
127
|
+
registerBackendProxy: (pattern, options) =>
|
|
128
|
+
declared.proxies.push({ pattern, options }),
|
|
129
|
+
});
|
|
130
|
+
return declared;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function methodsOf(method) {
|
|
134
|
+
const methods = Array.isArray(method) ? method : [method];
|
|
135
|
+
return methods.length && methods.every((name) => METHODS.has(name))
|
|
136
|
+
? methods
|
|
137
|
+
: undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function registrationRejection(registration) {
|
|
141
|
+
if (!methodsOf(registration.method))
|
|
142
|
+
return `the method is not one of ${[...METHODS].join(", ")}`;
|
|
143
|
+
if (typeof registration.handler !== "function")
|
|
144
|
+
return "its handler is not a function";
|
|
145
|
+
return (
|
|
146
|
+
pathRejection(registration.path) ?? optionsRejection(registration.options)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function describeRoute(method, path) {
|
|
151
|
+
return `"${[method].flat().join(", ")} ${path}"`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function backendShadowWarning(id, route) {
|
|
155
|
+
if (
|
|
156
|
+
!BACKEND_PREFIX.test(route.path) ||
|
|
157
|
+
MODULE_API_PREFIX.test(route.path) ||
|
|
158
|
+
route.overridesBackend
|
|
159
|
+
)
|
|
160
|
+
return undefined;
|
|
161
|
+
return (
|
|
162
|
+
`Frontend module ${id} serves ${describeRoute(route.method, route.path)} from the frontend server, ` +
|
|
163
|
+
"which hides whatever the backend serves there. Use a path under /api/_<module>/, " +
|
|
164
|
+
"or pass { overridesBackend: true } if that is the intent."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function publicEndpointWarning(id, route, publicEndpoints) {
|
|
169
|
+
const sample = route.path.replaceAll(/\/:[^/]+/g, "/x");
|
|
170
|
+
if (!matchPublicEndpoint(route.method, sample, publicEndpoints))
|
|
171
|
+
return undefined;
|
|
172
|
+
return (
|
|
173
|
+
`Frontend module ${id} serves ${describeRoute(route.method, route.path)}, ` +
|
|
174
|
+
"a public endpoint a backend module declared: the module route answers it, not the backend."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Runs a module's setup; a setup that throws declares nothing. */
|
|
179
|
+
function declarationsOfModule(declaration, warn) {
|
|
180
|
+
try {
|
|
181
|
+
return collectDeclarations(declaration.setup);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
warn(
|
|
184
|
+
`Frontend module ${declaration.id} has a dms.server.ts that failed to declare its routes, which are ignored: ${error instanceof Error ? error.message : String(error)}`,
|
|
185
|
+
);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function routesOfModule(module, warn) {
|
|
191
|
+
const { id, priority = 0 } = module;
|
|
192
|
+
return module.declared.routes.flatMap((registration, order) => {
|
|
193
|
+
const reason = registrationRejection(registration);
|
|
194
|
+
if (reason) {
|
|
195
|
+
warn(
|
|
196
|
+
`Frontend module ${id} declares route ${describeRoute(registration.method, registration.path)}, which is ignored: ${reason}.`,
|
|
197
|
+
);
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const compiled = compilePath(registration.path);
|
|
201
|
+
return methodsOf(registration.method).map((method) => ({
|
|
202
|
+
...compiled,
|
|
203
|
+
id,
|
|
204
|
+
priority,
|
|
205
|
+
order,
|
|
206
|
+
method,
|
|
207
|
+
handler: registration.handler,
|
|
208
|
+
sameOrigin: registration.options?.sameOrigin ?? true,
|
|
209
|
+
bodyLimit: registration.options?.bodyLimit ?? DEFAULT_BODY_LIMIT,
|
|
210
|
+
overridesBackend: registration.options?.overridesBackend === true,
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function byPriority(left, right) {
|
|
216
|
+
return (
|
|
217
|
+
(right.priority ?? 0) - (left.priority ?? 0) ||
|
|
218
|
+
left.id.localeCompare(right.id)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function shadowWarning(kept, shadowed) {
|
|
223
|
+
const settled =
|
|
224
|
+
kept.priority === shadowed.priority
|
|
225
|
+
? "same priority, the first module id wins"
|
|
226
|
+
: "the higher priority wins";
|
|
227
|
+
return (
|
|
228
|
+
`${describeRoute(shadowed.method, shadowed.path)}: ${shadowed.id} (priority ${shadowed.priority}) ` +
|
|
229
|
+
`is shadowed by ${kept.id} (priority ${kept.priority}); ${settled}.`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parameterCount(route) {
|
|
234
|
+
return route.params.length;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether one request path can match both routes: as many segments, and at
|
|
239
|
+
* each position the same literal or a parameter on either side.
|
|
240
|
+
*/
|
|
241
|
+
function routesOverlap(left, right) {
|
|
242
|
+
const leftSegments = left.shape === "" ? [] : left.shape.split("/");
|
|
243
|
+
const rightSegments = right.shape === "" ? [] : right.shape.split("/");
|
|
244
|
+
return (
|
|
245
|
+
leftSegments.length === rightSegments.length &&
|
|
246
|
+
leftSegments.every(
|
|
247
|
+
(segment, index) =>
|
|
248
|
+
segment === ":" ||
|
|
249
|
+
rightSegments[index] === ":" ||
|
|
250
|
+
segment === rightSegments[index],
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** `first` is tried before `second`: it answers what both can match. */
|
|
256
|
+
function overlapWarning(first, second) {
|
|
257
|
+
const settled =
|
|
258
|
+
parameterCount(first) < parameterCount(second)
|
|
259
|
+
? "it has fewer parameters"
|
|
260
|
+
: first.priority === second.priority
|
|
261
|
+
? "same priority, the first module id wins"
|
|
262
|
+
: "the higher priority wins";
|
|
263
|
+
return (
|
|
264
|
+
`${describeRoute(first.method, first.path)} of ${first.id} (priority ${first.priority}) and ` +
|
|
265
|
+
`${describeRoute(second.method, second.path)} of ${second.id} (priority ${second.priority}) ` +
|
|
266
|
+
`can match the same request, which ${first.id} answers: ${settled}.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function rootParameterWarning(route) {
|
|
271
|
+
return (
|
|
272
|
+
`Frontend module ${route.id} serves ${describeRoute(route.method, route.path)}, whose first ` +
|
|
273
|
+
"segment is a parameter: it answers every path of that shape, the pages and other modules' " +
|
|
274
|
+
"routes included. Start the path with a literal segment."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reports what the order of `routes` settles silently: two modules' routes
|
|
280
|
+
* that can match one request, and a route that catches every first segment.
|
|
281
|
+
*/
|
|
282
|
+
function warnOverlaps(routes, warn) {
|
|
283
|
+
for (const [index, route] of routes.entries()) {
|
|
284
|
+
if (route.shape.startsWith(":")) warn(rootParameterWarning(route));
|
|
285
|
+
for (const later of routes.slice(index + 1))
|
|
286
|
+
if (
|
|
287
|
+
later.method === route.method &&
|
|
288
|
+
later.id !== route.id &&
|
|
289
|
+
routesOverlap(route, later)
|
|
290
|
+
)
|
|
291
|
+
warn(overlapWarning(route, later));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resolveRoutes(modules, warn, publicEndpoints) {
|
|
296
|
+
const kept = new Map();
|
|
297
|
+
for (const module of modules) {
|
|
298
|
+
for (const route of routesOfModule(module, warn)) {
|
|
299
|
+
const key = `${route.method} ${route.shape}`;
|
|
300
|
+
const current = kept.get(key);
|
|
301
|
+
if (current) {
|
|
302
|
+
warn(shadowWarning(current, route));
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
kept.set(key, route);
|
|
306
|
+
const notice =
|
|
307
|
+
backendShadowWarning(route.id, route) ??
|
|
308
|
+
publicEndpointWarning(route.id, route, publicEndpoints);
|
|
309
|
+
if (notice) warn(notice);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const routes = [...kept.values()].sort(
|
|
313
|
+
(left, right) =>
|
|
314
|
+
parameterCount(left) - parameterCount(right) ||
|
|
315
|
+
byPriority(left, right) ||
|
|
316
|
+
left.order - right.order,
|
|
317
|
+
);
|
|
318
|
+
warnOverlaps(routes, warn);
|
|
319
|
+
return routes;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const AUTH_HOOKS = ["refreshSession", "publicSession"];
|
|
323
|
+
|
|
324
|
+
function authRejection(auth) {
|
|
325
|
+
if (typeof auth !== "object" || auth === null) return "it is not an object";
|
|
326
|
+
for (const hook of AUTH_HOOKS)
|
|
327
|
+
if (auth[hook] !== undefined && typeof auth[hook] !== "function")
|
|
328
|
+
return `${hook} is not a function`;
|
|
329
|
+
if (
|
|
330
|
+
auth.signInPath !== undefined &&
|
|
331
|
+
(typeof auth.signInPath !== "string" ||
|
|
332
|
+
!compilePathPattern(auth.signInPath) ||
|
|
333
|
+
auth.signInPath.includes("*"))
|
|
334
|
+
)
|
|
335
|
+
return "signInPath is not a page path";
|
|
336
|
+
if (
|
|
337
|
+
auth.signInExempt !== undefined &&
|
|
338
|
+
!(
|
|
339
|
+
Array.isArray(auth.signInExempt) &&
|
|
340
|
+
auth.signInExempt.every((pattern) => compilePathPattern(pattern))
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
return "signInExempt is not a list of /x, /x/* or /x/** patterns";
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The auth hooks: those of the highest-priority module that registers them.
|
|
349
|
+
* A module registering them twice, or after another one, is reported.
|
|
350
|
+
*/
|
|
351
|
+
function resolveAuth(modules, warn) {
|
|
352
|
+
let provider;
|
|
353
|
+
for (const module of modules) {
|
|
354
|
+
for (const auth of module.declared.auth) {
|
|
355
|
+
const reason = authRejection(auth);
|
|
356
|
+
if (reason) {
|
|
357
|
+
warn(
|
|
358
|
+
`Frontend module ${module.id} registers auth hooks, which are ignored: ${reason}.`,
|
|
359
|
+
);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (provider) {
|
|
363
|
+
warn(
|
|
364
|
+
`Frontend module ${module.id} registers auth hooks, which are ignored: ${provider.id} (priority ${provider.priority ?? 0}) provides them.`,
|
|
365
|
+
);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
provider = {
|
|
369
|
+
id: module.id,
|
|
370
|
+
priority: module.priority ?? 0,
|
|
371
|
+
signInPath: auth.signInPath,
|
|
372
|
+
signInExempt: auth.signInExempt?.map(compilePathPattern),
|
|
373
|
+
refreshSession: auth.refreshSession,
|
|
374
|
+
publicSession: auth.publicSession,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return provider;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The route rules: of two declarations of the same pattern, the module of
|
|
383
|
+
* higher priority keeps it, with a warning.
|
|
384
|
+
*/
|
|
385
|
+
function resolveRules(modules, warn) {
|
|
386
|
+
const kept = new Map();
|
|
387
|
+
for (const module of modules) {
|
|
388
|
+
for (const rules of module.declared.rules) {
|
|
389
|
+
if (typeof rules !== "object" || rules === null) {
|
|
390
|
+
warn(
|
|
391
|
+
`Frontend module ${module.id} declares route rules that are ignored: they are not an object.`,
|
|
392
|
+
);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
for (const [pattern, rule] of Object.entries(rules)) {
|
|
396
|
+
const reason = ruleRejection(pattern, rule);
|
|
397
|
+
const current = kept.get(pattern);
|
|
398
|
+
if (reason || current) {
|
|
399
|
+
warn(
|
|
400
|
+
`Frontend module ${module.id} declares the route rule "${pattern}", which is ignored: ${reason ?? `${current.id} (priority ${current.priority}) declares it`}.`,
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
kept.set(pattern, {
|
|
405
|
+
id: module.id,
|
|
406
|
+
priority: module.priority ?? 0,
|
|
407
|
+
pattern: compilePathPattern(pattern),
|
|
408
|
+
rule,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return [...kept.values()];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* What the modules declared, ready to serve. Each declaration is checked, and
|
|
418
|
+
* a bad one is ignored with a warning naming its module.
|
|
419
|
+
*
|
|
420
|
+
* `routes`: of two declarations of the same method and path, the module of
|
|
421
|
+
* higher priority keeps it, with a warning naming both (module id on a tie).
|
|
422
|
+
* Routes with fewer parameters are tried first, so a literal path is never
|
|
423
|
+
* hidden by a parameter of another route. Two modules' routes that can match
|
|
424
|
+
* the same request, and a route whose first segment is a parameter, are
|
|
425
|
+
* reported too: the order settles them, but silently.
|
|
426
|
+
*
|
|
427
|
+
* `auth`: the auth hooks of the highest-priority module that registers them,
|
|
428
|
+
* `undefined` when none does.
|
|
429
|
+
*
|
|
430
|
+
* `rules`: the route rules, one per pattern.
|
|
431
|
+
*
|
|
432
|
+
* `proxies`: the backend prefixes the modules and the manifest declare, the
|
|
433
|
+
* most specific first (see `backend-proxies.mjs`).
|
|
434
|
+
*
|
|
435
|
+
* @param declarations `{ id, priority, setup }` per module with a dms.server.ts
|
|
436
|
+
* @param warn Reports a declaration problem
|
|
437
|
+
* @param publicEndpoints The deployment's public endpoints (#108), to report a
|
|
438
|
+
* module route answering one
|
|
439
|
+
* @param manifestProxies `{ module, path, auth? }`, the backend proxies the
|
|
440
|
+
* backend modules declare in the manifest
|
|
441
|
+
*/
|
|
442
|
+
export function resolveServerDeclarations(
|
|
443
|
+
declarations,
|
|
444
|
+
warn,
|
|
445
|
+
publicEndpoints = [],
|
|
446
|
+
manifestProxies = [],
|
|
447
|
+
) {
|
|
448
|
+
const modules = [...declarations].sort(byPriority).flatMap((declaration) => {
|
|
449
|
+
const declared = declarationsOfModule(declaration, warn);
|
|
450
|
+
return declared ? [{ ...declaration, declared }] : [];
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
routes: resolveRoutes(modules, warn, publicEndpoints),
|
|
454
|
+
auth: resolveAuth(modules, warn),
|
|
455
|
+
rules: resolveRules(modules, warn),
|
|
456
|
+
proxies: resolveBackendProxies(
|
|
457
|
+
modules.flatMap((module) =>
|
|
458
|
+
module.declared.proxies.map((proxy) => ({ id: module.id, ...proxy })),
|
|
459
|
+
),
|
|
460
|
+
manifestProxies,
|
|
461
|
+
warn,
|
|
462
|
+
),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function decodedParams(route, match) {
|
|
467
|
+
const params = {};
|
|
468
|
+
for (const [index, name] of route.params.entries()) {
|
|
469
|
+
try {
|
|
470
|
+
params[name] = decodeURIComponent(match[index + 1]);
|
|
471
|
+
} catch {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return params;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The route a request reaches, with its parameters, or `undefined`.
|
|
480
|
+
*
|
|
481
|
+
* @param routes What `resolveServerDeclarations` returned
|
|
482
|
+
* @param method The request's method
|
|
483
|
+
* @param pathname The request's normalized path, without its query string
|
|
484
|
+
*/
|
|
485
|
+
export function matchModuleRoute(routes, method, pathname) {
|
|
486
|
+
const reached = matchMethod(routes, method, pathname);
|
|
487
|
+
// A `HEAD` no route declares is the `GET`'s, without its body.
|
|
488
|
+
if (reached || method !== "HEAD") return reached;
|
|
489
|
+
return matchMethod(routes, "GET", pathname);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function matchMethod(routes, method, pathname) {
|
|
493
|
+
for (const route of routes) {
|
|
494
|
+
if (route.method !== method) continue;
|
|
495
|
+
const match = route.matcher.exec(pathname);
|
|
496
|
+
if (!match) continue;
|
|
497
|
+
const params = decodedParams(route, match);
|
|
498
|
+
if (params) return { route, params };
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The methods the module routes answer on `pathname`: a path a module route
|
|
505
|
+
* serves is that module's for every method, the others answered `405`.
|
|
506
|
+
*/
|
|
507
|
+
export function moduleRouteMethods(routes, pathname) {
|
|
508
|
+
const methods = new Set();
|
|
509
|
+
for (const route of routes)
|
|
510
|
+
if (route.matcher.test(pathname)) methods.add(route.method);
|
|
511
|
+
if (methods.has("GET")) methods.add("HEAD");
|
|
512
|
+
return [...methods];
|
|
513
|
+
}
|