@ultimat3/render 27.0.0 → 27.2.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/package.json +6 -6
- package/src/mounted-routes.ts +17 -0
- package/src/registry.ts +50 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/render",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.2.0",
|
|
4
4
|
"description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ultimat3/cache": "27.
|
|
44
|
-
"@ultimat3/core": "27.
|
|
45
|
-
"@ultimat3/http": "27.
|
|
46
|
-
"@ultimat3/i18n": "27.
|
|
47
|
-
"@ultimat3/seo": "27.
|
|
43
|
+
"@ultimat3/cache": "27.2.0",
|
|
44
|
+
"@ultimat3/core": "27.2.0",
|
|
45
|
+
"@ultimat3/http": "27.2.0",
|
|
46
|
+
"@ultimat3/i18n": "27.2.0",
|
|
47
|
+
"@ultimat3/seo": "27.2.0",
|
|
48
48
|
"sass": "1.105.1"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/mounted-routes.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface RouteMount {
|
|
|
14
14
|
readonly by: string;
|
|
15
15
|
/** Every permission the route's own screen decides on, coarse gate first. */
|
|
16
16
|
readonly permissions: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* `true` when an app FILE serves the path in the mount's place — a config the mounter issued
|
|
19
|
+
* for it (`RouteMountInput.claimable`). The descriptor's `file` is then that file. Absent
|
|
20
|
+
* otherwise, never `false`.
|
|
21
|
+
*/
|
|
22
|
+
readonly claimed?: true;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/** The mount a set of routes belongs to. `key` is what a re-declaration replaces. */
|
|
@@ -23,6 +29,13 @@ export interface RouteMountInput {
|
|
|
23
29
|
/** What `RouteDescriptor.file` reads for these routes: the package, since no app file is. */
|
|
24
30
|
readonly file: string;
|
|
25
31
|
readonly surface: Exclude<Surface, 'shared'>;
|
|
32
|
+
/**
|
|
33
|
+
* May the file route declaring `config` serve `path` in the mount's place? The MOUNTER's
|
|
34
|
+
* answer, never render's: only the package that mounted a path knows which configs it issued
|
|
35
|
+
* (and with what guard inside). Asked with the config the module EXPORTED, by identity. Absent:
|
|
36
|
+
* no file may, and a file on a mounted path is `X_ROUTE_DUPLICATE`.
|
|
37
|
+
*/
|
|
38
|
+
readonly claimable?: (path: string, config: RouteConfig) => boolean;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
export interface MountedRouteInput {
|
|
@@ -62,6 +75,10 @@ export function clearMountedRoutes(): void {
|
|
|
62
75
|
mounted.clear();
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
/** Did the mount that owns `route` issue `config` for its path? */
|
|
79
|
+
export const claimedBy = (route: MountedRoute, config: RouteConfig): boolean =>
|
|
80
|
+
route.mount.claimable?.(route.path, config) === true;
|
|
81
|
+
|
|
65
82
|
/** One URL, one claimant — a file and a mount on the same path is two servers for it. */
|
|
66
83
|
export const mountCollision = (
|
|
67
84
|
path: string,
|
package/src/registry.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { assertModeInvariants, defaultIslandBudget } from './modes';
|
|
21
21
|
import type { MountedRouteInput, RouteMount, RouteMountInput } from './mounted-routes';
|
|
22
22
|
import {
|
|
23
|
+
claimedBy,
|
|
23
24
|
clearMountedRoutes,
|
|
24
25
|
mountCollision,
|
|
25
26
|
mountedAt,
|
|
@@ -87,8 +88,9 @@ export interface RouteDescriptor {
|
|
|
87
88
|
readonly islandSources: readonly string[];
|
|
88
89
|
readonly budgetJs: string | null;
|
|
89
90
|
/**
|
|
90
|
-
* Present on a route
|
|
91
|
-
*
|
|
91
|
+
* Present on a route a package mounted: who mounted it and every permission that gates it.
|
|
92
|
+
* `file` is the package — unless `mount.claimed`, when an app file serves the path in the
|
|
93
|
+
* mount's place and `file` is that file. Absent on every other file route.
|
|
92
94
|
*/
|
|
93
95
|
readonly mount?: RouteMount;
|
|
94
96
|
}
|
|
@@ -212,6 +214,11 @@ function toUrlSegment(segment: string): string {
|
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
const routes = new Map<string, RouteEntry>();
|
|
217
|
+
/**
|
|
218
|
+
* The config each path's module EXPORTED, before `withIslandBudget` may have copied it: a mount's
|
|
219
|
+
* `claimable` answers by identity, and the entry's own config is not always that object.
|
|
220
|
+
*/
|
|
221
|
+
const declaredConfigs = new Map<string, RouteConfig>();
|
|
215
222
|
/** The table `describePages()` last built, dropped whenever a route registers or the registry clears. */
|
|
216
223
|
let described: readonly RouteDescriptor[] | undefined;
|
|
217
224
|
|
|
@@ -297,7 +304,9 @@ export function registerRoute<TData = RouteData>(
|
|
|
297
304
|
);
|
|
298
305
|
}
|
|
299
306
|
const claimed = mountedAt(path);
|
|
300
|
-
if (claimed !== undefined
|
|
307
|
+
if (claimed !== undefined && !claimedBy(claimed, input.config as RouteConfig)) {
|
|
308
|
+
throw mountCollision(path, input.file, claimed.mount);
|
|
309
|
+
}
|
|
301
310
|
|
|
302
311
|
const entry: RouteEntry<TData> = {
|
|
303
312
|
file: input.file,
|
|
@@ -318,6 +327,7 @@ export function registerRoute<TData = RouteData>(
|
|
|
318
327
|
...(input.component === undefined ? {} : { component: input.component }),
|
|
319
328
|
};
|
|
320
329
|
routes.set(path, entry as RouteEntry);
|
|
330
|
+
declaredConfigs.set(path, input.config as RouteConfig);
|
|
321
331
|
described = undefined;
|
|
322
332
|
return entry;
|
|
323
333
|
}
|
|
@@ -355,7 +365,9 @@ export function registerMountedRoutes(
|
|
|
355
365
|
): void {
|
|
356
366
|
for (const route of declared) {
|
|
357
367
|
const file = routes.get(route.path)?.file;
|
|
358
|
-
|
|
368
|
+
const config = declaredConfigs.get(route.path);
|
|
369
|
+
const claimed = config !== undefined && claimedBy({ ...route, mount }, config);
|
|
370
|
+
if (file !== undefined && !claimed) throw mountCollision(route.path, file, mount);
|
|
359
371
|
assertPurgeableTags(mount.file, route.config.revalidate?.tags);
|
|
360
372
|
}
|
|
361
373
|
setMountedRoutes(mount, declared);
|
|
@@ -364,6 +376,7 @@ export function registerMountedRoutes(
|
|
|
364
376
|
|
|
365
377
|
export function clearRoutes(): void {
|
|
366
378
|
routes.clear();
|
|
379
|
+
declaredConfigs.clear();
|
|
367
380
|
clearMountedRoutes();
|
|
368
381
|
described = undefined;
|
|
369
382
|
}
|
|
@@ -395,21 +408,39 @@ export function describePages(): readonly RouteDescriptor[] {
|
|
|
395
408
|
}
|
|
396
409
|
|
|
397
410
|
function buildDescriptors(): RouteDescriptor[] {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
411
|
+
const mounted = mountedRoutes();
|
|
412
|
+
// A path an app file claimed is ONE row: the file's, still naming the mount that gates it.
|
|
413
|
+
const claimOf = (path: string): RouteMount | undefined => {
|
|
414
|
+
const route = mounted.find((candidate) => candidate.path === path);
|
|
415
|
+
return route === undefined
|
|
416
|
+
? undefined
|
|
417
|
+
: { by: route.mount.by, permissions: [...route.permissions], claimed: true };
|
|
418
|
+
};
|
|
419
|
+
const files = routeEntries().map((entry): RouteDescriptor => {
|
|
420
|
+
const row = descriptorOf(
|
|
421
|
+
entry.path,
|
|
422
|
+
entry.file,
|
|
423
|
+
entry.surface,
|
|
424
|
+
entry.config,
|
|
425
|
+
entry.pattern.keys.length,
|
|
426
|
+
);
|
|
427
|
+
const claim = claimOf(entry.path);
|
|
428
|
+
return claim === undefined ? row : { ...row, mount: claim };
|
|
429
|
+
});
|
|
430
|
+
const mounts = mounted
|
|
431
|
+
.filter((route) => !routes.has(route.path))
|
|
432
|
+
.map(
|
|
433
|
+
(route): RouteDescriptor => ({
|
|
434
|
+
...descriptorOf(
|
|
435
|
+
route.path,
|
|
436
|
+
route.mount.file,
|
|
437
|
+
route.mount.surface,
|
|
438
|
+
route.config,
|
|
439
|
+
compilePattern(route.path).keys.length,
|
|
440
|
+
),
|
|
441
|
+
mount: { by: route.mount.by, permissions: [...route.permissions] },
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
413
444
|
return mounts.length === 0
|
|
414
445
|
? files
|
|
415
446
|
: [...files, ...mounts].sort((a, b) => byCodeUnit(a.path, b.path));
|