@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,369 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { untilAborted } from "../abort.mjs";
|
|
3
|
+
import { assertRendererIdentity } from "../identity.mjs";
|
|
4
|
+
import { readBoundedDataText } from "./body.mjs";
|
|
5
|
+
import { DataProtocolError, MAX_DATA_BYTES, parsePublicData } from "./codec.mjs";
|
|
6
|
+
import { STATIC_DATA_PAYLOAD_MAX_BYTES, isPrerenderedDocument, isStaticDataPayload, staticDataPayloadPath } from "./static.mjs";
|
|
7
|
+
import { DATA_CONTENT_TYPE, DATA_PROTOCOL_VERSION, DATA_STREAM_CONTENT_TYPE, DIRECT_PARAM, LOADER_ID_PARAM } from "./types.mjs";
|
|
8
|
+
const isRecord = (value)=>null !== value && 'object' == typeof value && !Array.isArray(value);
|
|
9
|
+
function assertKeys(record, keys, required = []) {
|
|
10
|
+
if (Object.keys(record).some((key)=>!keys.includes(key))) throw new DataProtocolError('Unknown data protocol field');
|
|
11
|
+
if (required.some((key)=>!Object.hasOwn(record, key))) throw new DataProtocolError('Missing data protocol field');
|
|
12
|
+
}
|
|
13
|
+
function assertError(value) {
|
|
14
|
+
if (!isRecord(value)) throw new DataProtocolError('Malformed public error');
|
|
15
|
+
assertKeys(value, [
|
|
16
|
+
'name',
|
|
17
|
+
'message',
|
|
18
|
+
'stack'
|
|
19
|
+
]);
|
|
20
|
+
if ('string' != typeof value['name'] || 'string' != typeof value['message'] || Object.hasOwn(value, 'stack') && 'string' != typeof value['stack']) throw new DataProtocolError('Malformed public error');
|
|
21
|
+
}
|
|
22
|
+
function assertEnvelope(value, expected) {
|
|
23
|
+
if (!isRecord(value) || value['version'] !== DATA_PROTOCOL_VERSION || value['routeId'] !== expected.routeId || value['operation'] !== expected.operation || !isRecord(value['identity']) || !isRecord(value['outcome'])) throw new DataProtocolError('Data protocol or route identity mismatch');
|
|
24
|
+
assertKeys(value, [
|
|
25
|
+
'version',
|
|
26
|
+
'identity',
|
|
27
|
+
'routeId',
|
|
28
|
+
'operation',
|
|
29
|
+
'outcome',
|
|
30
|
+
'deferredKeys'
|
|
31
|
+
]);
|
|
32
|
+
assertKeys(value['identity'], [
|
|
33
|
+
'renderer',
|
|
34
|
+
'appId',
|
|
35
|
+
'entryName',
|
|
36
|
+
'protocolVersion',
|
|
37
|
+
'buildId'
|
|
38
|
+
]);
|
|
39
|
+
try {
|
|
40
|
+
assertRendererIdentity(value['identity'], expected.identity);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new DataProtocolError('Data renderer or build identity mismatch');
|
|
43
|
+
}
|
|
44
|
+
const outcome = value['outcome'];
|
|
45
|
+
if (!Number.isInteger(outcome['status']) || outcome['status'] < 200 || outcome['status'] > 599) throw new DataProtocolError('Malformed data response status');
|
|
46
|
+
switch(outcome['kind']){
|
|
47
|
+
case 'success':
|
|
48
|
+
assertKeys(outcome, [
|
|
49
|
+
'kind',
|
|
50
|
+
'value',
|
|
51
|
+
'status'
|
|
52
|
+
], [
|
|
53
|
+
'value'
|
|
54
|
+
]);
|
|
55
|
+
break;
|
|
56
|
+
case 'redirect':
|
|
57
|
+
assertKeys(outcome, [
|
|
58
|
+
'kind',
|
|
59
|
+
'location',
|
|
60
|
+
'status'
|
|
61
|
+
]);
|
|
62
|
+
if ('string' != typeof outcome['location'] || ![
|
|
63
|
+
301,
|
|
64
|
+
302,
|
|
65
|
+
303,
|
|
66
|
+
307,
|
|
67
|
+
308
|
|
68
|
+
].includes(outcome['status'])) throw new DataProtocolError('Malformed data redirect');
|
|
69
|
+
break;
|
|
70
|
+
case 'not-found':
|
|
71
|
+
assertKeys(outcome, [
|
|
72
|
+
'kind',
|
|
73
|
+
'value',
|
|
74
|
+
'thrown',
|
|
75
|
+
'status'
|
|
76
|
+
], [
|
|
77
|
+
'value'
|
|
78
|
+
]);
|
|
79
|
+
if (404 !== outcome['status'] || 'boolean' != typeof outcome['thrown']) throw new DataProtocolError('Malformed not-found outcome');
|
|
80
|
+
break;
|
|
81
|
+
case 'error':
|
|
82
|
+
assertKeys(outcome, [
|
|
83
|
+
'kind',
|
|
84
|
+
'error',
|
|
85
|
+
'data',
|
|
86
|
+
'thrown',
|
|
87
|
+
'status'
|
|
88
|
+
]);
|
|
89
|
+
assertError(outcome['error']);
|
|
90
|
+
if ('boolean' != typeof outcome['thrown']) throw new DataProtocolError('Malformed error outcome');
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw new DataProtocolError('Unknown data outcome');
|
|
94
|
+
}
|
|
95
|
+
if (Object.hasOwn(value, 'deferredKeys')) {
|
|
96
|
+
if (!Array.isArray(value['deferredKeys']) || value['deferredKeys'].some((key)=>'string' != typeof key) || new Set(value['deferredKeys']).size !== value['deferredKeys'].length || 'success' !== outcome['kind'] || !isRecord(outcome['value']) || value['deferredKeys'].some((key)=>Object.hasOwn(outcome['value'], key))) throw new DataProtocolError('Malformed deferred data keys');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function dataError(error) {
|
|
100
|
+
const result = new Error(error.message);
|
|
101
|
+
result.name = error.name;
|
|
102
|
+
if (void 0 !== error.stack) result.stack = error.stack;
|
|
103
|
+
else delete result.stack;
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
async function readDeferredResponse(response, expected, signal) {
|
|
107
|
+
if (!response.body) throw new DataProtocolError('Missing deferred data body');
|
|
108
|
+
const reader = response.body.getReader();
|
|
109
|
+
const decoder = new TextDecoder('utf-8', {
|
|
110
|
+
fatal: true
|
|
111
|
+
});
|
|
112
|
+
let buffer = '';
|
|
113
|
+
let initialized = false;
|
|
114
|
+
let completed = false;
|
|
115
|
+
let initialResolve;
|
|
116
|
+
let initialReject;
|
|
117
|
+
const initial = new Promise((resolve, reject)=>{
|
|
118
|
+
initialResolve = resolve;
|
|
119
|
+
initialReject = reject;
|
|
120
|
+
});
|
|
121
|
+
let completionResolve;
|
|
122
|
+
let completionReject;
|
|
123
|
+
const completion = new Promise((resolve, reject)=>{
|
|
124
|
+
completionResolve = resolve;
|
|
125
|
+
completionReject = reject;
|
|
126
|
+
});
|
|
127
|
+
completion.catch(()=>void 0);
|
|
128
|
+
const pending = new Map();
|
|
129
|
+
const reject = (error)=>{
|
|
130
|
+
completionReject(error);
|
|
131
|
+
if (!initialized) initialReject(error);
|
|
132
|
+
for (const promise of pending.values())promise.reject(error);
|
|
133
|
+
pending.clear();
|
|
134
|
+
};
|
|
135
|
+
const abort = ()=>{
|
|
136
|
+
const reason = signal?.reason ?? new DOMException('The data request was aborted', 'AbortError');
|
|
137
|
+
reject(reason);
|
|
138
|
+
reader.cancel(reason).catch(()=>void 0);
|
|
139
|
+
};
|
|
140
|
+
if (signal?.aborted) abort();
|
|
141
|
+
else signal?.addEventListener('abort', abort, {
|
|
142
|
+
once: true
|
|
143
|
+
});
|
|
144
|
+
const consume = (text)=>{
|
|
145
|
+
if (completed) throw new DataProtocolError('Data frames follow stream completion');
|
|
146
|
+
const frame = parsePublicData(text);
|
|
147
|
+
if (!isRecord(frame)) throw new DataProtocolError('Malformed data stream frame');
|
|
148
|
+
if ('initial' === frame['type']) {
|
|
149
|
+
assertKeys(frame, [
|
|
150
|
+
'type',
|
|
151
|
+
'envelope'
|
|
152
|
+
]);
|
|
153
|
+
if (initialized) throw new DataProtocolError('Duplicate initial data frame');
|
|
154
|
+
assertEnvelope(frame['envelope'], expected);
|
|
155
|
+
const envelope = frame['envelope'];
|
|
156
|
+
if (!Array.isArray(envelope.deferredKeys) || 'success' !== envelope.outcome.kind) throw new DataProtocolError('Initial stream frame needs deferred keys');
|
|
157
|
+
const value = envelope.outcome.value;
|
|
158
|
+
for (const key of envelope.deferredKeys){
|
|
159
|
+
const promise = new Promise((resolve, rejectPromise)=>{
|
|
160
|
+
pending.set(key, {
|
|
161
|
+
resolve,
|
|
162
|
+
reject: rejectPromise
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
promise.catch(()=>void 0);
|
|
166
|
+
Object.defineProperty(value, key, {
|
|
167
|
+
value: promise,
|
|
168
|
+
writable: true,
|
|
169
|
+
enumerable: true,
|
|
170
|
+
configurable: true
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
initialized = true;
|
|
174
|
+
const outcome = {
|
|
175
|
+
...envelope.outcome,
|
|
176
|
+
value
|
|
177
|
+
};
|
|
178
|
+
Object.defineProperty(outcome, 'completion', {
|
|
179
|
+
value: completion
|
|
180
|
+
});
|
|
181
|
+
initialResolve(outcome);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!initialized) throw new DataProtocolError('Deferred data arrived before the initial frame');
|
|
185
|
+
if ('complete' === frame['type']) {
|
|
186
|
+
assertKeys(frame, [
|
|
187
|
+
'type'
|
|
188
|
+
]);
|
|
189
|
+
if (0 !== pending.size) throw new DataProtocolError('Deferred stream completed with unresolved values');
|
|
190
|
+
completed = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if ('resolve' !== frame['type'] && 'reject' !== frame['type']) throw new DataProtocolError('Unknown data stream frame');
|
|
194
|
+
'resolve' === frame['type'] ? assertKeys(frame, [
|
|
195
|
+
'type',
|
|
196
|
+
'key',
|
|
197
|
+
'value'
|
|
198
|
+
], [
|
|
199
|
+
'value'
|
|
200
|
+
]) : assertKeys(frame, [
|
|
201
|
+
'type',
|
|
202
|
+
'key',
|
|
203
|
+
'error'
|
|
204
|
+
]);
|
|
205
|
+
if ('string' != typeof frame['key'] || !pending.has(frame['key'])) throw new DataProtocolError('Unknown or duplicate deferred data key');
|
|
206
|
+
const promise = pending.get(frame['key']);
|
|
207
|
+
if ('resolve' === frame['type']) promise.resolve(frame['value']);
|
|
208
|
+
else {
|
|
209
|
+
assertError(frame['error']);
|
|
210
|
+
promise.reject(dataError(frame['error']));
|
|
211
|
+
}
|
|
212
|
+
pending.delete(frame['key']);
|
|
213
|
+
};
|
|
214
|
+
const append = (text)=>{
|
|
215
|
+
buffer += text;
|
|
216
|
+
let lineEnd = buffer.indexOf('\n');
|
|
217
|
+
while(-1 !== lineEnd){
|
|
218
|
+
const line = buffer.slice(0, lineEnd);
|
|
219
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
220
|
+
if (!line) throw new DataProtocolError('Empty data stream frame');
|
|
221
|
+
consume(line);
|
|
222
|
+
lineEnd = buffer.indexOf('\n');
|
|
223
|
+
}
|
|
224
|
+
if (new TextEncoder().encode(buffer).byteLength > MAX_DATA_BYTES) throw new DataProtocolError('Deferred data frame exceeds its size limit');
|
|
225
|
+
};
|
|
226
|
+
(async ()=>{
|
|
227
|
+
try {
|
|
228
|
+
while(true){
|
|
229
|
+
const chunk = await reader.read();
|
|
230
|
+
if (chunk.done) {
|
|
231
|
+
append(decoder.decode());
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
for(let offset = 0; offset < chunk.value.byteLength; offset += 65536)append(decoder.decode(chunk.value.subarray(offset, offset + 65536), {
|
|
235
|
+
stream: true
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
if ('' !== buffer || !completed) throw new DataProtocolError('Truncated deferred data stream');
|
|
239
|
+
completionResolve();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
reject(error);
|
|
242
|
+
reader.cancel(error).catch(()=>void 0);
|
|
243
|
+
} finally{
|
|
244
|
+
signal?.removeEventListener('abort', abort);
|
|
245
|
+
reader.releaseLock();
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
return initial;
|
|
249
|
+
}
|
|
250
|
+
async function readDataResponse(response, expected, signal) {
|
|
251
|
+
const discard = (reason)=>{
|
|
252
|
+
response.body?.cancel(reason).catch(()=>void 0);
|
|
253
|
+
};
|
|
254
|
+
if (signal?.aborted) {
|
|
255
|
+
discard(signal.reason);
|
|
256
|
+
signal.throwIfAborted();
|
|
257
|
+
}
|
|
258
|
+
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim();
|
|
259
|
+
if (contentType === DATA_STREAM_CONTENT_TYPE) return readDeferredResponse(response, expected, signal);
|
|
260
|
+
if (contentType !== DATA_CONTENT_TYPE) {
|
|
261
|
+
const error = new DataProtocolError(`Expected an UltraModern data response, received HTTP ${response.status}`);
|
|
262
|
+
discard(error);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
const value = parsePublicData(await readBoundedDataText(response, signal));
|
|
266
|
+
signal?.throwIfAborted();
|
|
267
|
+
assertEnvelope(value, expected);
|
|
268
|
+
if (Object.hasOwn(value, 'deferredKeys')) throw new DataProtocolError('Deferred values require the stream protocol');
|
|
269
|
+
return value.outcome;
|
|
270
|
+
}
|
|
271
|
+
async function readStaticPayload(url, routeId, fetchData, request) {
|
|
272
|
+
if (url.search || !isPrerenderedDocument()) return;
|
|
273
|
+
let response;
|
|
274
|
+
try {
|
|
275
|
+
response = await untilAborted(fetchData(new URL(staticDataPayloadPath(url.pathname, routeId), url), {
|
|
276
|
+
credentials: 'same-origin',
|
|
277
|
+
signal: request.signal
|
|
278
|
+
}), request.signal);
|
|
279
|
+
} catch {
|
|
280
|
+
request.signal.throwIfAborted();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!response.ok) return void response.body?.cancel().catch(()=>void 0);
|
|
284
|
+
let payload;
|
|
285
|
+
try {
|
|
286
|
+
payload = JSON.parse(await readBoundedDataText(response, request.signal, STATIC_DATA_PAYLOAD_MAX_BYTES));
|
|
287
|
+
} catch {
|
|
288
|
+
request.signal.throwIfAborted();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!isStaticDataPayload(payload)) return;
|
|
292
|
+
return new Response(payload.body, {
|
|
293
|
+
status: payload.status,
|
|
294
|
+
headers: {
|
|
295
|
+
'content-type': payload.contentType
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function bufferRequestBody(request) {
|
|
300
|
+
if (!request.body) return null;
|
|
301
|
+
const { signal } = request;
|
|
302
|
+
if (/^multipart\/form-data\b/iu.test(request.headers.get('content-type') ?? '')) return untilAborted(request.blob(), signal);
|
|
303
|
+
const reader = request.body.getReader();
|
|
304
|
+
const cancel = ()=>{
|
|
305
|
+
reader.cancel(signal.reason).catch(()=>{});
|
|
306
|
+
};
|
|
307
|
+
signal.addEventListener('abort', cancel, {
|
|
308
|
+
once: true
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const chunks = [];
|
|
312
|
+
for(;;){
|
|
313
|
+
const part = await reader.read();
|
|
314
|
+
signal.throwIfAborted();
|
|
315
|
+
if (part.done) break;
|
|
316
|
+
chunks.push(part.value);
|
|
317
|
+
}
|
|
318
|
+
return new Blob(chunks, {
|
|
319
|
+
type: request.headers.get('content-type') ?? ''
|
|
320
|
+
});
|
|
321
|
+
} finally{
|
|
322
|
+
signal.removeEventListener('abort', cancel);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function createDataClient(routeId, identity, options = {}) {
|
|
326
|
+
if (!routeId) throw new DataProtocolError('A data client requires a route ID');
|
|
327
|
+
const fetchData = options.fetch ?? globalThis.fetch;
|
|
328
|
+
const execute = async (operation, input)=>{
|
|
329
|
+
const request = input.request;
|
|
330
|
+
request.signal.throwIfAborted();
|
|
331
|
+
if ('loader' === operation && 'GET' !== request.method) throw new DataProtocolError('A loader client needs a GET Request');
|
|
332
|
+
if ('action' === operation && [
|
|
333
|
+
'GET',
|
|
334
|
+
'HEAD'
|
|
335
|
+
].includes(request.method)) throw new DataProtocolError('An action client needs a mutation Request');
|
|
336
|
+
const url = new URL(request.url);
|
|
337
|
+
if ('loader' === operation) {
|
|
338
|
+
const replayed = await readStaticPayload(url, routeId, fetchData, request);
|
|
339
|
+
if (replayed) return readDataResponse(replayed, {
|
|
340
|
+
identity,
|
|
341
|
+
routeId,
|
|
342
|
+
operation
|
|
343
|
+
}, request.signal);
|
|
344
|
+
}
|
|
345
|
+
url.searchParams.set(LOADER_ID_PARAM, routeId);
|
|
346
|
+
url.searchParams.set(DIRECT_PARAM, 'true');
|
|
347
|
+
const proxyRequest = new Request(url, {
|
|
348
|
+
method: request.method,
|
|
349
|
+
headers: request.headers,
|
|
350
|
+
body: 'action' === operation ? await bufferRequestBody(request) : null,
|
|
351
|
+
signal: request.signal
|
|
352
|
+
});
|
|
353
|
+
const response = await untilAborted(fetchData(proxyRequest, {
|
|
354
|
+
credentials: 'same-origin',
|
|
355
|
+
redirect: 'manual',
|
|
356
|
+
signal: request.signal
|
|
357
|
+
}), request.signal);
|
|
358
|
+
return readDataResponse(response, {
|
|
359
|
+
identity,
|
|
360
|
+
routeId,
|
|
361
|
+
operation
|
|
362
|
+
}, request.signal);
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
loader: (input)=>execute('loader', input),
|
|
366
|
+
action: (input)=>execute('action', input)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
export { createDataClient, readDataResponse };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { fromJSON, toJSON } from "seroval";
|
|
3
|
+
import { validateSerializedData } from "./tree.mjs";
|
|
4
|
+
const DATA_CODEC = 'seroval-json@1.6.8';
|
|
5
|
+
const MAX_DATA_BYTES = 1048576;
|
|
6
|
+
const MAX_DEPTH = 100;
|
|
7
|
+
const MAX_NODES = 20000;
|
|
8
|
+
class DataProtocolError extends Error {
|
|
9
|
+
constructor(message){
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'DataProtocolError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertPublicData(value, options = {}) {
|
|
15
|
+
const seen = new WeakSet();
|
|
16
|
+
let count = 0;
|
|
17
|
+
const visit = (current, depth)=>{
|
|
18
|
+
if (++count > MAX_NODES || depth > MAX_DEPTH) throw new DataProtocolError('Public data exceeds its size or depth limit');
|
|
19
|
+
if (null == current) return;
|
|
20
|
+
const type = typeof current;
|
|
21
|
+
if (('object' === type || 'function' === type) && options.privateReferences?.has(current)) throw new DataProtocolError('Request-private context cannot be transferred as public data');
|
|
22
|
+
if ('string' === type || 'boolean' === type || 'number' === type || 'bigint' === type) return;
|
|
23
|
+
if ('object' !== type) throw new DataProtocolError(`Unsupported public data type: ${type}`);
|
|
24
|
+
const object = current;
|
|
25
|
+
if (seen.has(object)) return;
|
|
26
|
+
seen.add(object);
|
|
27
|
+
const prototype = Object.getPrototypeOf(object);
|
|
28
|
+
if (prototype !== Object.prototype && null !== prototype && prototype !== Array.prototype && prototype !== Date.prototype && prototype !== RegExp.prototype && prototype !== Map.prototype && prototype !== Set.prototype) throw new DataProtocolError('Public data must not contain native views, request context or class instances');
|
|
29
|
+
const ownKeys = Reflect.ownKeys(object);
|
|
30
|
+
const descriptors = [];
|
|
31
|
+
let hidden;
|
|
32
|
+
for (const key of ownKeys){
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
34
|
+
if ('symbol' == typeof key || descriptor.get || descriptor.set) throw new DataProtocolError('Public data must not contain symbols or accessors');
|
|
35
|
+
if (!descriptor.enumerable && (prototype === Object.prototype || null === prototype)) hidden ??= key;
|
|
36
|
+
descriptors.push(descriptor);
|
|
37
|
+
}
|
|
38
|
+
if (prototype === Array.prototype && ownKeys.some((key)=>'length' !== key && ('string' != typeof key || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= 0xffffffff))) throw new DataProtocolError('Public data arrays must contain only length and canonical index properties');
|
|
39
|
+
if (prototype === Array.prototype) {
|
|
40
|
+
const length = Object.getOwnPropertyDescriptor(object, 'length')?.value;
|
|
41
|
+
if ('number' == typeof length) count += length - (ownKeys.length - 1);
|
|
42
|
+
if (count > MAX_NODES) throw new DataProtocolError('Public data exceeds its size or depth limit');
|
|
43
|
+
}
|
|
44
|
+
for (const descriptor of descriptors)visit(descriptor.value, depth + 1);
|
|
45
|
+
if (void 0 !== hidden) throw new DataProtocolError(`Public data must not contain non-enumerable properties (${hidden})`);
|
|
46
|
+
try {
|
|
47
|
+
if (prototype === Array.prototype && !Array.isArray(object)) throw new TypeError('Missing native array slots');
|
|
48
|
+
if (prototype === Date.prototype) Date.prototype.valueOf.call(object);
|
|
49
|
+
if (prototype === RegExp.prototype) Object.getOwnPropertyDescriptor(RegExp.prototype, 'source').get.call(object);
|
|
50
|
+
if (prototype === Map.prototype) Map.prototype.has.call(object, void 0);
|
|
51
|
+
if (prototype === Set.prototype) Set.prototype.has.call(object, void 0);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new DataProtocolError('Public data requires valid native container values');
|
|
54
|
+
}
|
|
55
|
+
if ((prototype === Date.prototype || prototype === Map.prototype || prototype === Set.prototype) && ownKeys.length > 0 || prototype === RegExp.prototype && ownKeys.some((key)=>'lastIndex' !== key)) throw new DataProtocolError('Public data native containers must not override their built-in properties');
|
|
56
|
+
if (prototype === RegExp.prototype && Object.getOwnPropertyDescriptor(object, 'lastIndex')?.value !== 0) throw new DataProtocolError('Public data RegExp values must have lastIndex 0');
|
|
57
|
+
if (prototype === Map.prototype) for (const [key, entry] of Map.prototype.entries.call(object)){
|
|
58
|
+
visit(key, depth + 1);
|
|
59
|
+
visit(entry, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
else if (prototype === Set.prototype) for (const entry of Set.prototype.values.call(object))visit(entry, depth + 1);
|
|
62
|
+
};
|
|
63
|
+
visit(value, 0);
|
|
64
|
+
}
|
|
65
|
+
function escapeInlineDataJSON(text) {
|
|
66
|
+
return text.replace(/[<>&\u2028\u2029]/gu, (character)=>{
|
|
67
|
+
switch(character){
|
|
68
|
+
case '<':
|
|
69
|
+
return '\\u003C';
|
|
70
|
+
case '>':
|
|
71
|
+
return '\\u003E';
|
|
72
|
+
case '&':
|
|
73
|
+
return '\\u0026';
|
|
74
|
+
case '\u2028':
|
|
75
|
+
return '\\u2028';
|
|
76
|
+
default:
|
|
77
|
+
return '\\u2029';
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function serializePublicData(value) {
|
|
82
|
+
assertPublicData(value);
|
|
83
|
+
const text = JSON.stringify({
|
|
84
|
+
codec: DATA_CODEC,
|
|
85
|
+
data: toJSON(value, {
|
|
86
|
+
depthLimit: MAX_DEPTH
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
if (new TextEncoder().encode(escapeInlineDataJSON(text)).byteLength > MAX_DATA_BYTES) throw new DataProtocolError('Public data exceeds its byte limit');
|
|
90
|
+
try {
|
|
91
|
+
validateSerializedData(JSON.parse(text).data);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new DataProtocolError('Public data exceeds the supported codec limits');
|
|
94
|
+
}
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
function parsePublicData(text) {
|
|
98
|
+
if (new TextEncoder().encode(escapeInlineDataJSON(text)).byteLength > MAX_DATA_BYTES) throw new DataProtocolError('Public data exceeds its byte limit');
|
|
99
|
+
try {
|
|
100
|
+
const envelope = JSON.parse(text);
|
|
101
|
+
if (!envelope || 'object' != typeof envelope || Array.isArray(envelope)) throw new DataProtocolError('Malformed public data envelope');
|
|
102
|
+
const record = envelope;
|
|
103
|
+
if (2 !== Object.keys(record).length || record.codec !== DATA_CODEC || !Object.hasOwn(record, 'data')) throw new DataProtocolError('Unsupported public data codec');
|
|
104
|
+
validateSerializedData(record.data);
|
|
105
|
+
const result = fromJSON(record.data);
|
|
106
|
+
assertPublicData(result);
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof DataProtocolError) throw error;
|
|
110
|
+
throw new DataProtocolError('Malformed public data payload');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export { DATA_CODEC, DataProtocolError, MAX_DATA_BYTES, assertPublicData, escapeInlineDataJSON, parsePublicData, serializePublicData };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
export * from "./client.mjs";
|
|
3
|
+
export * from "./codec.mjs";
|
|
4
|
+
export * from "./routes.mjs";
|
|
5
|
+
export * from "./server.mjs";
|
|
6
|
+
export * from "./static.mjs";
|
|
7
|
+
export * from "./types.mjs";
|
|
8
|
+
export { createRequestDataPolicy } from "./private.mjs";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { DataProtocolError, assertPublicData } from "./codec.mjs";
|
|
3
|
+
function createRequestDataPolicy(roots) {
|
|
4
|
+
const references = new WeakSet();
|
|
5
|
+
const refresh = ()=>{
|
|
6
|
+
const seen = new WeakSet();
|
|
7
|
+
let count = 0;
|
|
8
|
+
const visit = (value, depth)=>{
|
|
9
|
+
if (null === value || 'object' != typeof value && 'function' != typeof value) return;
|
|
10
|
+
if (seen.has(value)) return;
|
|
11
|
+
if (++count > 20000 || depth > 100) throw new DataProtocolError('Private request context exceeds its size or depth limit');
|
|
12
|
+
seen.add(value);
|
|
13
|
+
references.add(value);
|
|
14
|
+
for (const key of Reflect.ownKeys(value)){
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
16
|
+
if (!descriptor.get && !descriptor.set) visit(descriptor.value, depth + 1);
|
|
17
|
+
}
|
|
18
|
+
if (value instanceof Map) for (const [key, entry] of Map.prototype.entries.call(value)){
|
|
19
|
+
visit(key, depth + 1);
|
|
20
|
+
visit(entry, depth + 1);
|
|
21
|
+
}
|
|
22
|
+
else if (value instanceof Set) for (const entry of Set.prototype.values.call(value))visit(entry, depth + 1);
|
|
23
|
+
};
|
|
24
|
+
for (const root of roots)visit(root, 0);
|
|
25
|
+
};
|
|
26
|
+
refresh();
|
|
27
|
+
return {
|
|
28
|
+
assertRoot (value) {
|
|
29
|
+
refresh();
|
|
30
|
+
if (null !== value && ('object' == typeof value || 'function' == typeof value) && references.has(value)) throw new DataProtocolError('Request-private context cannot be transferred as public data');
|
|
31
|
+
},
|
|
32
|
+
assertValue (value) {
|
|
33
|
+
refresh();
|
|
34
|
+
assertPublicData(value, {
|
|
35
|
+
privateReferences: references
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export { createRequestDataPolicy };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const moduleFields = [
|
|
3
|
+
'data',
|
|
4
|
+
'clientData',
|
|
5
|
+
'loading',
|
|
6
|
+
'error',
|
|
7
|
+
'search',
|
|
8
|
+
'head',
|
|
9
|
+
'notFound'
|
|
10
|
+
];
|
|
11
|
+
const viewFields = [
|
|
12
|
+
'file',
|
|
13
|
+
'_component',
|
|
14
|
+
'component',
|
|
15
|
+
'filename'
|
|
16
|
+
];
|
|
17
|
+
function routeRecord(value, location) {
|
|
18
|
+
if (null === value || 'object' != typeof value || Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`${location} must be a plain route object`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function routeField(record, key, location) {
|
|
22
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
23
|
+
if (descriptor && !('value' in descriptor)) throw new TypeError(`${location}.${key} must be a value, not an accessor`);
|
|
24
|
+
return descriptor?.value;
|
|
25
|
+
}
|
|
26
|
+
function optionalString(record, key, location, allowEmpty = false) {
|
|
27
|
+
const value = routeField(record, key, location);
|
|
28
|
+
if (void 0 === value) return;
|
|
29
|
+
if ('string' != typeof value || !allowEmpty && 0 === value.trim().length) throw new TypeError(`${location}.${key} must be ${allowEmpty ? 'a' : 'a non-empty'} string`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function optionalBoolean(record, key, location) {
|
|
33
|
+
const value = routeField(record, key, location);
|
|
34
|
+
if (void 0 === value) return;
|
|
35
|
+
if ('boolean' != typeof value) throw new TypeError(`${location}.${key} must be a boolean`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function projectFileSystemRoutes(routes) {
|
|
39
|
+
if (!Array.isArray(routes)) throw new TypeError('routes must be an array');
|
|
40
|
+
const ids = new Set();
|
|
41
|
+
const ancestors = new Set();
|
|
42
|
+
const project = (value, coordinates)=>{
|
|
43
|
+
const location = `routes[${coordinates.join('].children[')}]`;
|
|
44
|
+
if (coordinates.length > 256) throw new TypeError(`${location} exceeds the maximum route depth`);
|
|
45
|
+
const record = routeRecord(value, location);
|
|
46
|
+
if (ancestors.has(record)) throw new TypeError(`${location} contains a route cycle`);
|
|
47
|
+
let file;
|
|
48
|
+
for (const key of viewFields){
|
|
49
|
+
const reference = optionalString(record, key, location);
|
|
50
|
+
file ??= reference;
|
|
51
|
+
}
|
|
52
|
+
const id = optionalString(record, 'id', location) ?? file ?? `route-${coordinates.join('.')}`;
|
|
53
|
+
if (ids.has(id)) throw new TypeError(`${location} duplicates route id "${id}"`);
|
|
54
|
+
ids.add(id);
|
|
55
|
+
const path = optionalString(record, 'path', location, true);
|
|
56
|
+
const index = optionalBoolean(record, 'index', location);
|
|
57
|
+
const isRoot = optionalBoolean(record, 'isRoot', location);
|
|
58
|
+
const children = routeField(record, 'children', location);
|
|
59
|
+
if (void 0 !== children && !Array.isArray(children)) throw new TypeError(`${location}.children must be an array`);
|
|
60
|
+
if (true === index && (children?.length || true === isRoot)) throw new TypeError(`${location} index routes cannot have children or be root routes`);
|
|
61
|
+
const modules = {};
|
|
62
|
+
for (const key of moduleFields){
|
|
63
|
+
const reference = optionalString(record, key, location);
|
|
64
|
+
if (void 0 !== reference) modules[key] = reference;
|
|
65
|
+
}
|
|
66
|
+
ancestors.add(record);
|
|
67
|
+
const projectedChildren = (children ?? []).map((child, position)=>project(child, [
|
|
68
|
+
...coordinates,
|
|
69
|
+
position
|
|
70
|
+
]));
|
|
71
|
+
ancestors.delete(record);
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
...void 0 === file ? {} : {
|
|
75
|
+
file
|
|
76
|
+
},
|
|
77
|
+
...void 0 === path ? {} : {
|
|
78
|
+
path
|
|
79
|
+
},
|
|
80
|
+
...void 0 === index ? {} : {
|
|
81
|
+
index
|
|
82
|
+
},
|
|
83
|
+
...void 0 === isRoot ? {} : {
|
|
84
|
+
isRoot
|
|
85
|
+
},
|
|
86
|
+
...0 === Object.keys(modules).length ? {} : {
|
|
87
|
+
modules
|
|
88
|
+
},
|
|
89
|
+
children: projectedChildren
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
return routes.map((route, position)=>project(route, [
|
|
93
|
+
position
|
|
94
|
+
]));
|
|
95
|
+
}
|
|
96
|
+
function toTanstackPath(pathname) {
|
|
97
|
+
if ('string' != typeof pathname) throw new TypeError('route path must be a string');
|
|
98
|
+
return pathname.split('/').map((segment)=>{
|
|
99
|
+
if ('*' === segment) return '$';
|
|
100
|
+
if (segment.startsWith(':')) {
|
|
101
|
+
const optional = segment.endsWith('?');
|
|
102
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
103
|
+
if (!name || /[:?*]/.test(name)) throw new TypeError(`Invalid route parameter "${segment}"`);
|
|
104
|
+
return optional ? `{-$${name}}` : `$${name}`;
|
|
105
|
+
}
|
|
106
|
+
return segment;
|
|
107
|
+
}).join('/');
|
|
108
|
+
}
|
|
109
|
+
export { projectFileSystemRoutes, toTanstackPath };
|