@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,51 @@
|
|
|
1
|
+
import { MAX_DATA_BYTES } from "./codec.mjs";
|
|
2
|
+
import { DATA_CONTENT_TYPE, DATA_STREAM_CONTENT_TYPE } from "./types.mjs";
|
|
3
|
+
const PRERENDERED_DOCUMENT_META = 'ultramodern-prerendered';
|
|
4
|
+
const STATIC_DATA_DIRECTORY = '__ultramodern-data';
|
|
5
|
+
function encodeRouteId(routeId) {
|
|
6
|
+
let binary = '';
|
|
7
|
+
for (const byte of new TextEncoder().encode(routeId))binary += String.fromCharCode(byte);
|
|
8
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
|
|
9
|
+
}
|
|
10
|
+
function staticDataPayloadPath(pathname, routeId) {
|
|
11
|
+
if (!routeId) throw new TypeError('A static data payload needs a route ID');
|
|
12
|
+
const directory = pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
13
|
+
return `${directory}${STATIC_DATA_DIRECTORY}/${encodeRouteId(routeId)}.json`;
|
|
14
|
+
}
|
|
15
|
+
const STATIC_DATA_PAYLOAD_MAX_BYTES = 6 * MAX_DATA_BYTES + 1024;
|
|
16
|
+
const payloadTooLarge = (bytes)=>new RangeError(`A static loader payload of ${bytes} bytes exceeds the ${STATIC_DATA_PAYLOAD_MAX_BYTES} bytes the data client replays; reduce its (deferred) data or serve the route dynamically.`);
|
|
17
|
+
async function readStaticDataPayloadBody(response) {
|
|
18
|
+
if (!response.body) return '';
|
|
19
|
+
const reader = response.body.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
let text = '';
|
|
23
|
+
for(;;){
|
|
24
|
+
const part = await reader.read();
|
|
25
|
+
if (part.done) return text + decoder.decode();
|
|
26
|
+
bytes += part.value.byteLength;
|
|
27
|
+
if (bytes > STATIC_DATA_PAYLOAD_MAX_BYTES) {
|
|
28
|
+
const error = payloadTooLarge(`more than ${bytes}`);
|
|
29
|
+
reader.cancel(error).catch(()=>{});
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
text += decoder.decode(part.value, {
|
|
33
|
+
stream: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function encodeStaticDataPayload(payload) {
|
|
38
|
+
const text = JSON.stringify(payload);
|
|
39
|
+
const bytes = new TextEncoder().encode(text).byteLength;
|
|
40
|
+
if (bytes > STATIC_DATA_PAYLOAD_MAX_BYTES) throw payloadTooLarge(bytes);
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
function isStaticDataPayload(value) {
|
|
44
|
+
if (null === value || 'object' != typeof value || Array.isArray(value)) return false;
|
|
45
|
+
const payload = value;
|
|
46
|
+
return Number.isInteger(payload.status) && payload.status >= 200 && payload.status <= 599 && (payload.contentType === DATA_CONTENT_TYPE || payload.contentType === DATA_STREAM_CONTENT_TYPE) && 'string' == typeof payload.body;
|
|
47
|
+
}
|
|
48
|
+
function isPrerenderedDocument() {
|
|
49
|
+
return "u" > typeof document && null !== document.querySelector(`meta[name="${PRERENDERED_DOCUMENT_META}"]`);
|
|
50
|
+
}
|
|
51
|
+
export { PRERENDERED_DOCUMENT_META, STATIC_DATA_DIRECTORY, STATIC_DATA_PAYLOAD_MAX_BYTES, encodeStaticDataPayload, isPrerenderedDocument, isStaticDataPayload, readStaticDataPayloadBody, staticDataPayloadPath };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const MAX_NODES = 20000;
|
|
2
|
+
const MAX_DEPTH = 300;
|
|
3
|
+
function record(value) {
|
|
4
|
+
if (null === value || 'object' != typeof value || Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError('Malformed public data tree record');
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function exactFields(value, fields) {
|
|
8
|
+
if (Reflect.ownKeys(value).length !== fields.length || fields.some((field)=>{
|
|
9
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, field);
|
|
10
|
+
return void 0 === descriptor || !('value' in descriptor);
|
|
11
|
+
})) throw new TypeError('Malformed public data node fields');
|
|
12
|
+
}
|
|
13
|
+
function integer(value, maximum) {
|
|
14
|
+
return 'number' == typeof value && Number.isInteger(value) && value >= 0 && value <= maximum;
|
|
15
|
+
}
|
|
16
|
+
function nodeArray(value) {
|
|
17
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > MAX_NODES || Reflect.ownKeys(value).length !== value.length + 1) throw new TypeError('Malformed or oversized public data node array');
|
|
18
|
+
for(let index = 0; index < value.length; index++){
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
20
|
+
if (!descriptor || !('value' in descriptor)) throw new TypeError('Public data node arrays must be dense values');
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function pairs(value) {
|
|
25
|
+
const entry = record(value);
|
|
26
|
+
exactFields(entry, [
|
|
27
|
+
'k',
|
|
28
|
+
'v'
|
|
29
|
+
]);
|
|
30
|
+
const keys = nodeArray(entry.k);
|
|
31
|
+
const values = nodeArray(entry.v);
|
|
32
|
+
if (keys.length !== values.length) throw new TypeError('Public data keys and values have different lengths');
|
|
33
|
+
return {
|
|
34
|
+
k: keys,
|
|
35
|
+
v: values
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function validateSerializedData(value) {
|
|
39
|
+
const tree = record(value);
|
|
40
|
+
exactFields(tree, [
|
|
41
|
+
't',
|
|
42
|
+
'f',
|
|
43
|
+
'm'
|
|
44
|
+
]);
|
|
45
|
+
if (!integer(tree.f, 127)) throw new TypeError('Malformed public data feature flags');
|
|
46
|
+
const metadata = nodeArray(tree.m);
|
|
47
|
+
if (metadata.some((id)=>!integer(id, MAX_NODES)) || new Set(metadata).size !== metadata.length) throw new TypeError('Malformed public data reference metadata');
|
|
48
|
+
const declarations = new Map();
|
|
49
|
+
let count = 0;
|
|
50
|
+
const declare = (id, tag)=>{
|
|
51
|
+
if (!integer(id, MAX_NODES)) throw new TypeError('Malformed public data reference id');
|
|
52
|
+
if (declarations.has(id)) throw new TypeError('Duplicate public data reference id');
|
|
53
|
+
declarations.set(id, tag);
|
|
54
|
+
};
|
|
55
|
+
const visit = (input, depth, mapSentinel = false)=>{
|
|
56
|
+
if (++count > MAX_NODES || depth > MAX_DEPTH) throw new TypeError('Public data tree exceeds its size or depth limit');
|
|
57
|
+
const node = record(input);
|
|
58
|
+
const tagDescriptor = Object.getOwnPropertyDescriptor(node, 't');
|
|
59
|
+
if (!tagDescriptor || !('value' in tagDescriptor) || !integer(tagDescriptor.value, 26)) throw new TypeError('Malformed public data node tag');
|
|
60
|
+
const tag = tagDescriptor.value;
|
|
61
|
+
if (mapSentinel && 4 !== tag && 26 !== tag) throw new TypeError('Malformed public data Map sentinel');
|
|
62
|
+
switch(tag){
|
|
63
|
+
case 0:
|
|
64
|
+
exactFields(node, [
|
|
65
|
+
't',
|
|
66
|
+
's'
|
|
67
|
+
]);
|
|
68
|
+
if ('number' != typeof node.s || !Number.isFinite(node.s)) throw new TypeError('Malformed public data number');
|
|
69
|
+
return;
|
|
70
|
+
case 1:
|
|
71
|
+
exactFields(node, [
|
|
72
|
+
't',
|
|
73
|
+
's'
|
|
74
|
+
]);
|
|
75
|
+
if ('string' != typeof node.s) throw new TypeError('Malformed public data string');
|
|
76
|
+
return;
|
|
77
|
+
case 2:
|
|
78
|
+
exactFields(node, [
|
|
79
|
+
't',
|
|
80
|
+
's'
|
|
81
|
+
]);
|
|
82
|
+
if (!integer(node.s, 7)) throw new TypeError('Malformed public data constant');
|
|
83
|
+
return;
|
|
84
|
+
case 3:
|
|
85
|
+
exactFields(node, [
|
|
86
|
+
't',
|
|
87
|
+
's'
|
|
88
|
+
]);
|
|
89
|
+
if ('string' != typeof node.s || node.s.length > 10000 || !/^-?(?:0|[1-9]\d*)$/.test(node.s)) throw new TypeError('Malformed public data bigint');
|
|
90
|
+
return;
|
|
91
|
+
case 4:
|
|
92
|
+
exactFields(node, [
|
|
93
|
+
't',
|
|
94
|
+
'i'
|
|
95
|
+
]);
|
|
96
|
+
if (!integer(node.i, MAX_NODES) || !declarations.has(node.i)) throw new TypeError('Missing or forward public data reference');
|
|
97
|
+
if (26 === declarations.get(node.i) !== mapSentinel) throw new TypeError('Public data references a Map sentinel as a value');
|
|
98
|
+
return;
|
|
99
|
+
case 5:
|
|
100
|
+
exactFields(node, [
|
|
101
|
+
't',
|
|
102
|
+
'i',
|
|
103
|
+
's'
|
|
104
|
+
]);
|
|
105
|
+
if ('string' != typeof node.s) throw new TypeError('Malformed public data date');
|
|
106
|
+
if ('' !== node.s) {
|
|
107
|
+
const timestamp = new Date(node.s);
|
|
108
|
+
if (!Number.isFinite(timestamp.getTime()) || timestamp.toISOString() !== node.s) throw new TypeError('Malformed public data date');
|
|
109
|
+
}
|
|
110
|
+
declare(node.i, tag);
|
|
111
|
+
return;
|
|
112
|
+
case 6:
|
|
113
|
+
exactFields(node, [
|
|
114
|
+
't',
|
|
115
|
+
'i',
|
|
116
|
+
'c',
|
|
117
|
+
'm'
|
|
118
|
+
]);
|
|
119
|
+
if ('string' != typeof node.c || 'string' != typeof node.m || !/^[dgimsuvy]*$/.test(node.m) || new Set(node.m).size !== node.m.length || node.m.includes('u') && node.m.includes('v')) throw new TypeError('Malformed public data regular expression');
|
|
120
|
+
declare(node.i, tag);
|
|
121
|
+
return;
|
|
122
|
+
case 7:
|
|
123
|
+
exactFields(node, [
|
|
124
|
+
't',
|
|
125
|
+
'i',
|
|
126
|
+
'a'
|
|
127
|
+
]);
|
|
128
|
+
declare(node.i, tag);
|
|
129
|
+
for (const item of nodeArray(node.a))visit(item, depth + 1);
|
|
130
|
+
return;
|
|
131
|
+
case 8:
|
|
132
|
+
{
|
|
133
|
+
exactFields(node, [
|
|
134
|
+
't',
|
|
135
|
+
'i',
|
|
136
|
+
'e',
|
|
137
|
+
'f'
|
|
138
|
+
]);
|
|
139
|
+
declare(node.i, tag);
|
|
140
|
+
const entries = pairs(node.e);
|
|
141
|
+
for(let index = 0; index < entries.k.length; index++){
|
|
142
|
+
visit(entries.k[index], depth + 1);
|
|
143
|
+
visit(entries.v[index], depth + 1);
|
|
144
|
+
}
|
|
145
|
+
visit(node.f, depth + 1, true);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case 9:
|
|
149
|
+
exactFields(node, [
|
|
150
|
+
't',
|
|
151
|
+
'i',
|
|
152
|
+
'a',
|
|
153
|
+
'o'
|
|
154
|
+
]);
|
|
155
|
+
if (!integer(node.o, 3)) throw new TypeError('Malformed public data object flags');
|
|
156
|
+
declare(node.i, tag);
|
|
157
|
+
for (const item of nodeArray(node.a))if (0 === item) {
|
|
158
|
+
if (++count > MAX_NODES) throw new TypeError('Public data tree exceeds its size limit');
|
|
159
|
+
} else visit(item, depth + 1);
|
|
160
|
+
return;
|
|
161
|
+
case 10:
|
|
162
|
+
case 11:
|
|
163
|
+
{
|
|
164
|
+
exactFields(node, [
|
|
165
|
+
't',
|
|
166
|
+
'i',
|
|
167
|
+
'p',
|
|
168
|
+
'o'
|
|
169
|
+
]);
|
|
170
|
+
if (!integer(node.o, 3)) throw new TypeError('Malformed public data object flags');
|
|
171
|
+
declare(node.i, tag);
|
|
172
|
+
const properties = pairs(node.p);
|
|
173
|
+
if (properties.k.some((key)=>'string' != typeof key) || new Set(properties.k).size !== properties.k.length) throw new TypeError('Malformed or duplicate public data object keys');
|
|
174
|
+
for (const item of properties.v)visit(item, depth + 1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
case 26:
|
|
178
|
+
exactFields(node, [
|
|
179
|
+
't',
|
|
180
|
+
'i',
|
|
181
|
+
's'
|
|
182
|
+
]);
|
|
183
|
+
if (!mapSentinel || 0 !== node.s) throw new TypeError('Unsupported public data special reference');
|
|
184
|
+
declare(node.i, tag);
|
|
185
|
+
return;
|
|
186
|
+
default:
|
|
187
|
+
throw new TypeError('Unsupported public data node');
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
visit(tree.t, 0);
|
|
191
|
+
if (metadata.some((id)=>!declarations.has(id))) throw new TypeError('Missing public data metadata reference');
|
|
192
|
+
}
|
|
193
|
+
export { validateSerializedData };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const DATA_PROTOCOL_VERSION = 1;
|
|
2
|
+
const DATA_CONTENT_TYPE = 'application/vnd.ultramodern.data+json';
|
|
3
|
+
const DATA_STREAM_CONTENT_TYPE = 'application/vnd.ultramodern.data-stream+json';
|
|
4
|
+
const LOADER_ID_PARAM = '__loader';
|
|
5
|
+
const DIRECT_PARAM = '__ssrDirect';
|
|
6
|
+
export { DATA_CONTENT_TYPE, DATA_PROTOCOL_VERSION, DATA_STREAM_CONTENT_TYPE, DIRECT_PARAM, LOADER_ID_PARAM };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { escapeInlineDataJSON } from "./data/codec.mjs";
|
|
2
|
+
import { readRendererIdentity } from "./identity.mjs";
|
|
3
|
+
function escapeAttribute(value) {
|
|
4
|
+
return value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
5
|
+
}
|
|
6
|
+
function validateAsset(asset) {
|
|
7
|
+
if (![
|
|
8
|
+
'stylesheet',
|
|
9
|
+
'modulepreload',
|
|
10
|
+
"script"
|
|
11
|
+
].includes(asset.kind)) throw new TypeError('A document asset has an unknown kind.');
|
|
12
|
+
if ('string' != typeof asset.href || 0 === asset.href.length || asset.href.trim() !== asset.href || /[\u0000-\u0020\u007f]/u.test(asset.href)) throw new TypeError('A document asset requires a nonempty URL without whitespace or control characters.');
|
|
13
|
+
const url = new URL(asset.href, 'https://ultramodern.invalid/');
|
|
14
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) throw new TypeError('A document asset must use an HTTP URL or relative URL.');
|
|
15
|
+
if (void 0 !== asset.crossOrigin && 'anonymous' !== asset.crossOrigin && 'use-credentials' !== asset.crossOrigin) throw new TypeError('A document asset has an unknown crossOrigin value.');
|
|
16
|
+
if (void 0 !== asset.scriptType && ("script" !== asset.kind || ![
|
|
17
|
+
'module',
|
|
18
|
+
'classic'
|
|
19
|
+
].includes(asset.scriptType))) throw new TypeError("Only a script asset can declare module or classic scriptType.");
|
|
20
|
+
}
|
|
21
|
+
function collectDocumentAssets(assets) {
|
|
22
|
+
const collected = new Map();
|
|
23
|
+
for (const asset of assets){
|
|
24
|
+
validateAsset(asset);
|
|
25
|
+
const key = JSON.stringify([
|
|
26
|
+
asset.kind,
|
|
27
|
+
asset.href
|
|
28
|
+
]);
|
|
29
|
+
const existing = collected.get(key);
|
|
30
|
+
if (existing) {
|
|
31
|
+
if (existing.integrity !== asset.integrity || existing.crossOrigin !== asset.crossOrigin || (existing.scriptType ?? 'module') !== (asset.scriptType ?? 'module')) throw new Error(`Conflicting document asset metadata for ${asset.href}.`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
collected.set(key, Object.freeze({
|
|
35
|
+
...asset
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze([
|
|
39
|
+
...collected.values()
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
function nonceAttribute(nonce) {
|
|
43
|
+
if (void 0 === nonce) return '';
|
|
44
|
+
if ('string' != typeof nonce || 0 === nonce.length) throw new TypeError('A document nonce must be a nonempty string.');
|
|
45
|
+
return ` nonce="${escapeAttribute(nonce)}"`;
|
|
46
|
+
}
|
|
47
|
+
function serializeDocumentAsset(asset, nonce, options = {}) {
|
|
48
|
+
validateAsset(asset);
|
|
49
|
+
if (void 0 !== options.async && ("script" !== asset.kind || 'boolean' != typeof options.async)) throw new TypeError("Async scheduling is only available for a script asset.");
|
|
50
|
+
if (void 0 !== options.defer && ("script" !== asset.kind || 'boolean' != typeof options.defer)) throw new TypeError("Deferred scheduling is only available for a script asset.");
|
|
51
|
+
if (true === options.async && true === options.defer) throw new TypeError("A script asset cannot be both async and deferred.");
|
|
52
|
+
const url = escapeAttribute(asset.href);
|
|
53
|
+
const integrity = void 0 === asset.integrity ? '' : ` integrity="${escapeAttribute(asset.integrity)}"`;
|
|
54
|
+
const crossOrigin = void 0 === asset.crossOrigin ? '' : ` crossorigin="${asset.crossOrigin}"`;
|
|
55
|
+
const attributes = `${integrity}${crossOrigin}${nonceAttribute(nonce)}`;
|
|
56
|
+
if ("script" === asset.kind) {
|
|
57
|
+
const scriptType = asset.scriptType ?? 'module';
|
|
58
|
+
const type = 'module' === scriptType ? ' type="module"' : '';
|
|
59
|
+
const scheduling = options.async ? ' async' : true === options.defer || 'classic' === scriptType && false !== options.defer ? ' defer' : '';
|
|
60
|
+
return `<script${type}${scheduling} src="${url}"${attributes}></script>`;
|
|
61
|
+
}
|
|
62
|
+
return `<link rel="${asset.kind}" href="${url}"${attributes}>`;
|
|
63
|
+
}
|
|
64
|
+
function serializeInlineData(input) {
|
|
65
|
+
if ('string' != typeof input.id || 0 === input.id.length) throw new TypeError('An inline data element requires a nonempty id.');
|
|
66
|
+
const payload = escapeInlineDataJSON(input.payload);
|
|
67
|
+
return `<script type="application/json" id="${escapeAttribute(input.id)}"${nonceAttribute(input.nonce)}>${payload}</script>`;
|
|
68
|
+
}
|
|
69
|
+
const RENDERER_BOOTSTRAP_ID = '__ULTRAMODERN_RENDERER__';
|
|
70
|
+
function documentAttribute(value, name) {
|
|
71
|
+
if ('string' != typeof value || !value.trim()) throw new TypeError(`A native document requires a nonempty ${name}.`);
|
|
72
|
+
return escapeAttribute(value);
|
|
73
|
+
}
|
|
74
|
+
function prepareDocument(bootstrap, options = {}) {
|
|
75
|
+
if ('string' != typeof bootstrap.documentId || !bootstrap.documentId.trim()) throw new TypeError('A native document requires a nonempty document id.');
|
|
76
|
+
if ('boolean' != typeof bootstrap.hydrating) throw new TypeError('A native document must identify whether its root requires hydration.');
|
|
77
|
+
readRendererIdentity(bootstrap.identity, bootstrap.identity);
|
|
78
|
+
const rootId = options.rootId ?? 'root';
|
|
79
|
+
const root = documentAttribute(rootId, 'rootId');
|
|
80
|
+
const lang = documentAttribute(options.lang ?? 'en', 'lang');
|
|
81
|
+
if (rootId === RENDERER_BOOTSTRAP_ID) throw new TypeError('A document root cannot reuse the bootstrap id.');
|
|
82
|
+
const { script, style } = options.nonce ?? {};
|
|
83
|
+
const inlineIds = new Set();
|
|
84
|
+
const inlineData = (options.inlineData ?? []).map((item)=>{
|
|
85
|
+
if (item.id === RENDERER_BOOTSTRAP_ID || item.id === rootId) throw new TypeError('Document inline data cannot reuse the root or bootstrap id.');
|
|
86
|
+
if (inlineIds.has(item.id)) throw new TypeError(`Document inline data id ${item.id} is repeated.`);
|
|
87
|
+
inlineIds.add(item.id);
|
|
88
|
+
return serializeInlineData({
|
|
89
|
+
...item,
|
|
90
|
+
nonce: script
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const assets = collectDocumentAssets(options.assets ?? []);
|
|
94
|
+
return {
|
|
95
|
+
rootId: root,
|
|
96
|
+
lang,
|
|
97
|
+
headAssets: assets.filter((asset)=>"script" !== asset.kind).map((asset)=>serializeDocumentAsset(asset, 'stylesheet' === asset.kind ? style : script)).join(''),
|
|
98
|
+
bootstrap: serializeInlineData({
|
|
99
|
+
id: RENDERER_BOOTSTRAP_ID,
|
|
100
|
+
payload: JSON.stringify(bootstrap),
|
|
101
|
+
nonce: script
|
|
102
|
+
}) + inlineData.join(''),
|
|
103
|
+
entryScripts: assets.filter((asset)=>"script" === asset.kind).map((asset)=>serializeDocumentAsset(asset, script, bootstrap.hydrating ? 'classic' === asset.scriptType ? {
|
|
104
|
+
defer: false
|
|
105
|
+
} : {
|
|
106
|
+
async: true
|
|
107
|
+
} : {})).join('')
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function readDocumentBootstrap(document, expectedIdentity, extraFields = []) {
|
|
111
|
+
const elements = document.querySelectorAll(`[id="${RENDERER_BOOTSTRAP_ID}"]`);
|
|
112
|
+
if (1 !== elements.length) throw new Error('A native document requires exactly one renderer bootstrap.');
|
|
113
|
+
const element = elements.item(0);
|
|
114
|
+
if (!element || 'SCRIPT' !== element.tagName || 'application/json' !== element.getAttribute('type')) throw new Error('The native document is missing its renderer bootstrap.');
|
|
115
|
+
const payload = JSON.parse(element.textContent ?? '');
|
|
116
|
+
if (!payload || 'object' != typeof payload || Array.isArray(payload) || Object.keys(payload).some((key)=>![
|
|
117
|
+
'identity',
|
|
118
|
+
'documentId',
|
|
119
|
+
'hydrating',
|
|
120
|
+
...extraFields
|
|
121
|
+
].includes(key))) throw new Error('The native renderer bootstrap is malformed.');
|
|
122
|
+
const fields = payload;
|
|
123
|
+
const identity = readRendererIdentity(fields.identity, expectedIdentity);
|
|
124
|
+
if ('string' != typeof fields.documentId || !fields.documentId.trim()) throw new Error('The native renderer bootstrap requires a document id.');
|
|
125
|
+
if ('boolean' != typeof fields.hydrating) throw new Error('The native renderer bootstrap must say whether its root hydrates.');
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
...fields,
|
|
128
|
+
identity
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export { RENDERER_BOOTSTRAP_ID, collectDocumentAssets, prepareDocument, readDocumentBootstrap, serializeDocumentAsset, serializeInlineData };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createDataClient, invokeRouteData } from "./data/index.mjs";
|
|
2
|
+
import { RENDERER_BOOTSTRAP_ID } from "./document.mjs";
|
|
3
|
+
import { nativeI18nView } from "./localization.mjs";
|
|
4
|
+
import { isRoutedApplication } from "./router.mjs";
|
|
5
|
+
import { untilAborted } from "./abort.mjs";
|
|
6
|
+
function isNativeEntryHot(value) {
|
|
7
|
+
return 'object' == typeof value && null !== value && 'dispose' in value && 'function' == typeof value.dispose;
|
|
8
|
+
}
|
|
9
|
+
function clientRouteLoader(application, identity) {
|
|
10
|
+
const serverDataRoutes = new Set(application.serverDataRoutes);
|
|
11
|
+
return async (route, input)=>{
|
|
12
|
+
const loader = application.dataModules[route.id]?.loader;
|
|
13
|
+
if (loader) return invokeRouteData(loader, input, {
|
|
14
|
+
production: 'production' === process.env.NODE_ENV
|
|
15
|
+
});
|
|
16
|
+
if (!serverDataRoutes.has(route.id)) return {
|
|
17
|
+
kind: 'success',
|
|
18
|
+
value: void 0,
|
|
19
|
+
status: 200
|
|
20
|
+
};
|
|
21
|
+
return createDataClient(route.id, identity).loader({
|
|
22
|
+
request: input.request
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function startNativeClientEntry(options, adapter) {
|
|
27
|
+
const identity = Object.freeze({
|
|
28
|
+
...options.identity
|
|
29
|
+
});
|
|
30
|
+
const root = document.getElementById(options.rootId ?? 'root');
|
|
31
|
+
if (!root) throw new Error('The native application mount element is missing');
|
|
32
|
+
const bootstrap = document.getElementById(RENDERER_BOOTSTRAP_ID) ? adapter.readBootstrap(document, identity) : void 0;
|
|
33
|
+
const hydrating = bootstrap?.hydrating ?? false;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const { signal } = controller;
|
|
36
|
+
let disposed = false;
|
|
37
|
+
let dispose;
|
|
38
|
+
if (isNativeEntryHot(options.hot)) options.hot.dispose(()=>{
|
|
39
|
+
disposed = true;
|
|
40
|
+
controller.abort(new DOMException('The application entry was disposed', 'AbortError'));
|
|
41
|
+
dispose?.();
|
|
42
|
+
});
|
|
43
|
+
const load = async ()=>{
|
|
44
|
+
const application = await untilAborted(options.load(), signal);
|
|
45
|
+
const { i18n } = options;
|
|
46
|
+
let instance;
|
|
47
|
+
if (i18n) {
|
|
48
|
+
const handoff = i18n.clientHandoff();
|
|
49
|
+
instance = await untilAborted(i18n.create(handoff.language, handoff.resources), signal);
|
|
50
|
+
const stopPersist = i18n.persist?.(instance);
|
|
51
|
+
if (stopPersist) signal.addEventListener('abort', stopPersist, {
|
|
52
|
+
once: true
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const current = instance;
|
|
56
|
+
const localization = i18n && current ? {
|
|
57
|
+
i18n: nativeI18nView(i18n, current)
|
|
58
|
+
} : {};
|
|
59
|
+
if (!isRoutedApplication(application)) return {
|
|
60
|
+
kind: 'component',
|
|
61
|
+
component: application.default,
|
|
62
|
+
...localization
|
|
63
|
+
};
|
|
64
|
+
const router = adapter.createRouter(application, {
|
|
65
|
+
identity,
|
|
66
|
+
loadRoute: clientRouteLoader(application, identity),
|
|
67
|
+
signal,
|
|
68
|
+
...i18n && current ? {
|
|
69
|
+
rewrite: i18n.rewrite(()=>current.language)
|
|
70
|
+
} : {}
|
|
71
|
+
});
|
|
72
|
+
const stopSync = i18n && current ? i18n.syncWithRouter(router, current) : void 0;
|
|
73
|
+
if (stopSync) signal.addEventListener('abort', stopSync, {
|
|
74
|
+
once: true
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
await untilAborted(adapter.prepareRouter(router, hydrating, signal), signal);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
stopSync?.();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: 'router',
|
|
84
|
+
router,
|
|
85
|
+
...localization
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
adapter.start({
|
|
89
|
+
root,
|
|
90
|
+
identity,
|
|
91
|
+
bootstrap,
|
|
92
|
+
signal,
|
|
93
|
+
load
|
|
94
|
+
}).then((release)=>{
|
|
95
|
+
dispose = release;
|
|
96
|
+
if (disposed) release();
|
|
97
|
+
}).catch((error)=>{
|
|
98
|
+
if (!signal.aborted) controller.abort(error);
|
|
99
|
+
if (!disposed) queueMicrotask(()=>{
|
|
100
|
+
throw error;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export { startNativeClientEntry };
|