@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.8 → 3.9.3-ultramodern.9

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 (61) hide show
  1. package/dist/cjs/client.js +56 -6
  2. package/dist/cjs/entry-application.js +7 -2
  3. package/dist/cjs/entry-client.js +3 -0
  4. package/dist/cjs/entry-server.js +11 -3
  5. package/dist/cjs/federation.js +108 -6
  6. package/dist/cjs/i18n/LocalizedLink.js +24 -33
  7. package/dist/cjs/i18n/index.js +0 -3
  8. package/dist/cjs/i18n-external-snapshot.js +645 -0
  9. package/dist/cjs/plugin/index.js +1 -1
  10. package/dist/cjs/plugin/profile.js +5 -4
  11. package/dist/cjs/router-external-snapshot.js +263 -0
  12. package/dist/cjs/server.js +15 -2
  13. package/dist/cjs/worker-federation-context.js +63 -0
  14. package/dist/cjs/worker-federation-contexts.js +97 -0
  15. package/dist/cjs/worker-federation.js +97 -0
  16. package/dist/esm/client.mjs +56 -6
  17. package/dist/esm/entry-application.mjs +7 -2
  18. package/dist/esm/entry-client.mjs +3 -0
  19. package/dist/esm/entry-server.mjs +8 -3
  20. package/dist/esm/federation.mjs +110 -8
  21. package/dist/esm/i18n/LocalizedLink.mjs +24 -30
  22. package/dist/esm/i18n/index.mjs +1 -1
  23. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  24. package/dist/esm/router-external-snapshot.mjs +216 -0
  25. package/dist/esm/server.mjs +15 -2
  26. package/dist/esm/worker-federation-context.mjs +19 -0
  27. package/dist/esm/worker-federation-contexts.mjs +47 -0
  28. package/dist/esm/worker-federation.mjs +59 -0
  29. package/dist/esm-node/plugin/index.mjs +1 -1
  30. package/dist/esm-node/plugin/profile.mjs +5 -4
  31. package/dist/types/client.d.ts +5 -1
  32. package/dist/types/entry-client.d.ts +2 -0
  33. package/dist/types/entry-server.d.ts +4 -1
  34. package/dist/types/federation.d.ts +2 -0
  35. package/dist/types/i18n/LocalizedLink.d.ts +5 -32
  36. package/dist/types/i18n/context.d.ts +1 -1
  37. package/dist/types/i18n/index.d.ts +1 -1
  38. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  39. package/dist/types/router-external-snapshot.d.ts +10 -0
  40. package/dist/types/server.d.ts +3 -0
  41. package/dist/types/worker-federation-context.d.ts +34 -0
  42. package/dist/types/worker-federation-contexts.d.ts +25 -0
  43. package/dist/types/worker-federation.d.ts +20 -0
  44. package/package.json +69 -12
  45. package/src/client.ts +99 -16
  46. package/src/entry-application.ts +7 -3
  47. package/src/entry-client.ts +10 -0
  48. package/src/entry-server.ts +17 -6
  49. package/src/federation.ts +159 -15
  50. package/src/i18n/LocalizedLink.ts +46 -72
  51. package/src/i18n/context.ts +1 -1
  52. package/src/i18n/index.ts +1 -5
  53. package/src/i18n-external-snapshot.ts +803 -0
  54. package/src/plugin/compiler-manifest.cjs +27 -1
  55. package/src/plugin/index.ts +1 -1
  56. package/src/plugin/profile.ts +6 -4
  57. package/src/router-external-snapshot.ts +306 -0
  58. package/src/server.ts +29 -4
  59. package/src/worker-federation-context.ts +51 -0
  60. package/src/worker-federation-contexts.ts +91 -0
  61. package/src/worker-federation.ts +120 -0
@@ -3,6 +3,7 @@ import { createRoot, hydrateRoot } from "octane";
3
3
  import { bootstrapStreamedSignalHydration } from "octane/hydration/streamed-signals";
4
4
  import { assertNativeHydrationBuildId, assertOctaneIdentity, readOctaneDocumentBootstrap } from "./bootstrap.mjs";
5
5
  import { FederationApplication, createFederationScope } from "./federation-context.mjs";
