@bleedingdev/modern-js-renderer-core 0.0.0-bootstrap → 3.9.3-ultramodern.1
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/LICENSE +21 -0
- package/dist/cjs/abort.js +65 -0
- package/dist/cjs/adapter.js +62 -0
- package/dist/cjs/data/body.js +78 -0
- package/dist/cjs/data/client.js +409 -0
- package/dist/cjs/data/codec.js +168 -0
- package/dist/cjs/data/index.js +125 -0
- package/dist/cjs/data/private.js +77 -0
- package/dist/cjs/data/routes.js +149 -0
- package/dist/cjs/data/server.js +791 -0
- package/dist/cjs/data/static.js +110 -0
- package/dist/cjs/data/tree.js +231 -0
- package/dist/cjs/data/types.js +56 -0
- package/dist/cjs/document.js +184 -0
- package/dist/cjs/entry-client.js +142 -0
- package/dist/cjs/entry-server.js +253 -0
- package/dist/cjs/federation.js +80 -0
- package/dist/cjs/identity.js +120 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/localization.js +48 -0
- package/dist/cjs/router.js +293 -0
- package/dist/cjs/server/assets.js +56 -0
- package/dist/cjs/server/dispatch.js +351 -0
- package/dist/cjs/server/index.js +57 -0
- package/dist/cjs/server/types.js +18 -0
- package/dist/cjs/server/worker.js +73 -0
- package/dist/cjs/session/cache.js +172 -0
- package/dist/cjs/session/index.js +63 -0
- package/dist/cjs/session/request.js +269 -0
- package/dist/cjs/session/types.js +18 -0
- package/dist/esm/abort.mjs +27 -0
- package/dist/esm/adapter.mjs +21 -0
- package/dist/esm/data/body.mjs +40 -0
- package/dist/esm/data/client.mjs +368 -0
- package/dist/esm/data/codec.mjs +112 -0
- package/dist/esm/data/index.mjs +7 -0
- package/dist/esm/data/private.mjs +39 -0
- package/dist/esm/data/routes.mjs +108 -0
- package/dist/esm/data/server.mjs +723 -0
- package/dist/esm/data/static.mjs +51 -0
- package/dist/esm/data/tree.mjs +193 -0
- package/dist/esm/data/types.mjs +6 -0
- package/dist/esm/document.mjs +131 -0
- package/dist/esm/entry-client.mjs +104 -0
- package/dist/esm/entry-server.mjs +215 -0
- package/dist/esm/federation.mjs +36 -0
- package/dist/esm/identity.mjs +63 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/localization.mjs +10 -0
- package/dist/esm/router.mjs +225 -0
- package/dist/esm/server/assets.mjs +15 -0
- package/dist/esm/server/dispatch.mjs +307 -0
- package/dist/esm/server/index.mjs +3 -0
- package/dist/esm/server/types.mjs +0 -0
- package/dist/esm/server/worker.mjs +35 -0
- package/dist/esm/session/cache.mjs +116 -0
- package/dist/esm/session/index.mjs +3 -0
- package/dist/esm/session/request.mjs +231 -0
- package/dist/esm/session/types.mjs +0 -0
- package/dist/esm-node/abort.mjs +28 -0
- package/dist/esm-node/adapter.mjs +22 -0
- package/dist/esm-node/data/body.mjs +41 -0
- package/dist/esm-node/data/client.mjs +369 -0
- package/dist/esm-node/data/codec.mjs +113 -0
- package/dist/esm-node/data/index.mjs +8 -0
- package/dist/esm-node/data/private.mjs +40 -0
- package/dist/esm-node/data/routes.mjs +109 -0
- package/dist/esm-node/data/server.mjs +724 -0
- package/dist/esm-node/data/static.mjs +52 -0
- package/dist/esm-node/data/tree.mjs +194 -0
- package/dist/esm-node/data/types.mjs +7 -0
- package/dist/esm-node/document.mjs +132 -0
- package/dist/esm-node/entry-client.mjs +105 -0
- package/dist/esm-node/entry-server.mjs +216 -0
- package/dist/esm-node/federation.mjs +37 -0
- package/dist/esm-node/identity.mjs +64 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/localization.mjs +11 -0
- package/dist/esm-node/router.mjs +226 -0
- package/dist/esm-node/server/assets.mjs +16 -0
- package/dist/esm-node/server/dispatch.mjs +308 -0
- package/dist/esm-node/server/index.mjs +4 -0
- package/dist/esm-node/server/types.mjs +1 -0
- package/dist/esm-node/server/worker.mjs +36 -0
- package/dist/esm-node/session/cache.mjs +117 -0
- package/dist/esm-node/session/index.mjs +4 -0
- package/dist/esm-node/session/request.mjs +232 -0
- package/dist/esm-node/session/types.mjs +1 -0
- package/dist/types/abort.d.ts +6 -0
- package/dist/types/adapter.d.ts +195 -0
- package/dist/types/data/body.d.ts +2 -0
- package/dist/types/data/client.d.ts +17 -0
- package/dist/types/data/codec.d.ts +14 -0
- package/dist/types/data/index.d.ts +8 -0
- package/dist/types/data/private.d.ts +6 -0
- package/dist/types/data/routes.d.ts +28 -0
- package/dist/types/data/server.d.ts +59 -0
- package/dist/types/data/static.d.ts +31 -0
- package/dist/types/data/tree.d.ts +3 -0
- package/dist/types/data/types.d.ts +104 -0
- package/dist/types/document.d.ts +76 -0
- package/dist/types/entry-client.d.ts +53 -0
- package/dist/types/entry-server.d.ts +63 -0
- package/dist/types/federation.d.ts +56 -0
- package/dist/types/identity.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/localization.d.ts +58 -0
- package/dist/types/router.d.ts +201 -0
- package/dist/types/server/assets.d.ts +14 -0
- package/dist/types/server/dispatch.d.ts +11 -0
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/types.d.ts +64 -0
- package/dist/types/server/worker.d.ts +25 -0
- package/dist/types/session/cache.d.ts +25 -0
- package/dist/types/session/index.d.ts +4 -0
- package/dist/types/session/request.d.ts +8 -0
- package/dist/types/session/types.d.ts +48 -0
- package/package.json +159 -4
- package/src/abort.ts +40 -0
- package/src/adapter.ts +255 -0
- package/src/data/body.ts +43 -0
- package/src/data/client.ts +501 -0
- package/src/data/codec.ts +247 -0
- package/src/data/index.ts +8 -0
- package/src/data/private.ts +68 -0
- package/src/data/routes.ts +201 -0
- package/src/data/server.ts +977 -0
- package/src/data/static.ts +105 -0
- package/src/data/tree.ts +244 -0
- package/src/data/types.ts +98 -0
- package/src/document.ts +338 -0
- package/src/entry-client.ts +236 -0
- package/src/entry-server.ts +393 -0
- package/src/federation.ts +111 -0
- package/src/identity.ts +128 -0
- package/src/index.ts +7 -0
- package/src/localization.ts +77 -0
- package/src/router.ts +475 -0
- package/src/server/assets.ts +60 -0
- package/src/server/dispatch.ts +523 -0
- package/src/server/index.ts +16 -0
- package/src/server/types.ts +94 -0
- package/src/server/worker.ts +94 -0
- package/src/session/cache.ts +177 -0
- package/src/session/index.ts +11 -0
- package/src/session/request.ts +324 -0
- package/src/session/types.ts +60 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { fromJSON, toJSON } from 'seroval';
|
|
2
|
+
import { validateSerializedData } from './tree';
|
|
3
|
+
|
|
4
|
+
export const DATA_CODEC = 'seroval-json@1.6.8' as const;
|
|
5
|
+
export const MAX_DATA_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_DEPTH = 100;
|
|
7
|
+
const MAX_NODES = 20_000;
|
|
8
|
+
|
|
9
|
+
export class DataProtocolError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'DataProtocolError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Only explicit loader/action results are public. Request context is not data. */
|
|
17
|
+
export function assertPublicData(
|
|
18
|
+
value: unknown,
|
|
19
|
+
options: { privateReferences?: WeakSet<object> } = {},
|
|
20
|
+
): void {
|
|
21
|
+
const seen = new WeakSet<object>();
|
|
22
|
+
let count = 0;
|
|
23
|
+
const visit = (current: unknown, depth: number): void => {
|
|
24
|
+
if (++count > MAX_NODES || depth > MAX_DEPTH) {
|
|
25
|
+
throw new DataProtocolError(
|
|
26
|
+
'Public data exceeds its size or depth limit',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (current === null || current === undefined) return;
|
|
30
|
+
const type = typeof current;
|
|
31
|
+
if (
|
|
32
|
+
(type === 'object' || type === 'function') &&
|
|
33
|
+
options.privateReferences?.has(current as object)
|
|
34
|
+
) {
|
|
35
|
+
throw new DataProtocolError(
|
|
36
|
+
'Request-private context cannot be transferred as public data',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
type === 'string' ||
|
|
41
|
+
type === 'boolean' ||
|
|
42
|
+
type === 'number' ||
|
|
43
|
+
type === 'bigint'
|
|
44
|
+
)
|
|
45
|
+
return;
|
|
46
|
+
if (type !== 'object') {
|
|
47
|
+
throw new DataProtocolError(`Unsupported public data type: ${type}`);
|
|
48
|
+
}
|
|
49
|
+
const object = current as object;
|
|
50
|
+
if (seen.has(object)) return;
|
|
51
|
+
seen.add(object);
|
|
52
|
+
const prototype = Object.getPrototypeOf(object);
|
|
53
|
+
if (
|
|
54
|
+
prototype !== Object.prototype &&
|
|
55
|
+
prototype !== null &&
|
|
56
|
+
prototype !== Array.prototype &&
|
|
57
|
+
prototype !== Date.prototype &&
|
|
58
|
+
prototype !== RegExp.prototype &&
|
|
59
|
+
prototype !== Map.prototype &&
|
|
60
|
+
prototype !== Set.prototype
|
|
61
|
+
) {
|
|
62
|
+
throw new DataProtocolError(
|
|
63
|
+
'Public data must not contain native views, request context or class instances',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const ownKeys = Reflect.ownKeys(object);
|
|
67
|
+
const descriptors: PropertyDescriptor[] = [];
|
|
68
|
+
let hidden: string | undefined;
|
|
69
|
+
for (const key of ownKeys) {
|
|
70
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key)!;
|
|
71
|
+
if (typeof key === 'symbol' || descriptor.get || descriptor.set) {
|
|
72
|
+
throw new DataProtocolError(
|
|
73
|
+
'Public data must not contain symbols or accessors',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
// Serialization enumerates a plain object's own keys, so a hidden
|
|
77
|
+
// property would vanish on the client. Arrays are written by index, and
|
|
78
|
+
// native containers reject own properties below.
|
|
79
|
+
if (
|
|
80
|
+
!descriptor.enumerable &&
|
|
81
|
+
(prototype === Object.prototype || prototype === null)
|
|
82
|
+
)
|
|
83
|
+
hidden ??= key;
|
|
84
|
+
descriptors.push(descriptor);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
prototype === Array.prototype &&
|
|
88
|
+
ownKeys.some(
|
|
89
|
+
key =>
|
|
90
|
+
key !== 'length' &&
|
|
91
|
+
(typeof key !== 'string' ||
|
|
92
|
+
!/^(?:0|[1-9]\d*)$/.test(key) ||
|
|
93
|
+
Number(key) >= 0xffff_ffff),
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
throw new DataProtocolError(
|
|
97
|
+
'Public data arrays must contain only length and canonical index properties',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (prototype === Array.prototype) {
|
|
101
|
+
// The serializer writes every hole of a sparse array, so holes count
|
|
102
|
+
// toward the node budget before it runs.
|
|
103
|
+
const length: unknown = Object.getOwnPropertyDescriptor(
|
|
104
|
+
object,
|
|
105
|
+
'length',
|
|
106
|
+
)?.value;
|
|
107
|
+
if (typeof length === 'number') count += length - (ownKeys.length - 1);
|
|
108
|
+
if (count > MAX_NODES)
|
|
109
|
+
throw new DataProtocolError(
|
|
110
|
+
'Public data exceeds its size or depth limit',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
// Native serializers may read nonenumerable method overrides too. Validate
|
|
114
|
+
// every own value before using a container, without calling its properties.
|
|
115
|
+
for (const descriptor of descriptors) visit(descriptor.value, depth + 1);
|
|
116
|
+
if (hidden !== undefined)
|
|
117
|
+
throw new DataProtocolError(
|
|
118
|
+
`Public data must not contain non-enumerable properties (${hidden})`,
|
|
119
|
+
);
|
|
120
|
+
try {
|
|
121
|
+
if (prototype === Array.prototype && !Array.isArray(object)) {
|
|
122
|
+
throw new TypeError('Missing native array slots');
|
|
123
|
+
}
|
|
124
|
+
if (prototype === Date.prototype) Date.prototype.valueOf.call(object);
|
|
125
|
+
if (prototype === RegExp.prototype) {
|
|
126
|
+
Object.getOwnPropertyDescriptor(RegExp.prototype, 'source')!.get!.call(
|
|
127
|
+
object,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (prototype === Map.prototype)
|
|
131
|
+
Map.prototype.has.call(object, undefined);
|
|
132
|
+
if (prototype === Set.prototype)
|
|
133
|
+
Set.prototype.has.call(object, undefined);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new DataProtocolError(
|
|
136
|
+
'Public data requires valid native container values',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
((prototype === Date.prototype ||
|
|
141
|
+
prototype === Map.prototype ||
|
|
142
|
+
prototype === Set.prototype) &&
|
|
143
|
+
ownKeys.length > 0) ||
|
|
144
|
+
(prototype === RegExp.prototype &&
|
|
145
|
+
ownKeys.some(key => key !== 'lastIndex'))
|
|
146
|
+
) {
|
|
147
|
+
throw new DataProtocolError(
|
|
148
|
+
'Public data native containers must not override their built-in properties',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// A decoded RegExp starts at lastIndex 0, so a global or sticky RegExp
|
|
152
|
+
// advanced by exec/test would silently lose its position.
|
|
153
|
+
if (
|
|
154
|
+
prototype === RegExp.prototype &&
|
|
155
|
+
Object.getOwnPropertyDescriptor(object, 'lastIndex')?.value !== 0
|
|
156
|
+
) {
|
|
157
|
+
throw new DataProtocolError(
|
|
158
|
+
'Public data RegExp values must have lastIndex 0',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (prototype === Map.prototype) {
|
|
162
|
+
for (const [key, entry] of Map.prototype.entries.call(object)) {
|
|
163
|
+
visit(key, depth + 1);
|
|
164
|
+
visit(entry, depth + 1);
|
|
165
|
+
}
|
|
166
|
+
} else if (prototype === Set.prototype) {
|
|
167
|
+
for (const entry of Set.prototype.values.call(object)) {
|
|
168
|
+
visit(entry, depth + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
visit(value, 0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** JSON text stays inert when it is placed in a script element. */
|
|
176
|
+
export function escapeInlineDataJSON(text: string): string {
|
|
177
|
+
return text.replace(/[<>&\u2028\u2029]/gu, character => {
|
|
178
|
+
switch (character) {
|
|
179
|
+
case '<':
|
|
180
|
+
return '\\u003C';
|
|
181
|
+
case '>':
|
|
182
|
+
return '\\u003E';
|
|
183
|
+
case '&':
|
|
184
|
+
return '\\u0026';
|
|
185
|
+
case '\u2028':
|
|
186
|
+
return '\\u2028';
|
|
187
|
+
default:
|
|
188
|
+
return '\\u2029';
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** JSON text. `serializeInlineData` escapes it when it goes into a document. */
|
|
194
|
+
export function serializePublicData(value: unknown): string {
|
|
195
|
+
assertPublicData(value);
|
|
196
|
+
const text = JSON.stringify({
|
|
197
|
+
codec: DATA_CODEC,
|
|
198
|
+
data: toJSON(value, { depthLimit: MAX_DEPTH }),
|
|
199
|
+
});
|
|
200
|
+
// A document carries the escaped form, and the decoder bounds that size.
|
|
201
|
+
if (
|
|
202
|
+
new TextEncoder().encode(escapeInlineDataJSON(text)).byteLength >
|
|
203
|
+
MAX_DATA_BYTES
|
|
204
|
+
) {
|
|
205
|
+
throw new DataProtocolError('Public data exceeds its byte limit');
|
|
206
|
+
}
|
|
207
|
+
// Apply the exact decoder limits to our wire representation before emitting it.
|
|
208
|
+
try {
|
|
209
|
+
validateSerializedData(JSON.parse(text).data);
|
|
210
|
+
} catch {
|
|
211
|
+
throw new DataProtocolError(
|
|
212
|
+
'Public data exceeds the supported codec limits',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return text;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function parsePublicData(text: string): unknown {
|
|
219
|
+
// A document carries the escaped form, and the decoder bounds that size.
|
|
220
|
+
if (
|
|
221
|
+
new TextEncoder().encode(escapeInlineDataJSON(text)).byteLength >
|
|
222
|
+
MAX_DATA_BYTES
|
|
223
|
+
) {
|
|
224
|
+
throw new DataProtocolError('Public data exceeds its byte limit');
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const envelope: unknown = JSON.parse(text);
|
|
228
|
+
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
229
|
+
throw new DataProtocolError('Malformed public data envelope');
|
|
230
|
+
}
|
|
231
|
+
const record = envelope as Record<string, unknown>;
|
|
232
|
+
if (
|
|
233
|
+
Object.keys(record).length !== 2 ||
|
|
234
|
+
record.codec !== DATA_CODEC ||
|
|
235
|
+
!Object.hasOwn(record, 'data')
|
|
236
|
+
) {
|
|
237
|
+
throw new DataProtocolError('Unsupported public data codec');
|
|
238
|
+
}
|
|
239
|
+
validateSerializedData(record.data);
|
|
240
|
+
const result: unknown = fromJSON(record.data);
|
|
241
|
+
assertPublicData(result);
|
|
242
|
+
return result;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof DataProtocolError) throw error;
|
|
245
|
+
throw new DataProtocolError('Malformed public data payload');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './client';
|
|
2
|
+
export * from './codec';
|
|
3
|
+
export type { RequestDataPolicy } from './private';
|
|
4
|
+
export { createRequestDataPolicy } from './private';
|
|
5
|
+
export * from './routes';
|
|
6
|
+
export * from './server';
|
|
7
|
+
export * from './static';
|
|
8
|
+
export * from './types';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { assertPublicData, DataProtocolError } from './codec';
|
|
2
|
+
|
|
3
|
+
export interface RequestDataPolicy {
|
|
4
|
+
assertRoot(value: unknown): void;
|
|
5
|
+
assertValue(value: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Capture aliases before callbacks, and retain them as request context changes. */
|
|
9
|
+
export function createRequestDataPolicy(
|
|
10
|
+
roots: readonly unknown[],
|
|
11
|
+
): RequestDataPolicy {
|
|
12
|
+
const references = new WeakSet<object>();
|
|
13
|
+
const refresh = (): void => {
|
|
14
|
+
const seen = new WeakSet<object>();
|
|
15
|
+
let count = 0;
|
|
16
|
+
const visit = (value: unknown, depth: number): void => {
|
|
17
|
+
if (
|
|
18
|
+
value === null ||
|
|
19
|
+
(typeof value !== 'object' && typeof value !== 'function')
|
|
20
|
+
)
|
|
21
|
+
return;
|
|
22
|
+
if (seen.has(value)) return;
|
|
23
|
+
if (++count > 20_000 || depth > 100) {
|
|
24
|
+
throw new DataProtocolError(
|
|
25
|
+
'Private request context exceeds its size or depth limit',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
seen.add(value);
|
|
29
|
+
references.add(value);
|
|
30
|
+
// Context may hold opaque hosts or callables; owned data properties still
|
|
31
|
+
// carry private identities, while accessors remain completely untouched.
|
|
32
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
34
|
+
if (!descriptor.get && !descriptor.set)
|
|
35
|
+
visit(descriptor.value, depth + 1);
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof Map) {
|
|
38
|
+
for (const [key, entry] of Map.prototype.entries.call(value)) {
|
|
39
|
+
visit(key, depth + 1);
|
|
40
|
+
visit(entry, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
} else if (value instanceof Set) {
|
|
43
|
+
for (const entry of Set.prototype.values.call(value))
|
|
44
|
+
visit(entry, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for (const root of roots) visit(root, 0);
|
|
48
|
+
};
|
|
49
|
+
refresh();
|
|
50
|
+
return {
|
|
51
|
+
assertRoot(value) {
|
|
52
|
+
refresh();
|
|
53
|
+
if (
|
|
54
|
+
value !== null &&
|
|
55
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
56
|
+
references.has(value)
|
|
57
|
+
) {
|
|
58
|
+
throw new DataProtocolError(
|
|
59
|
+
'Request-private context cannot be transferred as public data',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
assertValue(value) {
|
|
64
|
+
refresh();
|
|
65
|
+
assertPublicData(value, { privateReferences: references });
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** Module references remain build-time strings; adapters import their own views. */
|
|
2
|
+
export interface RouteModuleReferences {
|
|
3
|
+
data?: string;
|
|
4
|
+
clientData?: string;
|
|
5
|
+
loading?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
search?: string;
|
|
8
|
+
head?: string;
|
|
9
|
+
notFound?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Filesystem structure only. Matching and scheduling belong to native routers. */
|
|
13
|
+
export interface FileSystemRouteIR {
|
|
14
|
+
id: string;
|
|
15
|
+
file?: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
index?: boolean;
|
|
18
|
+
isRoot?: boolean;
|
|
19
|
+
modules?: RouteModuleReferences;
|
|
20
|
+
children: FileSystemRouteIR[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const moduleFields = [
|
|
24
|
+
'data',
|
|
25
|
+
'clientData',
|
|
26
|
+
'loading',
|
|
27
|
+
'error',
|
|
28
|
+
'search',
|
|
29
|
+
'head',
|
|
30
|
+
'notFound',
|
|
31
|
+
] as const;
|
|
32
|
+
const viewFields = ['file', '_component', 'component', 'filename'] as const;
|
|
33
|
+
|
|
34
|
+
function routeRecord(
|
|
35
|
+
value: unknown,
|
|
36
|
+
location: string,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
if (
|
|
39
|
+
value === null ||
|
|
40
|
+
typeof value !== 'object' ||
|
|
41
|
+
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
42
|
+
Object.getPrototypeOf(value) !== null)
|
|
43
|
+
) {
|
|
44
|
+
throw new TypeError(`${location} must be a plain route object`);
|
|
45
|
+
}
|
|
46
|
+
return value as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function routeField(
|
|
50
|
+
record: Record<string, unknown>,
|
|
51
|
+
key: string,
|
|
52
|
+
location: string,
|
|
53
|
+
) {
|
|
54
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
55
|
+
if (descriptor && !('value' in descriptor)) {
|
|
56
|
+
throw new TypeError(`${location}.${key} must be a value, not an accessor`);
|
|
57
|
+
}
|
|
58
|
+
return descriptor?.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function optionalString(
|
|
62
|
+
record: Record<string, unknown>,
|
|
63
|
+
key: string,
|
|
64
|
+
location: string,
|
|
65
|
+
allowEmpty = false,
|
|
66
|
+
): string | undefined {
|
|
67
|
+
const value = routeField(record, key, location);
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value !== 'string' || (!allowEmpty && value.trim().length === 0)) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
`${location}.${key} must be ${allowEmpty ? 'a' : 'a non-empty'} string`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function optionalBoolean(
|
|
80
|
+
record: Record<string, unknown>,
|
|
81
|
+
key: string,
|
|
82
|
+
location: string,
|
|
83
|
+
): boolean | undefined {
|
|
84
|
+
const value = routeField(record, key, location);
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value !== 'boolean') {
|
|
89
|
+
throw new TypeError(`${location}.${key} must be a boolean`);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Project the structural CLI route shape without retaining source objects,
|
|
96
|
+
* executable handlers, component values, or arbitrary configuration.
|
|
97
|
+
* Input order and conventional path syntax survive the projection unchanged.
|
|
98
|
+
*/
|
|
99
|
+
export function projectFileSystemRoutes(routes: unknown): FileSystemRouteIR[] {
|
|
100
|
+
if (!Array.isArray(routes)) {
|
|
101
|
+
throw new TypeError('routes must be an array');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ids = new Set<string>();
|
|
105
|
+
const ancestors = new Set<object>();
|
|
106
|
+
|
|
107
|
+
const project = (
|
|
108
|
+
value: unknown,
|
|
109
|
+
coordinates: number[],
|
|
110
|
+
): FileSystemRouteIR => {
|
|
111
|
+
const location = `routes[${coordinates.join('].children[')}]`;
|
|
112
|
+
if (coordinates.length > 256) {
|
|
113
|
+
throw new TypeError(`${location} exceeds the maximum route depth`);
|
|
114
|
+
}
|
|
115
|
+
const record = routeRecord(value, location);
|
|
116
|
+
if (ancestors.has(record)) {
|
|
117
|
+
throw new TypeError(`${location} contains a route cycle`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let file: string | undefined;
|
|
121
|
+
for (const key of viewFields) {
|
|
122
|
+
// Validate every recognized reference even when an earlier one wins.
|
|
123
|
+
const reference = optionalString(record, key, location);
|
|
124
|
+
file ??= reference;
|
|
125
|
+
}
|
|
126
|
+
const id =
|
|
127
|
+
optionalString(record, 'id', location) ??
|
|
128
|
+
file ??
|
|
129
|
+
`route-${coordinates.join('.')}`;
|
|
130
|
+
if (ids.has(id)) {
|
|
131
|
+
throw new TypeError(`${location} duplicates route id "${id}"`);
|
|
132
|
+
}
|
|
133
|
+
ids.add(id);
|
|
134
|
+
|
|
135
|
+
const path = optionalString(record, 'path', location, true);
|
|
136
|
+
const index = optionalBoolean(record, 'index', location);
|
|
137
|
+
const isRoot = optionalBoolean(record, 'isRoot', location);
|
|
138
|
+
const children = routeField(record, 'children', location);
|
|
139
|
+
if (children !== undefined && !Array.isArray(children)) {
|
|
140
|
+
throw new TypeError(`${location}.children must be an array`);
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
index === true &&
|
|
144
|
+
((children as unknown[] | undefined)?.length || isRoot === true)
|
|
145
|
+
) {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`${location} index routes cannot have children or be root routes`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const modules: RouteModuleReferences = {};
|
|
152
|
+
for (const key of moduleFields) {
|
|
153
|
+
const reference = optionalString(record, key, location);
|
|
154
|
+
if (reference !== undefined) {
|
|
155
|
+
modules[key] = reference;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ancestors.add(record);
|
|
160
|
+
const projectedChildren = ((children as unknown[] | undefined) ?? []).map(
|
|
161
|
+
(child, position) => project(child, [...coordinates, position]),
|
|
162
|
+
);
|
|
163
|
+
ancestors.delete(record);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
...(file === undefined ? {} : { file }),
|
|
168
|
+
...(path === undefined ? {} : { path }),
|
|
169
|
+
...(index === undefined ? {} : { index }),
|
|
170
|
+
...(isRoot === undefined ? {} : { isRoot }),
|
|
171
|
+
...(Object.keys(modules).length === 0 ? {} : { modules }),
|
|
172
|
+
children: projectedChildren,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return routes.map((route, position) => project(route, [position]));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Convert conventional parameter spelling for TanStack without matching it. */
|
|
180
|
+
export function toTanstackPath(pathname: string): string {
|
|
181
|
+
if (typeof pathname !== 'string') {
|
|
182
|
+
throw new TypeError('route path must be a string');
|
|
183
|
+
}
|
|
184
|
+
return pathname
|
|
185
|
+
.split('/')
|
|
186
|
+
.map(segment => {
|
|
187
|
+
if (segment === '*') {
|
|
188
|
+
return '$';
|
|
189
|
+
}
|
|
190
|
+
if (segment.startsWith(':')) {
|
|
191
|
+
const optional = segment.endsWith('?');
|
|
192
|
+
const name = segment.slice(1, optional ? -1 : undefined);
|
|
193
|
+
if (!name || /[:?*]/.test(name)) {
|
|
194
|
+
throw new TypeError(`Invalid route parameter "${segment}"`);
|
|
195
|
+
}
|
|
196
|
+
return optional ? `{-$${name}}` : `$${name}`;
|
|
197
|
+
}
|
|
198
|
+
return segment;
|
|
199
|
+
})
|
|
200
|
+
.join('/');
|
|
201
|
+
}
|