@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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/abort.js +65 -0
  3. package/dist/cjs/adapter.js +62 -0
  4. package/dist/cjs/data/body.js +78 -0
  5. package/dist/cjs/data/client.js +409 -0
  6. package/dist/cjs/data/codec.js +168 -0
  7. package/dist/cjs/data/index.js +125 -0
  8. package/dist/cjs/data/private.js +77 -0
  9. package/dist/cjs/data/routes.js +149 -0
  10. package/dist/cjs/data/server.js +791 -0
  11. package/dist/cjs/data/static.js +110 -0
  12. package/dist/cjs/data/tree.js +231 -0
  13. package/dist/cjs/data/types.js +56 -0
  14. package/dist/cjs/document.js +184 -0
  15. package/dist/cjs/entry-client.js +142 -0
  16. package/dist/cjs/entry-server.js +253 -0
  17. package/dist/cjs/federation.js +80 -0
  18. package/dist/cjs/identity.js +120 -0
  19. package/dist/cjs/index.js +46 -0
  20. package/dist/cjs/localization.js +48 -0
  21. package/dist/cjs/router.js +293 -0
  22. package/dist/cjs/server/assets.js +56 -0
  23. package/dist/cjs/server/dispatch.js +351 -0
  24. package/dist/cjs/server/index.js +57 -0
  25. package/dist/cjs/server/types.js +18 -0
  26. package/dist/cjs/server/worker.js +73 -0
  27. package/dist/cjs/session/cache.js +172 -0
  28. package/dist/cjs/session/index.js +63 -0
  29. package/dist/cjs/session/request.js +269 -0
  30. package/dist/cjs/session/types.js +18 -0
  31. package/dist/esm/abort.mjs +27 -0
  32. package/dist/esm/adapter.mjs +21 -0
  33. package/dist/esm/data/body.mjs +40 -0
  34. package/dist/esm/data/client.mjs +368 -0
  35. package/dist/esm/data/codec.mjs +112 -0
  36. package/dist/esm/data/index.mjs +7 -0
  37. package/dist/esm/data/private.mjs +39 -0
  38. package/dist/esm/data/routes.mjs +108 -0
  39. package/dist/esm/data/server.mjs +723 -0
  40. package/dist/esm/data/static.mjs +51 -0
  41. package/dist/esm/data/tree.mjs +193 -0
  42. package/dist/esm/data/types.mjs +6 -0
  43. package/dist/esm/document.mjs +131 -0
  44. package/dist/esm/entry-client.mjs +104 -0
  45. package/dist/esm/entry-server.mjs +215 -0
  46. package/dist/esm/federation.mjs +36 -0
  47. package/dist/esm/identity.mjs +63 -0
  48. package/dist/esm/index.mjs +1 -0
  49. package/dist/esm/localization.mjs +10 -0
  50. package/dist/esm/router.mjs +225 -0
  51. package/dist/esm/server/assets.mjs +15 -0
  52. package/dist/esm/server/dispatch.mjs +307 -0
  53. package/dist/esm/server/index.mjs +3 -0
  54. package/dist/esm/server/types.mjs +0 -0
  55. package/dist/esm/server/worker.mjs +35 -0
  56. package/dist/esm/session/cache.mjs +116 -0
  57. package/dist/esm/session/index.mjs +3 -0
  58. package/dist/esm/session/request.mjs +231 -0
  59. package/dist/esm/session/types.mjs +0 -0
  60. package/dist/esm-node/abort.mjs +28 -0
  61. package/dist/esm-node/adapter.mjs +22 -0
  62. package/dist/esm-node/data/body.mjs +41 -0
  63. package/dist/esm-node/data/client.mjs +369 -0
  64. package/dist/esm-node/data/codec.mjs +113 -0
  65. package/dist/esm-node/data/index.mjs +8 -0
  66. package/dist/esm-node/data/private.mjs +40 -0
  67. package/dist/esm-node/data/routes.mjs +109 -0
  68. package/dist/esm-node/data/server.mjs +724 -0
  69. package/dist/esm-node/data/static.mjs +52 -0
  70. package/dist/esm-node/data/tree.mjs +194 -0
  71. package/dist/esm-node/data/types.mjs +7 -0
  72. package/dist/esm-node/document.mjs +132 -0
  73. package/dist/esm-node/entry-client.mjs +105 -0
  74. package/dist/esm-node/entry-server.mjs +216 -0
  75. package/dist/esm-node/federation.mjs +37 -0
  76. package/dist/esm-node/identity.mjs +64 -0
  77. package/dist/esm-node/index.mjs +2 -0
  78. package/dist/esm-node/localization.mjs +11 -0
  79. package/dist/esm-node/router.mjs +226 -0
  80. package/dist/esm-node/server/assets.mjs +16 -0
  81. package/dist/esm-node/server/dispatch.mjs +308 -0
  82. package/dist/esm-node/server/index.mjs +4 -0
  83. package/dist/esm-node/server/types.mjs +1 -0
  84. package/dist/esm-node/server/worker.mjs +36 -0
  85. package/dist/esm-node/session/cache.mjs +117 -0
  86. package/dist/esm-node/session/index.mjs +4 -0
  87. package/dist/esm-node/session/request.mjs +232 -0
  88. package/dist/esm-node/session/types.mjs +1 -0
  89. package/dist/types/abort.d.ts +6 -0
  90. package/dist/types/adapter.d.ts +195 -0
  91. package/dist/types/data/body.d.ts +2 -0
  92. package/dist/types/data/client.d.ts +17 -0
  93. package/dist/types/data/codec.d.ts +14 -0
  94. package/dist/types/data/index.d.ts +8 -0
  95. package/dist/types/data/private.d.ts +6 -0
  96. package/dist/types/data/routes.d.ts +28 -0
  97. package/dist/types/data/server.d.ts +59 -0
  98. package/dist/types/data/static.d.ts +31 -0
  99. package/dist/types/data/tree.d.ts +3 -0
  100. package/dist/types/data/types.d.ts +104 -0
  101. package/dist/types/document.d.ts +76 -0
  102. package/dist/types/entry-client.d.ts +53 -0
  103. package/dist/types/entry-server.d.ts +63 -0
  104. package/dist/types/federation.d.ts +56 -0
  105. package/dist/types/identity.d.ts +27 -0
  106. package/dist/types/index.d.ts +1 -0
  107. package/dist/types/localization.d.ts +58 -0
  108. package/dist/types/router.d.ts +201 -0
  109. package/dist/types/server/assets.d.ts +14 -0
  110. package/dist/types/server/dispatch.d.ts +11 -0
  111. package/dist/types/server/index.d.ts +4 -0
  112. package/dist/types/server/types.d.ts +64 -0
  113. package/dist/types/server/worker.d.ts +25 -0
  114. package/dist/types/session/cache.d.ts +25 -0
  115. package/dist/types/session/index.d.ts +4 -0
  116. package/dist/types/session/request.d.ts +8 -0
  117. package/dist/types/session/types.d.ts +48 -0
  118. package/package.json +159 -4
  119. package/src/abort.ts +40 -0
  120. package/src/adapter.ts +255 -0
  121. package/src/data/body.ts +43 -0
  122. package/src/data/client.ts +501 -0
  123. package/src/data/codec.ts +247 -0
  124. package/src/data/index.ts +8 -0
  125. package/src/data/private.ts +68 -0
  126. package/src/data/routes.ts +201 -0
  127. package/src/data/server.ts +977 -0
  128. package/src/data/static.ts +105 -0
  129. package/src/data/tree.ts +244 -0
  130. package/src/data/types.ts +98 -0
  131. package/src/document.ts +338 -0
  132. package/src/entry-client.ts +236 -0
  133. package/src/entry-server.ts +393 -0
  134. package/src/federation.ts +111 -0
  135. package/src/identity.ts +128 -0
  136. package/src/index.ts +7 -0
  137. package/src/localization.ts +77 -0
  138. package/src/router.ts +475 -0
  139. package/src/server/assets.ts +60 -0
  140. package/src/server/dispatch.ts +523 -0
  141. package/src/server/index.ts +16 -0
  142. package/src/server/types.ts +94 -0
  143. package/src/server/worker.ts +94 -0
  144. package/src/session/cache.ts +177 -0
  145. package/src/session/index.ts +11 -0
  146. package/src/session/request.ts +324 -0
  147. package/src/session/types.ts +60 -0