6
+ import { WorkerFederationApplication } from "./worker-federation-context.mjs";
6
7
  const roots = new WeakMap();
7
8
  const documents = new WeakMap();
8
9
  const moduleResources = new WeakMap();
@@ -186,7 +187,11 @@ function createHandle(input) {
186
187
  const nextRelease = claimModuleResources(next);
187
188
  releaseModule = nextRelease;
188
189
  try {
189
- input.federation ? input.root.render(FederationApplication, {
190
+ input.workerFederation ? input.root.render(WorkerFederationApplication, {
191
+ scope: input.workerFederation,
192
+ federation: input.federation,
193
+ application: next
194
+ }) : input.federation ? input.root.render(FederationApplication, {
190
195
  scope: input.federation,
191
196
  application: next
192
197
  }) : input.root.render(next.default, next.props);
@@ -204,6 +209,7 @@ function createHandle(input) {
204
209
  disposed = true;
205
210
  input.signal?.removeEventListener('abort', onAbort);
206
211
  cleanupResources([
212
+ ()=>input.retireWorkerFederation?.(),
207
213
  ()=>input.root.unmount(),
208
214
  ()=>releaseModule(),
209
215
  ()=>input.bridge?.dispose(),
@@ -227,6 +233,12 @@ function createHandle(input) {
227
233
  }
228
234
  async function startApplication(input, release, hydration) {
229
235
  const errors = observeRootErrors(input.options);
236
+ const workerController = input.workerFederation ? new AbortController() : void 0;
237
+ let workerFederation;
238
+ const retireWorkerFederation = ()=>cleanupResources([
239
+ ()=>workerController?.abort(new DOMException('The Worker federation document was disposed', 'AbortError')),
240
+ ()=>workerFederation?.transport.dispose()
241
+ ]);
230
242
  let bridge;
231
243
  let root;
232
244
  let releaseModule;
@@ -237,6 +249,7 @@ async function startApplication(input, release, hydration) {
237
249
  if (cleaned) return;
238
250
  cleaned = true;
239
251
  cleanupResources([
252
+ retireWorkerFederation,
240
253
  ()=>root?.unmount(),
241
254
  ()=>releaseModule?.(),
242
255
  ()=>bridge?.dispose(),
@@ -244,7 +257,24 @@ async function startApplication(input, release, hydration) {
244
257
  ]);
245
258
  };
246
259
  try {
247
- const federation = input.federation ? createFederationScope(input.federation, false) : void 0;
260
+ if (input.workerFederation && workerController) {
261
+ const signal = AbortSignal.any([
262
+ workerController.signal,
263
+ ...input.signal ? [
264
+ input.signal
265
+ ] : []
266
+ ]);
267
+ const scope = 'function' == typeof input.workerFederation ? input.workerFederation(signal) : input.workerFederation;
268
+ workerFederation = {
269
+ ...scope,
270
+ signal: scope.signal ? AbortSignal.any([
271
+ signal,
272
+ scope.signal
273
+ ]) : signal
274
+ };
275
+ }
276
+ if (workerFederation && !hydration && !input.documentId) throw new Error('Worker federation requires the trusted document identity before application loading');
277
+ const federation = input.federation || workerFederation ? createFederationScope(input.federation, false) : void 0;
248
278
  if (hydration) {
249
279
  const { document, documentId } = hydration;
250
280
  bridge = bootstrapStreamedSignalHydration({
@@ -259,16 +289,34 @@ async function startApplication(input, release, hydration) {
259
289
  const application = loaded.application;
260
290
  releaseModule = loaded.releaseModule;
261
291
  input.signal?.throwIfAborted();
262
- if (bridge) root = hydrateRoot(input.container, federation ? FederationApplication : application.default, federation ? {
292
+ if (bridge) root = hydrateRoot(input.container, workerFederation ? WorkerFederationApplication : federation ? FederationApplication : application.default, workerFederation ? {
293
+ scope: workerFederation,
294
+ federation,
295
+ application
296
+ } : federation ? {
263
297
  scope: federation,
264
298
  application
265
299
  } : application.props, {
266
300
  ...errors.options,
267
- signalOwner: bridge.signalOwner
301
+ signalOwner: bridge.signalOwner,
302
+ externalSnapshots: {
303
+ documentId: hydration.documentId
304
+ }
268
305
  });
269
306
  else {
270
- root = createRoot(input.container, errors.options);
271
- federation ? root.render(FederationApplication, {
307
+ root = createRoot(input.container, {
308
+ ...errors.options,
309
+ ...input.documentId ? {
310
+ externalSnapshots: {
311
+ documentId: input.documentId
312
+ }
313
+ } : {}
314
+ });
315
+ workerFederation ? root.render(WorkerFederationApplication, {
316
+ scope: workerFederation,
317
+ federation,
318
+ application
319
+ }) : federation ? root.render(FederationApplication, {
272
320
  scope: federation,
273
321
  application
274
322
  }) : root.render(application.default, application.props);
@@ -282,6 +330,8 @@ async function startApplication(input, release, hydration) {
282
330
  release,
283
331
  bridge,
284
332
  federation,
333
+ workerFederation,
334
+ retireWorkerFederation,
285
335
  ...void 0 === input.signal ? {} : {
286
336
  signal: input.signal
287
337
  },
@@ -1,6 +1,7 @@
1
1
  import { createMemoryHistory, createRouter } from "@octanejs/tanstack-router";
2
2
  import { createElement } from "octane";
3
3
  import { I18nProvider } from "./i18n/I18nProvider.mjs";
4
+ import { associateOctaneFileRouteSnapshot } from "./router-external-snapshot.mjs";
4
5
  import { createFileSystemRouteTree } from "./routes.mjs";
5
6
  function componentView(component, i18n) {
6
7
  const App = component;
@@ -14,7 +15,8 @@ function componentView(component, i18n) {
14
15
  }
15
16
  const octaneRouterFactory = {
16
17
  routeTree: (routes, modules, options)=>createFileSystemRouteTree(routes, modules, options),
17
- router: ({ location, ...options })=>createRouter({
18
+ router: ({ location, ...options })=>{
19
+ const router = createRouter({
18
20
  ...options,
19
21
  ...location ? {
20
22
  origin: location.origin,
@@ -24,7 +26,10 @@ const octaneRouterFactory = {
24
26
  ]
25
27
  })
26
28
  } : {}
27
- })
29
+ });
30
+ associateOctaneFileRouteSnapshot(router);
31
+ return router;
32
+ }
28
33
  };
29
34
  function localizedRouterRoot(Root) {
30
35
  return ({ router, i18n })=>createElement(I18nProvider, {
@@ -9,6 +9,7 @@ function startNativeClient(options) {
9
9
  startNativeClientEntry(options, {
10
10
  readBootstrap: (document, identity)=>readOctaneDocumentBootstrap(document, identity, nativeHydrationBuildId),
11
11
  createRouter: (application, routerOptions)=>createNativeRouter(application, routerOptions, octaneRouterFactory),
12
+ disposeRouter: (router)=>router.history.destroy(),
12
13
  async prepareRouter (router, hydrating, signal) {
13
14
  if (hydrating) await prepareOctaneRouterHydration(router, {
14
15
  signal
@@ -21,6 +22,8 @@ function startNativeClient(options) {
21
22
  identity,
22
23
  nativeHydrationBuildId,
23
24
  federation: options.federation,
25
+ workerFederation: options.workerFederation,
26
+ documentId: bootstrap?.documentId,
24
27
  signal,
25
28
  load: async ()=>{
26
29
  const view = await load();
@@ -34,13 +34,15 @@ function entry_server_createNativeServerEntry(options) {
34
34
  session,
35
35
  document
36
36
  }),
37
- renderComponent: ({ session }, component, document, i18n)=>renderOctaneApplication({
37
+ renderComponent: ({ session, workerFederation }, component, document, i18n)=>renderOctaneApplication({
38
38
  session,
39
39
  App: componentView(component, i18n),
40
40
  document,
41
- federation: options.federation
41
+ federation: options.federation,
42
+ workerFederation,
43
+ workerFederationLocale: options.workerFederationLocale
42
44
  }),
43
- async renderRoutes ({ request, context: { session }, router, outcomes, document, forbiddenValues, i18n }) {
45
+ async renderRoutes ({ request, context: { session, workerFederation }, router, outcomes, document, forbiddenValues, i18n }) {
44
46
  const response = await createOctaneRequestHandler({
45
47
  request,
46
48
  session,
@@ -73,6 +75,8 @@ function entry_server_createNativeServerEntry(options) {
73
75
  },
74
76
  document,
75
77
  federation: options.federation,
78
+ workerFederation,
79
+ workerFederationLocale: options.workerFederationLocale,
76
80
  router
77
81
  });
78
82
  return createSsrStreamResponse(router, rendered);
@@ -83,4 +87,5 @@ function entry_server_createNativeServerEntry(options) {
83
87
  }
84
88
  });
85
89
  }
90
+ export { createNativeWorkerFederationRuntime } from "./worker-federation.mjs";
86
91
  export { entry_server_createNativeServerEntry as createNativeServerEntry, withOctaneDocumentFields };
@@ -1,6 +1,9 @@
1
- import { loadFederatedModule, remoteBrowserAssets } from "@modern-js/renderer-core/federation";
2
- import { Suspense, createElement, lazy, preinit, useContext } from "octane";
1
+ import { loadFederatedModule } from "@modern-js/renderer-core/federation";
2
+ import { Suspense, createElement, decodeExternalSnapshot, externalSnapshotBoundary, hookSlots, lazy, preinit, publisherBoundary, serializeExternalSnapshotRequest, useContext, useEffect } from "octane";
3
3
  import { FederationContext } from "./federation-context.mjs";
4
+ import { I18nContext } from "./i18n/context.mjs";
5
+ import { WorkerFederationContext } from "./worker-federation-context.mjs";
6
+ import { useOctaneWorkerContextKeys } from "./worker-federation-contexts.mjs";
4
7
  function withTimeout(promise, timeout, label) {
5
8
  if (void 0 === timeout) return promise;
6
9
  return new Promise((resolve, reject)=>{
@@ -15,6 +18,7 @@ function withTimeout(promise, timeout, label) {
15
18
  });
16
19
  });
17
20
  }
21
+ const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
18
22
  function federatedComponent(remote, options = {}) {
19
23
  if ('string' == typeof remote ? !/^[^/]+\/.+/u.test(remote) : 'function' != typeof remote) throw new TypeError("federatedComponent requires a remote id such as 'remote/Widget' or a loader");
20
24
  if (void 0 !== options.timeout && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new TypeError('federatedComponent timeout must be a positive number of milliseconds');
@@ -24,19 +28,117 @@ function federatedComponent(remote, options = {}) {
24
28
  server: "u" < typeof document
25
29
  };
26
30
  const components = new WeakMap();
31
+ const workerOwners = new WeakMap();
27
32
  return (props)=>{
28
33
  const context = useContext(FederationContext) ?? standalone;
29
- let Remote = components.get(context);
34
+ const worker = useContext(WorkerFederationContext);
35
+ const language = worker ? useContext(I18nContext)?.language ?? worker.locale : void 0;
36
+ const contextKeys = worker ? useOctaneWorkerContextKeys(options.contextKeys) : void 0;
37
+ let owner = context;
38
+ if (worker) {
39
+ let scopes = workerOwners.get(context);
40
+ if (!scopes) workerOwners.set(context, scopes = new WeakMap());
41
+ let variants = scopes.get(worker);
42
+ if (!variants) scopes.set(worker, variants = new Map());
43
+ const key = JSON.stringify([
44
+ language,
45
+ contextKeys
46
+ ]);
47
+ let variant = variants.get(key);
48
+ if (!variant) variants.set(key, variant = {});
49
+ owner = variant;
50
+ }
51
+ let Remote = components.get(owner);
30
52
  if (!Remote) {
31
53
  Remote = lazy(async ()=>{
32
54
  try {
33
55
  if (context.server && !id) throw new Error('A federatedComponent loader renders in the browser only; use a remote id to server-render it');
34
- const module = await withTimeout(Promise.resolve().then(()=>id ? loadFederatedModule(context.instance, id) : remote()), options.timeout ?? (context.server ? 3000 : void 0), id ?? 'loader');
56
+ if (worker) {
57
+ if (!id || !language) throw new Error('Worker federation requires a remote id and the owning entry locale');
58
+ const separator = id.indexOf('/');
59
+ const remoteName = id.slice(0, separator);
60
+ const expose = `./${id.slice(separator + 1)}`;
61
+ const controller = new AbortController();
62
+ const signal = worker.signal ? AbortSignal.any([
63
+ worker.signal,
64
+ controller.signal
65
+ ]) : controller.signal;
66
+ const timeout = options.timeout ?? (context.server ? 3000 : void 0);
67
+ let admitted;
68
+ try {
69
+ admitted = await withTimeout(worker.transport.admit({
70
+ remote: remoteName,
71
+ expose,
72
+ locale: language,
73
+ signal
74
+ }), timeout, id);
75
+ } finally{
76
+ controller.abort();
77
+ }
78
+ const BrowserRemote = lazy(async ()=>{
79
+ const { module, assets } = await loadFederatedModule(context.instance, id);
80
+ if ('function' != typeof module?.default) throw new TypeError('A federated Octane module must default-export a native component');
81
+ const component = module.default;
82
+ const Loaded = (props, scope, extra)=>{
83
+ for (const href of assets?.css ?? [])preinit(href, {
84
+ as: 'style',
85
+ precedence: 'federated',
86
+ ...void 0 === context.nonce ? {} : {
87
+ nonce: context.nonce
88
+ }
89
+ });
90
+ return component(props, scope, extra);
91
+ };
92
+ return {
93
+ default: Loaded
94
+ };
95
+ });
96
+ const boundary = externalSnapshotBoundary({
97
+ authority: admitted.coordinate.native,
98
+ component: BrowserRemote,
99
+ contextKeys,
100
+ timeoutMs: timeout,
101
+ async snapshot (request, signal) {
102
+ const response = await worker.transport.request({
103
+ remote: remoteName,
104
+ expose,
105
+ locale: language,
106
+ payload: serializeExternalSnapshotRequest(request),
107
+ signal
108
+ });
109
+ return decodeExternalSnapshot(response.payload);
110
+ }
111
+ });
112
+ const Loaded = (props)=>{
113
+ for (const href of admitted.styles)preinit(href, {
114
+ as: 'style',
115
+ precedence: 'federated',
116
+ ...void 0 === context.nonce ? {} : {
117
+ nonce: context.nonce
118
+ }
119
+ });
120
+ return createElement(boundary, props);
121
+ };
122
+ return {
123
+ default: Loaded
124
+ };
125
+ }
126
+ const { module, assets } = await withTimeout(Promise.resolve().then(()=>id ? loadFederatedModule(context.instance, id) : remote().then((module)=>({
127
+ module,
128
+ assets: void 0
129
+ }))), options.timeout ?? (context.server ? 3000 : void 0), id ?? 'loader');
35
130
  if ('function' != typeof module?.default) throw new TypeError('A federated Octane module must default-export a native component');
36
- const assets = id && context.instance ? remoteBrowserAssets(context.instance, id) : void 0;
37
131
  if (context.server && !assets) throw new Error(`Cannot server-render ${id}: the host has no browser assets for this remote`);
38
- const component = module.default;
132
+ const component = publisherBoundary(module.default, {
133
+ publisherKey: id ?? 'ultramodern:browser-loader'
134
+ });
39
135
  const Loaded = (props)=>{
136
+ useEffect(()=>{
137
+ if (context.server || !id || !context.instance) return;
138
+ const register = Reflect.get(context.instance, Symbol.for('ultramodern.federation.browser-dev-boundary.v1'));
139
+ if ('function' != typeof register) return;
140
+ return register(id, module.default, component);
141
+ }, [], publisherRegistrationEffectSlot);
40
142
  for (const href of assets?.css ?? [])preinit(href, {
41
143
  as: 'style',
42
144
  precedence: 'federated',
@@ -50,11 +152,11 @@ function federatedComponent(remote, options = {}) {
50
152
  default: Loaded
51
153
  };
52
154
  } catch (error) {
53
- if (!context.server) components.delete(context);
155
+ if (!context.server) components.delete(owner);
54
156
  throw error;
55
157
  }
56
158
  });
57
- components.set(context, Remote);
159
+ components.set(owner, Remote);
58
160
  }
59
161
  return createElement(Suspense, {
60
162
  fallback: options.fallback,
@@ -1,48 +1,42 @@
1
- import { canonicalPath, configuredLanguage, localizePath } from "@modern-js/i18n-runtime-extensions/paths";
1
+ import { configuredLanguage, localizePath } from "@modern-js/i18n-runtime-extensions/paths";
2
2
  import { createElement, hookSlots, useContext } from "octane";
3
3
  import { Link, useLocation, useRouter } from "../router.mjs";
4
4
  import { I18nContext } from "./context.mjs";
5
- const locationSlot = Symbol(hookSlots(1));
5
+ const targetLocationSlot = Symbol(hookSlots(1));
6
6
  function LocalizedLink(props) {
7
7
  const context = useContext(I18nContext);
8
8
  if (!context) throw new Error('[renderer-octane] LocalizedLink was used outside of an I18nProvider.');
9
9
  const router = useRouter();
10
- const language = void 0 === props.language ? context.language : configuredLanguage(props.language, context.languages);
11
- const href = localizePath(props.to, language, {
10
+ const location = useLocation(void 0, targetLocationSlot);
11
+ const { language: requestedLanguage, to, children, ...options } = props;
12
+ const language = void 0 === requestedLanguage ? context.language : configuredLanguage(requestedLanguage, context.languages);
13
+ const externalTarget = to.startsWith('//') || URL.canParse(to);
14
+ const pathname = externalTarget ? to : localizePath(router.buildLocation({
15
+ to,
16
+ from: options.from,
17
+ params: options.params,
18
+ unsafeRelative: options.unsafeRelative,
19
+ _fromLocation: location
20
+ }).pathname, language, {
12
21
  languages: [
13
22
  ...context.languages
14
23
  ],
15
24
  localisedUrls: context.localisedUrls
16
25
  });
17
- const crossLanguage = language !== context.language;
18
- const rewrite = Boolean(router.options.rewrite);
19
26
  return createElement(Link, {
20
- to: rewrite ? props.to : href,
21
- ...rewrite && crossLanguage ? {
27
+ ...options,
28
+ to: router.options.rewrite ? to : pathname,
29
+ ...router.options.rewrite && !externalTarget ? {
22
30
  mask: {
23
- to: href
31
+ to: pathname,
32
+ inheritSearch: true,
33
+ hash: options.hash,
34
+ state: options.state
24
35
  }
25
36
  } : {},
26
- ...crossLanguage ? {
37
+ ...language === context.language ? {} : {
27
38
  hreflang: language
28
- } : {},
29
- class: props.class,
30
- activeProps: props.activeProps,
31
- inactiveProps: props.inactiveProps,
32
- activeOptions: props.activeOptions,
33
- replace: props.replace,
34
- preload: props.preload
35
- }, props.children);
36
- }
37
- function useIsLocalizedActive(canonicalTarget) {
38
- const context = useContext(I18nContext);
39
- if (!context) throw new Error('[renderer-octane] useIsLocalizedActive was used outside of an I18nProvider.');
40
- const location = useLocation(void 0, locationSlot);
41
- return canonicalPath(location.pathname, {
42
- languages: [
43
- ...context.languages
44
- ],
45
- localisedUrls: context.localisedUrls
46
- }) === canonicalTarget;
39
+ }
40
+ }, children);
47
41
  }
48
- export { LocalizedLink, useIsLocalizedActive };
42
+ export { LocalizedLink };
@@ -3,6 +3,6 @@ export { I18N_SSR_HANDOFF_ELEMENT_ID, createI18nSsrHandoffInlineData, readI18nSs
3
3
  export { createI18nUrlRewrite } from "@modern-js/i18n-runtime-extensions/urlRewrite";
4
4
  export { I18nContext } from "./context.mjs";
5
5
  export { I18nProvider } from "./I18nProvider.mjs";
6
- export { LocalizedLink, useIsLocalizedActive } from "./LocalizedLink.mjs";
6
+ export { LocalizedLink } from "./LocalizedLink.mjs";
7
7
  export { useLatestLanguageSync } from "./languageSync.mjs";
8
8
  export { useI18n } from "./useI18n.mjs";