@bleedingdev/modern-js-renderer-core 3.9.3-ultramodern.1 → 3.9.3-ultramodern.11

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.
Files changed (53) hide show
  1. package/dist/cjs/adapter.js +1 -1
  2. package/dist/cjs/data/codec.js +1 -1
  3. package/dist/cjs/entry-client.js +5 -0
  4. package/dist/cjs/federation.js +14 -5
  5. package/dist/cjs/server/worker.js +2 -1
  6. package/dist/cjs/worker-federation/contract.js +298 -0
  7. package/dist/cjs/worker-federation/handler.js +197 -0
  8. package/dist/cjs/worker-federation/index.js +63 -0
  9. package/dist/cjs/worker-federation/lease.js +167 -0
  10. package/dist/cjs/worker-federation/transport.js +272 -0
  11. package/dist/esm/adapter.mjs +1 -1
  12. package/dist/esm/data/codec.mjs +1 -1
  13. package/dist/esm/entry-client.mjs +5 -0
  14. package/dist/esm/federation.mjs +14 -5
  15. package/dist/esm/server/worker.mjs +2 -1
  16. package/dist/esm/worker-federation/contract.mjs +227 -0
  17. package/dist/esm/worker-federation/handler.mjs +159 -0
  18. package/dist/esm/worker-federation/index.mjs +3 -0
  19. package/dist/esm/worker-federation/lease.mjs +107 -0
  20. package/dist/esm/worker-federation/transport.mjs +234 -0
  21. package/dist/esm-node/adapter.mjs +1 -1
  22. package/dist/esm-node/data/codec.mjs +1 -1
  23. package/dist/esm-node/entry-client.mjs +5 -0
  24. package/dist/esm-node/federation.mjs +14 -5
  25. package/dist/esm-node/server/worker.mjs +2 -1
  26. package/dist/esm-node/worker-federation/contract.mjs +228 -0
  27. package/dist/esm-node/worker-federation/handler.mjs +160 -0
  28. package/dist/esm-node/worker-federation/index.mjs +4 -0
  29. package/dist/esm-node/worker-federation/lease.mjs +108 -0
  30. package/dist/esm-node/worker-federation/transport.mjs +235 -0
  31. package/dist/types/adapter.d.ts +1 -1
  32. package/dist/types/data/codec.d.ts +1 -1
  33. package/dist/types/entry-client.d.ts +2 -0
  34. package/dist/types/federation.d.ts +15 -2
  35. package/dist/types/server/types.d.ts +3 -0
  36. package/dist/types/server/worker.d.ts +19 -0
  37. package/dist/types/worker-federation/contract.d.ts +70 -0
  38. package/dist/types/worker-federation/handler.d.ts +21 -0
  39. package/dist/types/worker-federation/index.d.ts +6 -0
  40. package/dist/types/worker-federation/lease.d.ts +11 -0
  41. package/dist/types/worker-federation/transport.d.ts +47 -0
  42. package/package.json +17 -7
  43. package/src/adapter.ts +4 -2
  44. package/src/data/codec.ts +1 -1
  45. package/src/entry-client.ts +5 -0
  46. package/src/federation.ts +36 -8
  47. package/src/server/types.ts +3 -0
  48. package/src/server/worker.ts +23 -0
  49. package/src/worker-federation/contract.ts +449 -0
  50. package/src/worker-federation/handler.ts +274 -0
  51. package/src/worker-federation/index.ts +23 -0
  52. package/src/worker-federation/lease.ts +143 -0
  53. package/src/worker-federation/transport.ts +430 -0