@@ -0,0 +1,393 @@
1
+ import { untilAborted } from './abort';
2
+ import {
3
+ type DataOutcome,
4
+ handleDataRequest,
5
+ invokeRouteData,
6
+ normalizeDataResult,
7
+ } from './data';
8
+ import type { DocumentAsset, DocumentInlineData } from './document';
9
+ import type { NativeFederationBinding } from './federation';
10
+ import { assertRendererIdentity, type RendererIdentity } from './identity';
11
+ import {
12
+ type NativeEntryI18n,
13
+ type NativeI18nInstance,
14
+ type NativeI18nView,
15
+ type NativeRequestLanguage,
16
+ nativeI18nView,
17
+ } from './localization';
18
+ import {
19
+ isRoutedApplication,
20
+ matchApplicationRouteIds,
21
+ type NativeApplicationModule,
22
+ type NativeRoutedApplication,
23
+ type NativeRouteMatch,
24
+ type NativeRouteMatcher,
25
+ type NativeRouterOptions,
26
+ selectApplicationDataRoute,
27
+ } from './router';
28
+ import type { NativeRequestContext, NativeServerManifest } from './server';
29
+ import type { RequestSession } from './session';
30
+
31
+ export type * from './localization';
32
+ export type {
33
+ NativeApplicationModule,
34
+ NativeRoutedApplication,
35
+ NativeRouterOptions,
36
+ } from './router';
37
+
38
+ /** What a generated server entry passes to its renderer. */
39
+ export interface NativeServerEntryOptions<
40
+ Instance extends NativeI18nInstance = NativeI18nInstance,
41
+ LocalisedUrls = unknown,
42
+ > {
43
+ readonly identity: RendererIdentity;
44
+ readonly federation?: NativeFederationBinding;
45
+ /** The generated application module, imported per request. */
46
+ readonly app: () => Promise<NativeApplicationModule>;
47
+ readonly i18n?: NativeEntryI18n<Instance, LocalisedUrls>;
48
+ /** The document mount element id; the client entry must use the same. */
49
+ readonly rootId?: string;
50
+ }
51
+
52
+ /** The renderer-neutral part of one response document. */
53
+ export interface NativeServerDocument {
54
+ readonly documentId: string;
55
+ readonly rootId: string;
56
+ readonly nonce?: string;
57
+ readonly assets?: readonly DocumentAsset[];
58
+ readonly lang?: string;
59
+ readonly inlineData?: readonly DocumentInlineData[];
60
+ }
61
+
62
+ export interface NativeServerRoutesInput<
63
+ Router,
64
+ Document,
65
+ Instance,
66
+ LocalisedUrls,
67
+ > {
68
+ /** The request bound to the session signal; data loaders observe it. */
69
+ readonly request: Request;
70
+ readonly context: NativeRequestContext;
71
+ readonly router: Router;
72
+ /** Blocking data outcomes, recorded while the router loads. */
73
+ readonly outcomes: readonly DataOutcome[];
74
+ readonly document: Document;
75
+ /** Request-private values the router must never serialize. */
76
+ readonly forbiddenValues: readonly object[];
77
+ readonly i18n?: NativeI18nView<Instance, LocalisedUrls>;
78
+ }
79
+
80
+ /** The renderer's own request scope, manifest, router and document. */
81
+ export interface NativeServerAdapter<
82
+ Router extends NativeRouteMatcher<unknown, NativeRouteMatch>,
83
+ Document extends NativeServerDocument,
84
+ Instance extends NativeI18nInstance,
85
+ LocalisedUrls,
86
+ > {
87
+ /** Validate the compiler manifest and complete the document. */
88
+ document(
89
+ base: NativeServerDocument,
90
+ nativeManifest: unknown,
91
+ identity: Readonly<RendererIdentity>,
92
+ ): Document;
93
+ /** Run one request inside the renderer's native request scope. */
94
+ run<Result>(
95
+ session: RequestSession,
96
+ callback: () => Promise<Result>,
97
+ ): Promise<Result>;
98
+ /** Commit a terminal data or redirect response. */
99
+ respond(session: RequestSession, response: Response): Response;
100
+ createRouter(
101
+ application: NativeRoutedApplication,
102
+ options: NativeRouterOptions,
103
+ ): Router;
104
+ renderCSR(
105
+ context: NativeRequestContext,
106
+ document: Document,
107
+ ): Response | Promise<Response>;
108
+ renderComponent(
109
+ context: NativeRequestContext,
110
+ component: unknown,
111
+ document: Document,
112
+ i18n?: NativeI18nView<Instance, LocalisedUrls>,
113
+ ): Promise<Response>;
114
+ /** Load the router, resolve its HTTP outcome and render the document. */
115
+ renderRoutes(
116
+ input: NativeServerRoutesInput<Router, Document, Instance, LocalisedUrls>,
117
+ ): Promise<Response>;
118
+ }
119
+
120
+ export type NativeServerEntry = Required<NativeServerManifest>;
121
+
122
+ function serverRouteLoader(
123
+ application: NativeRoutedApplication,
124
+ ): NativeRouterOptions['loadRoute'] {
125
+ return async (route, input) => {
126
+ const loader = application.dataModules[route.id]?.loader;
127
+ if (!loader) return { kind: 'success', value: undefined, status: 200 };
128
+ return invokeRouteData(loader, input, {
129
+ production: process.env.NODE_ENV !== 'development',
130
+ });
131
+ };
132
+ }
133
+
134
+ /** `vary` names the request headers a detected document language came from. */
135
+ function resolveDocumentPolicy(
136
+ session: RequestSession,
137
+ vary?: readonly string[],
138
+ ): void {
139
+ if (!session.responsePolicy)
140
+ session.resolveResponse({
141
+ kind: 'document',
142
+ status: 200,
143
+ headers: [
144
+ ['content-type', 'text/html; charset=utf-8'],
145
+ ...(vary?.length ? [['vary', vary.join(', ')] as const] : []),
146
+ ],
147
+ cache: { mode: 'no-store' },
148
+ });
149
+ }
150
+
151
+ const detectedVary = (language: NativeRequestLanguage | undefined) =>
152
+ language?.kind === 'language' ? language.vary : undefined;
153
+
154
+ /**
155
+ * Create the native transport handlers of a generated server entry: identity
156
+ * and session ownership, CSR and SSR documents, localized redirects, route
157
+ * matching and the data endpoint. The renderer adapter owns its request
158
+ * scope, router and document.
159
+ */
160
+ export function createNativeServerEntry<
161
+ Router extends NativeRouteMatcher<unknown, NativeRouteMatch>,
162
+ Document extends NativeServerDocument,
163
+ Instance extends NativeI18nInstance,
164
+ LocalisedUrls,
165
+ >(
166
+ options: NativeServerEntryOptions<Instance, LocalisedUrls>,
167
+ adapter: NativeServerAdapter<Router, Document, Instance, LocalisedUrls>,
168
+ ): NativeServerEntry {
169
+ const rendererIdentity = Object.freeze({ ...options.identity });
170
+ const { i18n } = options;
171
+
172
+ const own = (request: Request, context: NativeRequestContext) => {
173
+ assertRendererIdentity(context.entry, rendererIdentity);
174
+ assertRendererIdentity(context.session.identity, rendererIdentity);
175
+ if (request !== context.session.request)
176
+ throw new Error('Native request/session ownership mismatch');
177
+ };
178
+
179
+ const prepare = (request: Request, context: NativeRequestContext) => {
180
+ own(request, context);
181
+ return adapter.document(
182
+ {
183
+ documentId: crypto.randomUUID(),
184
+ rootId: options.rootId ?? 'root',
185
+ ...(context.nonce === undefined ? {} : { nonce: context.nonce }),
186
+ ...(context.assets === undefined ? {} : { assets: context.assets }),
187
+ },
188
+ context.nativeManifest,
189
+ rendererIdentity,
190
+ );
191
+ };
192
+
193
+ // Application and translation loads race the session signal: a stalled
194
+ // load, such as a development compilation, must not hold an aborted
195
+ // request, its context and its session.
196
+ const routedApplication = async (
197
+ signal: AbortSignal,
198
+ ): Promise<NativeRoutedApplication> => {
199
+ const application = await untilAborted(options.app(), signal);
200
+ if (!isRoutedApplication(application))
201
+ throw new Error(
202
+ 'unsupported-renderer-capability: ssrByRouteIds requires native route matching.',
203
+ );
204
+ return application;
205
+ };
206
+
207
+ const nativeCSRRequestHandler = (
208
+ request: Request,
209
+ context: NativeRequestContext,
210
+ ) =>
211
+ adapter.run(context.session, async () => {
212
+ const document = prepare(request, context);
213
+ if (!i18n) {
214
+ resolveDocumentPolicy(context.session);
215
+ return adapter.renderCSR(context, document);
216
+ }
217
+ const language = i18n.resolveRequest(request);
218
+ if (language.kind === 'redirect')
219
+ return adapter.respond(
220
+ context.session,
221
+ i18n.redirect(language.location),
222
+ );
223
+ resolveDocumentPolicy(context.session, detectedVary(language));
224
+ return adapter.renderCSR(context, {
225
+ ...document,
226
+ lang: language.language,
227
+ inlineData: [i18n.handoff(language.language)],
228
+ });
229
+ });
230
+
231
+ const nativeMatchRouteIds = (
232
+ request: Request,
233
+ context: NativeRequestContext,
234
+ ) =>
235
+ adapter.run(context.session, async () => {
236
+ own(request, context);
237
+ const application = await routedApplication(context.session.signal);
238
+ const router = adapter.createRouter(application, {
239
+ identity: rendererIdentity,
240
+ loadRoute: serverRouteLoader(application),
241
+ request,
242
+ context: context.session.platform.bindings,
243
+ session: context.session,
244
+ ...(i18n
245
+ ? {
246
+ rewrite: i18n.rewrite(
247
+ () => i18n.resolveRequest(request).language,
248
+ ),
249
+ }
250
+ : {}),
251
+ });
252
+ return matchApplicationRouteIds(router, new URL(request.url));
253
+ });
254
+
255
+ const nativeRequestHandler = (
256
+ request: Request,
257
+ context: NativeRequestContext,
258
+ ) =>
259
+ adapter.run(context.session, async () => {
260
+ const document = prepare(request, context);
261
+ const { session } = context;
262
+ const application = await untilAborted(options.app(), session.signal);
263
+ const routed = isRoutedApplication(application);
264
+ const nativeRequest = routed
265
+ ? new Request(request, { signal: session.signal })
266
+ : request;
267
+ // One isolated instance per request; the URL decides its language.
268
+ const language = i18n?.resolveRequest(nativeRequest);
269
+ if (!routed && i18n && language?.kind === 'redirect')
270
+ return adapter.respond(session, i18n.redirect(language.location));
271
+ // Data requests and redirects never read translations, so the instance
272
+ // is created only once a document is about to render.
273
+ const localize = async (
274
+ inlineData: readonly DocumentInlineData[] = [],
275
+ ) => {
276
+ if (!i18n || !language) return { document, localization: undefined };
277
+ const instance = await untilAborted(
278
+ i18n.create(language.language),
279
+ session.signal,
280
+ );
281
+ return {
282
+ document: {
283
+ ...document,
284
+ lang: instance.language,
285
+ inlineData: [
286
+ ...inlineData,
287
+ i18n.handoff(instance.language, instance),
288
+ ],
289
+ },
290
+ localization: nativeI18nView(i18n, instance),
291
+ };
292
+ };
293
+ if (!routed) {
294
+ const localized = await localize(document.inlineData);
295
+ resolveDocumentPolicy(session, detectedVary(language));
296
+ return adapter.renderComponent(
297
+ context,
298
+ application.default,
299
+ localized.document,
300
+ localized.localization,
301
+ );
302
+ }
303
+ const bindings = session.platform.bindings;
304
+ const outcomes: DataOutcome[] = [];
305
+ // Loaders settle in any order, but later metadata wins singleton
306
+ // fields, so outcomes stay in matched parent-to-leaf route order.
307
+ const outcomeRanks: number[] = [];
308
+ let routeOrder: readonly string[] | undefined;
309
+ const addOutcome = (rank: number, outcome: DataOutcome) => {
310
+ const at = outcomeRanks.findIndex(other => other > rank);
311
+ const index = at < 0 ? outcomes.length : at;
312
+ outcomes.splice(index, 0, outcome);
313
+ outcomeRanks.splice(index, 0, rank);
314
+ };
315
+ const router = adapter.createRouter(application, {
316
+ identity: rendererIdentity,
317
+ loadRoute: serverRouteLoader(application),
318
+ request: nativeRequest,
319
+ context: bindings,
320
+ onOutcome: (routeId, outcome) => {
321
+ if ('response' in outcome) {
322
+ routeOrder ??= matchApplicationRouteIds(
323
+ router,
324
+ new URL(nativeRequest.url),
325
+ );
326
+ const rank = routeOrder.indexOf(routeId);
327
+ addOutcome(rank < 0 ? Number.POSITIVE_INFINITY : rank, outcome);
328
+ } else if (outcome.completion) {
329
+ const completion = outcome.completion;
330
+ void completion.catch(error => session.fail(error));
331
+ session.registerCleanup(() => completion);
332
+ }
333
+ },
334
+ session,
335
+ ...(context.nonce === undefined ? {} : { nonce: context.nonce }),
336
+ ...(i18n && language
337
+ ? { rewrite: i18n.rewrite(() => language.language) }
338
+ : {}),
339
+ });
340
+ const dataResponse = await handleDataRequest({
341
+ request: nativeRequest,
342
+ identity: rendererIdentity,
343
+ context: bindings,
344
+ privateValues: [context, session, session.platform, bindings],
345
+ // Only development keeps loader errors' messages; an unset mode stays private.
346
+ production: process.env.NODE_ENV !== 'development',
347
+ selectRoute: (dataRequest, routeId, operation) =>
348
+ selectApplicationDataRoute(
349
+ router,
350
+ dataRequest,
351
+ routeId,
352
+ operation,
353
+ application.dataModules,
354
+ ),
355
+ });
356
+ if (dataResponse) return adapter.respond(session, dataResponse);
357
+ if (i18n && language?.kind === 'redirect')
358
+ return adapter.respond(session, i18n.redirect(language.location));
359
+ // A header-detected language makes the document depend on those headers:
360
+ // the header-only outcome adds `Vary` and keeps it out of public caches.
361
+ const vary = detectedVary(language);
362
+ if (vary?.length)
363
+ addOutcome(
364
+ Number.POSITIVE_INFINITY,
365
+ await normalizeDataResult(
366
+ new Response(null, { headers: { vary: vary.join(', ') } }),
367
+ ),
368
+ );
369
+ const localized = await localize();
370
+ return adapter.renderRoutes({
371
+ request: nativeRequest,
372
+ context,
373
+ router,
374
+ outcomes,
375
+ document: localized.document,
376
+ forbiddenValues: [
377
+ context,
378
+ session,
379
+ session.platform,
380
+ bindings,
381
+ nativeRequest,
382
+ ],
383
+ ...(localized.localization ? { i18n: localized.localization } : {}),
384
+ });
385
+ });
386
+
387
+ return {
388
+ rendererIdentity,
389
+ nativeRequestHandler,
390
+ nativeCSRRequestHandler,
391
+ nativeMatchRouteIds,
392
+ };
393
+ }
@@ -0,0 +1,111 @@
1
+ /** The application compiler's native Module Federation runtime. */
2
+ export interface FederationInstance {
3
+ /** The application's federation name, unique among this realm's instances. */
4
+ readonly name?: string;
5
+ loadRemote<T>(id: string): Promise<T | null>;
6
+ readonly remoteHandler?: {
7
+ readonly idToRemoteMap?: Record<string, { name: string; expose: string }>;
8
+ };
9
+ readonly moduleCache?: Map<string, { readonly remoteInfo: unknown }>;
10
+ readonly snapshotHandler?: {
11
+ getGlobalRemoteInfo(info: unknown): { remoteSnapshot?: RemoteSnapshot };
12
+ };
13
+ }
14
+
15
+ interface RemoteSnapshot {
16
+ readonly publicPath?: unknown;
17
+ readonly remoteEntry?: unknown;
18
+ readonly modules?: readonly {
19
+ readonly modulePath?: unknown;
20
+ readonly assets?: {
21
+ readonly js?: { readonly sync?: readonly string[] };
22
+ readonly css?: {
23
+ readonly sync?: readonly string[];
24
+ readonly async?: readonly string[];
25
+ };
26
+ };
27
+ }[];
28
+ }
29
+
30
+ /** Generated entries retain their own compiler's host, including on the server. */
31
+ export interface NativeFederationBinding {
32
+ readonly instance: () => FederationInstance;
33
+ /**
34
+ * Server bindings: the client module, relative to the client asset base,
35
+ * that loads a server-rendered remote through this host before hydration.
36
+ */
37
+ readonly hydrationModule?: string;
38
+ }
39
+
40
+ export interface FederatedAssets {
41
+ readonly js: readonly string[];
42
+ readonly css: readonly string[];
43
+ }
44
+
45
+ /** Capture the host before importing application modules or remote containers. */
46
+ export function getFederationHost(
47
+ binding: NativeFederationBinding | undefined,
48
+ ): FederationInstance | undefined {
49
+ if (binding === undefined) return undefined;
50
+ if (typeof binding?.instance !== 'function')
51
+ throw new TypeError('The native federation host binding is invalid.');
52
+ const instance = binding.instance();
53
+ if (!instance || typeof instance.loadRemote !== 'function')
54
+ throw new TypeError('The application federation runtime has not started.');
55
+ return instance;
56
+ }
57
+
58
+ /** Load through the selected application's runtime, never another app's host. */
59
+ export async function loadFederatedModule<T>(
60
+ instance: FederationInstance | undefined,
61
+ id: string,
62
+ ): Promise<T> {
63
+ if (!instance)
64
+ throw new Error(
65
+ `Cannot load ${id}: this application has no Module Federation runtime. Add module-federation.config.ts with its remotes.`,
66
+ );
67
+ const module = await instance.loadRemote<T>(id);
68
+ if (!module) throw new Error(`Remote module ${id} is unavailable`);
69
+ return module;
70
+ }
71
+
72
+ /** Browser assets from the native snapshot that admitted and loaded this remote. */
73
+ export function remoteBrowserAssets(
74
+ instance: FederationInstance,
75
+ id: string,
76
+ ): FederatedAssets | undefined {
77
+ const target = instance.remoteHandler?.idToRemoteMap?.[id];
78
+ const loaded = target && instance.moduleCache?.get(target.name);
79
+ const snapshot =
80
+ loaded &&
81
+ instance.snapshotHandler?.getGlobalRemoteInfo(loaded.remoteInfo)
82
+ .remoteSnapshot;
83
+ if (
84
+ !snapshot ||
85
+ typeof snapshot.publicPath !== 'string' ||
86
+ !snapshot.publicPath ||
87
+ typeof snapshot.remoteEntry !== 'string' ||
88
+ !snapshot.remoteEntry
89
+ )
90
+ return undefined;
91
+ const expose = snapshot.modules?.find(
92
+ module => module.modulePath === target.expose,
93
+ );
94
+ if (!expose?.assets) return undefined;
95
+ const publicPath = snapshot.publicPath;
96
+ const url = (file: string) =>
97
+ /^(?:https?:)?\/\//iu.test(file)
98
+ ? file
99
+ : `${publicPath.replace(/\/$/u, '')}/${file.replace(/^\//u, '')}`;
100
+ return {
101
+ js: [url(snapshot.remoteEntry), ...(expose.assets.js?.sync ?? []).map(url)],
102
+ css: [
103
+ ...new Set(
104
+ [
105
+ ...(expose.assets.css?.sync ?? []),
106
+ ...(expose.assets.css?.async ?? []),
107
+ ].map(url),
108
+ ),
109
+ ],
110
+ };
111
+ }
@@ -0,0 +1,128 @@
1
+ export type Renderer = string;
2
+
3
+ /** Immutable identity for one application entry and its hydration bytes. */
4
+ export interface RendererIdentity {
5
+ renderer: Renderer;
6
+ appId: string;
7
+ entryName: string;
8
+ protocolVersion: 1;
9
+ buildId: string;
10
+ }
11
+
12
+ export function resolveRenderer(value: unknown = 'react'): Renderer {
13
+ if (
14
+ typeof value === 'string' &&
15
+ value.trim() === value &&
16
+ /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(value)
17
+ )
18
+ return value;
19
+ throw new Error(`Unsupported UltraModern renderer: ${String(value)}`);
20
+ }
21
+
22
+ export function identityCacheKey(identity: RendererIdentity): string {
23
+ resolveRenderer(identity.renderer);
24
+ for (const key of ['appId', 'entryName', 'buildId'] as const) {
25
+ if (
26
+ typeof identity[key] !== 'string' ||
27
+ identity[key].trim().length === 0
28
+ ) {
29
+ throw new Error(`Renderer identity requires a nonempty ${key}`);
30
+ }
31
+ }
32
+ if (identity.protocolVersion !== 1) {
33
+ throw new Error('Unsupported renderer data protocol version');
34
+ }
35
+ return JSON.stringify([
36
+ identity.renderer,
37
+ identity.appId,
38
+ identity.entryName,
39
+ identity.protocolVersion,
40
+ identity.buildId,
41
+ ]);
42
+ }
43
+
44
+ export function assertRendererIdentity(
45
+ actual: RendererIdentity,
46
+ expected: RendererIdentity,
47
+ ): void {
48
+ if (identityCacheKey(actual) !== identityCacheKey(expected)) {
49
+ throw new Error('Renderer identity conflicts with the application build');
50
+ }
51
+ }
52
+
53
+ const IDENTITY_FIELDS = [
54
+ 'renderer',
55
+ 'appId',
56
+ 'entryName',
57
+ 'protocolVersion',
58
+ 'buildId',
59
+ ] as const;
60
+
61
+ /**
62
+ * Read an identity from untrusted JSON (a document bootstrap or a build
63
+ * manifest). It must be a plain record with exactly the identity fields and
64
+ * equal the identity this application was built with.
65
+ */
66
+ export function readRendererIdentity(
67
+ value: unknown,
68
+ expected: RendererIdentity,
69
+ ): Readonly<RendererIdentity> {
70
+ if (
71
+ !value ||
72
+ typeof value !== 'object' ||
73
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
74
+ Object.keys(value).some(
75
+ key => !(IDENTITY_FIELDS as readonly string[]).includes(key),
76
+ )
77
+ ) {
78
+ throw new Error('A renderer identity must contain only identity fields.');
79
+ }
80
+ const identity = value as RendererIdentity;
81
+ assertRendererIdentity(identity, expected);
82
+ return Object.freeze({
83
+ renderer: identity.renderer,
84
+ appId: identity.appId,
85
+ entryName: identity.entryName,
86
+ protocolVersion: identity.protocolVersion,
87
+ buildId: identity.buildId,
88
+ });
89
+ }
90
+
91
+ /** The emitted client compiler manifest for one native renderer entry. */
92
+ export function nativeModuleManifestFilename(
93
+ renderer: Renderer,
94
+ entryName: string,
95
+ ): string {
96
+ if (typeof entryName !== 'string' || !entryName.trim()) {
97
+ throw new Error(
98
+ 'Native module manifests require an application entry name.',
99
+ );
100
+ }
101
+ return `${resolveRenderer(renderer)}-module-manifest.${encodeURIComponent(entryName)}.json`;
102
+ }
103
+
104
+ /** Response header naming the built entry identity that rendered a document. */
105
+ export const RENDERER_IDENTITY_HEADER = 'x-ultramodern-renderer-identity';
106
+
107
+ /**
108
+ * Serialize an entry identity as an ASCII-only header value (JSON with
109
+ * non-ASCII code units escaped), so every host emits the same bytes.
110
+ */
111
+ export function serializeRendererIdentityHeader(
112
+ identity: RendererIdentity,
113
+ ): string {
114
+ identityCacheKey(identity);
115
+ return JSON.stringify({
116
+ renderer: identity.renderer,
117
+ appId: identity.appId,
118
+ entryName: identity.entryName,
119
+ protocolVersion: identity.protocolVersion,
120
+ buildId: identity.buildId,
121
+ }).replace(/[\u007f-\u{10ffff}]/gu, character =>
122
+ Array.from(
123
+ { length: character.length },
124
+ (_, index) =>
125
+ `\\u${character.charCodeAt(index).toString(16).padStart(4, '0')}`,
126
+ ).join(''),
127
+ );
128
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export {
2
+ assertRendererIdentity,
3
+ identityCacheKey,
4
+ type Renderer,
5
+ type RendererIdentity,
6
+ resolveRenderer,
7
+ } from './identity';