@@ -0,0 +1,449 @@
1
+ import {
2
+ identityCacheKey,
3
+ type RendererIdentity,
4
+ readRendererIdentity,
5
+ } from '../identity';
6
+
7
+ export const WORKER_FEDERATION_METADATA_KEY = 'ultramodernWorkerFederation';
8
+ export const WORKER_FEDERATION_SCHEMA = 'ultramodern.worker-federation';
9
+ export const WORKER_FEDERATION_VERSION = 1;
10
+
11
+ export interface WorkerFederationExpose {
12
+ readonly entryName: string;
13
+ readonly boundaryId: string;
14
+ readonly paths: Readonly<Record<string, string>>;
15
+ readonly native: {
16
+ readonly publisherBuildId: string;
17
+ readonly runtimeABI: 1;
18
+ };
19
+ }
20
+
21
+ export interface WorkerFederationPublication {
22
+ readonly schema: typeof WORKER_FEDERATION_SCHEMA;
23
+ readonly schemaVersion: typeof WORKER_FEDERATION_VERSION;
24
+ readonly remote: string;
25
+ readonly exposes: Readonly<Record<string, WorkerFederationExpose>>;
26
+ }
27
+
28
+ /** The canonical renderer-contract reader and compatibility check are required. */
29
+ export type AdmitWorkerRendererContract = (value: unknown) => {
30
+ readonly identities: Readonly<Record<string, RendererIdentity>>;
31
+ };
32
+
33
+ export interface WorkerFederationAuthority {
34
+ readonly origin: string;
35
+ readonly renderer: ReturnType<AdmitWorkerRendererContract>;
36
+ readonly worker: WorkerFederationPublication;
37
+ }
38
+
39
+ export interface AdmittedWorkerFederationExpose {
40
+ readonly authority: WorkerFederationAuthority;
41
+ readonly expose: string;
42
+ readonly locale: string;
43
+ readonly coordinate: WorkerFederationExpose;
44
+ readonly path: string;
45
+ readonly identity: Readonly<RendererIdentity>;
46
+ readonly endpoint: string;
47
+ /** Browser styles captured from this exact admitted manifest/expose. */
48
+ readonly styles: readonly string[];
49
+ }
50
+
51
+ export interface WorkerFederationEnvelope {
52
+ readonly schema:
53
+ | 'ultramodern.worker-federation-request'
54
+ | 'ultramodern.worker-federation-response';
55
+ readonly schemaVersion: 1;
56
+ readonly authority: WorkerFederationAuthority;
57
+ readonly expose: string;
58
+ readonly locale: string;
59
+ /** Encoded and decoded by the native renderer; the framework never reads it. */
60
+ readonly payload: unknown;
61
+ }
62
+
63
+ export function workerFederationRecord(
64
+ value: unknown,
65
+ label: string,
66
+ ): Record<string, unknown> {
67
+ if (
68
+ !value ||
69
+ typeof value !== 'object' ||
70
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))
71
+ )
72
+ throw new Error(`Worker federation ${label} must be a plain record.`);
73
+ return value as Record<string, unknown>;
74
+ }
75
+
76
+ function exactFields(
77
+ value: Record<string, unknown>,
78
+ fields: readonly string[],
79
+ label: string,
80
+ ) {
81
+ if (
82
+ Object.keys(value).length !== fields.length ||
83
+ Object.keys(value).some(key => !fields.includes(key))
84
+ )
85
+ throw new Error(`Worker federation ${label} has unsupported fields.`);
86
+ }
87
+
88
+ function requiredString(value: unknown, label: string): string {
89
+ if (
90
+ typeof value !== 'string' ||
91
+ value.length === 0 ||
92
+ value.trim() !== value ||
93
+ /[\u0000-\u001f\u007f]/u.test(value)
94
+ )
95
+ throw new Error(`Worker federation requires ${label}.`);
96
+ return value;
97
+ }
98
+
99
+ export function readWorkerFederationPath(value: unknown): string {
100
+ const path = requiredString(value, 'an endpoint path');
101
+ let decoded: string;
102
+ try {
103
+ decoded = decodeURIComponent(path);
104
+ } catch {
105
+ throw new Error('Worker federation endpoint path has invalid encoding.');
106
+ }
107
+ if (
108
+ !path.startsWith('/') ||
109
+ path.startsWith('//') ||
110
+ /[\\?#\s\u0000-\u001f\u007f]/u.test(decoded) ||
111
+ /%[0-9a-f]{2}/iu.test(decoded) ||
112
+ decoded.split('/').some(segment => segment === '.' || segment === '..') ||
113
+ new URL(path, 'https://worker.invalid').pathname !== path
114
+ )
115
+ throw new Error('Worker federation endpoint must be an exact root path.');
116
+ return path;
117
+ }
118
+
119
+ export function readWorkerFederationManifestUrl(value: string): URL {
120
+ const url = new URL(value);
121
+ if (
122
+ !['http:', 'https:'].includes(url.protocol) ||
123
+ url.username ||
124
+ url.password ||
125
+ url.hash
126
+ )
127
+ throw new Error(
128
+ 'Worker federation manifest requires an HTTP(S) publication URL.',
129
+ );
130
+ return url;
131
+ }
132
+
133
+ export function readWorkerFederationPublication(
134
+ value: unknown,
135
+ identities: Readonly<Record<string, RendererIdentity>>,
136
+ ): WorkerFederationPublication {
137
+ const record = workerFederationRecord(value, 'publication');
138
+ exactFields(
139
+ record,
140
+ ['schema', 'schemaVersion', 'remote', 'exposes'],
141
+ 'publication',
142
+ );
143
+ if (
144
+ record.schema !== WORKER_FEDERATION_SCHEMA ||
145
+ record.schemaVersion !== WORKER_FEDERATION_VERSION
146
+ )
147
+ throw new Error('Unsupported Worker federation publication protocol.');
148
+ const remote = requiredString(record.remote, 'a remote');
149
+ const entries = workerFederationRecord(record.exposes, 'exposes');
150
+ if (Object.keys(entries).length === 0)
151
+ throw new Error('Worker federation publication has no exposes.');
152
+ const paths = new Set<string>();
153
+ const exposes = Object.fromEntries(
154
+ Object.entries(entries).map(([expose, raw]) => {
155
+ if (
156
+ !/^\.\/[A-Za-z0-9_$@.-]+(?:\/[A-Za-z0-9_$@.-]+)*$/u.test(expose) ||
157
+ expose
158
+ .slice(2)
159
+ .split('/')
160
+ .some(segment => segment === '.' || segment === '..')
161
+ )
162
+ throw new Error(`Unsupported Worker federation expose ${expose}.`);
163
+ const coordinate = workerFederationRecord(raw, 'expose coordinate');
164
+ exactFields(
165
+ coordinate,
166
+ ['entryName', 'boundaryId', 'paths', 'native'],
167
+ 'expose coordinate',
168
+ );
169
+ const entryName = requiredString(coordinate.entryName, 'an entry name');
170
+ if (!Object.hasOwn(identities, entryName))
171
+ throw new Error(
172
+ `Worker federation expose ${expose} has no built entry.`,
173
+ );
174
+ const identity = identities[entryName];
175
+ identityCacheKey(identity);
176
+ if (identity.entryName !== entryName)
177
+ throw new Error(
178
+ 'Worker federation entry identity conflicts with its key.',
179
+ );
180
+ const rawPaths = workerFederationRecord(coordinate.paths, 'locale paths');
181
+ const native = workerFederationRecord(
182
+ coordinate.native,
183
+ 'native authority',
184
+ );
185
+ exactFields(
186
+ native,
187
+ ['publisherBuildId', 'runtimeABI'],
188
+ 'native authority',
189
+ );
190
+ if (native.runtimeABI !== 1)
191
+ throw new Error('Unsupported native Worker federation runtime ABI.');
192
+ if (Object.keys(rawPaths).length === 0)
193
+ throw new Error('Worker federation expose has no locale endpoints.');
194
+ const locales = Object.keys(rawPaths);
195
+ if (
196
+ new Set(locales.map(locale => locale.toLowerCase())).size !==
197
+ locales.length ||
198
+ (locales.includes('default') && locales.length !== 1)
199
+ )
200
+ throw new Error(
201
+ 'Worker federation requires unique locales or only default.',
202
+ );
203
+ const localePaths = Object.fromEntries(
204
+ Object.entries(rawPaths).map(([locale, rawPath]) => {
205
+ if (!/^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/u.test(locale))
206
+ throw new Error(
207
+ 'Worker federation endpoint has an invalid locale.',
208
+ );
209
+ const path = readWorkerFederationPath(rawPath);
210
+ if (paths.has(path))
211
+ throw new Error(
212
+ 'Worker federation exposes have conflicting endpoint paths.',
213
+ );
214
+ paths.add(path);
215
+ return [locale, path];
216
+ }),
217
+ );
218
+ return [
219
+ expose,
220
+ Object.freeze({
221
+ entryName,
222
+ boundaryId: requiredString(coordinate.boundaryId, 'a boundary ID'),
223
+ paths: Object.freeze(localePaths),
224
+ native: Object.freeze({
225
+ publisherBuildId: requiredString(
226
+ native.publisherBuildId,
227
+ 'a native publisher build ID',
228
+ ),
229
+ runtimeABI: 1 as const,
230
+ }),
231
+ }),
232
+ ];
233
+ }),
234
+ );
235
+ return Object.freeze({
236
+ schema: WORKER_FEDERATION_SCHEMA,
237
+ schemaVersion: WORKER_FEDERATION_VERSION,
238
+ remote,
239
+ exposes: Object.freeze(exposes),
240
+ });
241
+ }
242
+
243
+ /** Stable JSON also prevents non-wire values from silently changing on POST. */
244
+ export function workerFederationWireJson(value: unknown): string {
245
+ const encode = (input: unknown): unknown => {
246
+ if (
247
+ input === null ||
248
+ typeof input === 'string' ||
249
+ typeof input === 'boolean'
250
+ )
251
+ return input;
252
+ if (typeof input === 'number' && Number.isFinite(input)) return input;
253
+ if (Array.isArray(input)) return input.map(encode);
254
+ const record = workerFederationRecord(input, 'wire value');
255
+ return Object.fromEntries(
256
+ Object.keys(record)
257
+ .toSorted()
258
+ .map(key => [key, encode(record[key])]),
259
+ );
260
+ };
261
+ return JSON.stringify(encode(value));
262
+ }
263
+
264
+ function freezeWireValue<T>(value: T): T {
265
+ const copy = JSON.parse(workerFederationWireJson(value)) as T;
266
+ const freeze = (input: unknown) => {
267
+ if (input && typeof input === 'object') {
268
+ Object.values(input).forEach(freeze);
269
+ Object.freeze(input);
270
+ }
271
+ };
272
+ freeze(copy);
273
+ return copy;
274
+ }
275
+
276
+ /** Preserve the completed MF browser publicPath/asset URL semantics. */
277
+ export function readWorkerFederationStyles(options: {
278
+ manifest: unknown;
279
+ manifestUrl: string;
280
+ expose: string;
281
+ }): readonly string[] {
282
+ const manifest = workerFederationRecord(options.manifest, 'manifest');
283
+ const metadata = workerFederationRecord(
284
+ manifest.metaData,
285
+ 'manifest metadata',
286
+ );
287
+ const manifestUrl = readWorkerFederationManifestUrl(options.manifestUrl);
288
+ if (!Array.isArray(manifest.exposes))
289
+ throw new Error('Worker federation manifest exposes must be an array.');
290
+ const selected = manifest.exposes
291
+ .map(value => workerFederationRecord(value, 'manifest expose'))
292
+ .filter(value => value.path === options.expose);
293
+ if (selected.length !== 1)
294
+ throw new Error(
295
+ 'Worker federation styles require one exact manifest expose.',
296
+ );
297
+ if (selected[0].assets === undefined) return Object.freeze([]);
298
+ const assets = workerFederationRecord(selected[0].assets, 'expose assets');
299
+ if (assets.css === undefined) return Object.freeze([]);
300
+ const css = workerFederationRecord(assets.css, 'expose CSS assets');
301
+ const files = ['sync', 'async'].flatMap(kind => {
302
+ const values = css[kind];
303
+ if (values === undefined) return [];
304
+ if (!Array.isArray(values))
305
+ throw new Error('Worker federation CSS assets must be string arrays.');
306
+ return values.map(value => requiredString(value, 'a CSS asset URL'));
307
+ });
308
+ if (files.length === 0) return Object.freeze([]);
309
+ if (metadata.publicPath === undefined || metadata.publicPath === 'auto')
310
+ throw new Error(
311
+ 'Worker federation CSS requires a completed static publicPath; auto and getPublicPath cannot resolve styles in a Worker.',
312
+ );
313
+ const publicPath = requiredString(
314
+ metadata.publicPath,
315
+ 'a completed public path',
316
+ );
317
+ const explicitScheme = /^[a-z][a-z0-9+.-]*:/iu;
318
+ const isHttpScheme = /^https?:/iu;
319
+ const assertScheme = (value: string) => {
320
+ // Check the authored value before concatenation. Otherwise e.g. data: or
321
+ // javascript: would be silently converted into an ordinary HTTP pathname.
322
+ if (explicitScheme.test(value) && !isHttpScheme.test(value))
323
+ throw new Error(
324
+ 'Worker federation styles require an HTTP(S) publication URL.',
325
+ );
326
+ };
327
+ assertScheme(publicPath);
328
+ const publicUrl = new URL(publicPath, manifestUrl);
329
+ if (
330
+ !['http:', 'https:'].includes(publicUrl.protocol) ||
331
+ publicUrl.username ||
332
+ publicUrl.password ||
333
+ publicUrl.hash
334
+ )
335
+ throw new Error(
336
+ 'Worker federation styles require an HTTP(S) publication URL.',
337
+ );
338
+ const styles = files.map(file => {
339
+ assertScheme(file);
340
+ // Match remoteBrowserAssets: a leading slash on a relative asset belongs
341
+ // under publicPath; absolute and protocol-relative CDN assets retain theirs.
342
+ const resolved = /^(?:https?:)?\/\//iu.test(file)
343
+ ? file
344
+ : `${publicPath.replace(/\/$/u, '')}/${file.replace(/^\//u, '')}`;
345
+ const url = new URL(resolved, manifestUrl);
346
+ if (
347
+ !['http:', 'https:'].includes(url.protocol) ||
348
+ url.username ||
349
+ url.password ||
350
+ url.hash
351
+ )
352
+ throw new Error(
353
+ 'Worker federation styles require an HTTP(S) publication URL.',
354
+ );
355
+ // Resource identity must match the browser, including protocol-relative
356
+ // URLs, authored host casing and dot segments. URL parsing validates only.
357
+ return resolved;
358
+ });
359
+ return Object.freeze([...new Set(styles)]);
360
+ }
361
+
362
+ export function admitWorkerFederationExpose(options: {
363
+ manifest: unknown;
364
+ manifestUrl: string;
365
+ remote: string;
366
+ expose: string;
367
+ locale: string;
368
+ admitRendererContract: AdmitWorkerRendererContract;
369
+ }): AdmittedWorkerFederationExpose {
370
+ const url = readWorkerFederationManifestUrl(options.manifestUrl);
371
+ const manifest = workerFederationRecord(options.manifest, 'manifest');
372
+ const metadata = workerFederationRecord(
373
+ manifest.metaData,
374
+ 'manifest metadata',
375
+ );
376
+ const renderer = freezeWireValue(
377
+ options.admitRendererContract(metadata.ultramodernRenderer),
378
+ );
379
+ const worker = readWorkerFederationPublication(
380
+ metadata[WORKER_FEDERATION_METADATA_KEY],
381
+ renderer.identities,
382
+ );
383
+ if (worker.remote !== options.remote)
384
+ throw new Error('Worker federation publication names another remote.');
385
+ if (!Object.hasOwn(worker.exposes, options.expose))
386
+ throw new Error(
387
+ `Worker federation expose ${options.expose} is not published.`,
388
+ );
389
+ const coordinate = worker.exposes[options.expose];
390
+ if (!Object.hasOwn(coordinate.paths, options.locale))
391
+ throw new Error('Worker federation locale has no published endpoint.');
392
+ const path = coordinate.paths[options.locale];
393
+ const identity = readRendererIdentity(
394
+ renderer.identities[coordinate.entryName],
395
+ renderer.identities[coordinate.entryName],
396
+ );
397
+ return Object.freeze({
398
+ authority: Object.freeze({ origin: url.origin, renderer, worker }),
399
+ expose: options.expose,
400
+ locale: options.locale,
401
+ coordinate,
402
+ path,
403
+ identity,
404
+ endpoint: new URL(path, url.origin).href,
405
+ // Handler coordinates carry no browser assets. Transport captures those
406
+ // separately from its actual completed MF manifest, before renderer hooks.
407
+ styles: Object.freeze([]),
408
+ });
409
+ }
410
+
411
+ export function workerFederationEnvelope(
412
+ kind: 'request' | 'response',
413
+ admitted: AdmittedWorkerFederationExpose,
414
+ payload: unknown,
415
+ ): WorkerFederationEnvelope {
416
+ return {
417
+ schema: `ultramodern.worker-federation-${kind}`,
418
+ schemaVersion: 1,
419
+ authority: admitted.authority,
420
+ expose: admitted.expose,
421
+ locale: admitted.locale,
422
+ payload,
423
+ };
424
+ }
425
+
426
+ export function readWorkerFederationEnvelope(
427
+ value: unknown,
428
+ kind: 'request' | 'response',
429
+ admitted: AdmittedWorkerFederationExpose,
430
+ ): unknown {
431
+ const record = workerFederationRecord(value, `${kind} envelope`);
432
+ exactFields(
433
+ record,
434
+ ['schema', 'schemaVersion', 'authority', 'expose', 'locale', 'payload'],
435
+ `${kind} envelope`,
436
+ );
437
+ if (
438
+ record.schema !== `ultramodern.worker-federation-${kind}` ||
439
+ record.schemaVersion !== 1 ||
440
+ record.expose !== admitted.expose ||
441
+ record.locale !== admitted.locale ||
442
+ workerFederationWireJson(record.authority) !==
443
+ workerFederationWireJson(admitted.authority)
444
+ )
445
+ throw new Error(
446
+ `Worker federation ${kind} conflicts with the admitted publication.`,
447
+ );
448
+ return record.payload;
449
+ }