@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
package/src/federation.ts CHANGED
@@ -1,21 +1,31 @@
1
- import {
2
- loadFederatedModule,
3
- remoteBrowserAssets,
4
- } from '@modern-js/renderer-core/federation';
1
+ import { loadFederatedModule } from '@modern-js/renderer-core/federation';
5
2
  import {
6
3
  type ComponentBody,
7
4
  createElement,
5
+ decodeExternalSnapshot,
6
+ externalSnapshotBoundary,
7
+ hookSlots,
8
8
  lazy,
9
9
  type OctaneNode,
10
10
  preinit,
11
+ publisherBoundary,
11
12
  Suspense,
13
+ serializeExternalSnapshotRequest,
12
14
  useContext,
15
+ useEffect,
13
16
  } from 'octane';
14
17
  import {
15
18
  FederationContext,
16
19
  type OctaneFederationScope,
17
20
  } from './federation-context';
18
21
 
22
+ import { I18nContext } from './i18n/context';
23
+ import {
24
+ type OctaneWorkerFederationScope,
25
+ WorkerFederationContext,
26
+ } from './worker-federation-context';
27
+ import { useOctaneWorkerContextKeys } from './worker-federation-contexts';
28
+
19
29
  /** The native component shape exposed by a same-renderer Octane remote. */
20
30
  export interface FederatedModule<P extends object> {
21
31
  readonly default: ComponentBody<P>;
@@ -26,6 +36,8 @@ export interface FederatedComponentOptions {
26
36
  readonly fallback?: () => OctaneNode;
27
37
  /** A positive load limit; the server defaults to 3000ms. */
28
38
  readonly timeout?: number;
39
+ /** Explicit registered server projections required by an application remote. */
40
+ readonly contextKeys?: readonly string[];
29
41
  }
30
42
 
31
43
  function withTimeout<T>(
@@ -56,6 +68,9 @@ function withTimeout<T>(
56
68
  });
57
69
  }
58
70
 
71
+ // This library wrapper is published as plain JavaScript, outside app compilation.
72
+ const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
73
+
59
74
  /**
60
75
  * Render a native Octane remote by its `remote/Expose` id or a browser loader.
61
76
  * Native Suspense owns server fallbacks, streamed styles and hydration adoption;
@@ -90,11 +105,33 @@ export function federatedComponent<P extends object = Record<string, never>>(
90
105
  };
91
106
  // Native lazy caches its module. Scope the cache to one application/response,
92
107
  // so two hosts using the same component definition cannot share a remote.
93
- const components = new WeakMap<OctaneFederationScope, ComponentBody<P>>();
108
+ const components = new WeakMap<object, ComponentBody<P>>();
109
+ const workerOwners = new WeakMap<
110
+ OctaneFederationScope,
111
+ WeakMap<OctaneWorkerFederationScope, Map<string, object>>
112
+ >();
94
113
 
95
114
  return props => {
96
115
  const context = useContext(FederationContext) ?? standalone;
97
- let Remote = components.get(context);
116
+ const worker = useContext(WorkerFederationContext);
117
+ const language = worker
118
+ ? (useContext(I18nContext)?.language ?? worker.locale)
119
+ : undefined;
120
+ const contextKeys = worker
121
+ ? useOctaneWorkerContextKeys(options.contextKeys)
122
+ : undefined;
123
+ let owner: object = context;
124
+ if (worker) {
125
+ let scopes = workerOwners.get(context);
126
+ if (!scopes) workerOwners.set(context, (scopes = new WeakMap()));
127
+ let variants = scopes.get(worker);
128
+ if (!variants) scopes.set(worker, (variants = new Map()));
129
+ const key = JSON.stringify([language, contextKeys]);
130
+ let variant = variants.get(key);
131
+ if (!variant) variants.set(key, (variant = {}));
132
+ owner = variant;
133
+ }
134
+ let Remote = components.get(owner);
98
135
  if (!Remote) {
99
136
  Remote = lazy(async () => {
100
137
  try {
@@ -103,11 +140,101 @@ export function federatedComponent<P extends object = Record<string, never>>(
103
140
  'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
104
141
  );
105
142
  }
106
- const module = await withTimeout(
143
+ if (worker) {
144
+ if (!id || !language) {
145
+ throw new Error(
146
+ 'Worker federation requires a remote id and the owning entry locale',
147
+ );
148
+ }
149
+ const separator = id.indexOf('/');
150
+ const remoteName = id.slice(0, separator);
151
+ const expose = `./${id.slice(separator + 1)}`;
152
+ const controller = new AbortController();
153
+ const signal = worker.signal
154
+ ? AbortSignal.any([worker.signal, controller.signal])
155
+ : controller.signal;
156
+ const timeout =
157
+ options.timeout ?? (context.server ? 3000 : undefined);
158
+ let admitted;
159
+ try {
160
+ admitted = await withTimeout(
161
+ worker.transport.admit({
162
+ remote: remoteName,
163
+ expose,
164
+ locale: language,
165
+ signal,
166
+ }),
167
+ timeout,
168
+ id,
169
+ );
170
+ } finally {
171
+ // Release this admission waiter, including after its own deadline.
172
+ controller.abort();
173
+ }
174
+ const BrowserRemote = lazy(async () => {
175
+ const { module, assets } = await loadFederatedModule<
176
+ FederatedModule<P>
177
+ >(context.instance, id);
178
+ if (typeof module?.default !== 'function') {
179
+ throw new TypeError(
180
+ 'A federated Octane module must default-export a native component',
181
+ );
182
+ }
183
+ const component = module.default;
184
+ const Loaded: ComponentBody<P> = (props, scope, extra) => {
185
+ for (const href of assets?.css ?? []) {
186
+ preinit(href, {
187
+ as: 'style',
188
+ precedence: 'federated',
189
+ ...(context.nonce === undefined
190
+ ? {}
191
+ : { nonce: context.nonce }),
192
+ });
193
+ }
194
+ return component(props, scope, extra);
195
+ };
196
+ return { default: Loaded };
197
+ });
198
+ const boundary = externalSnapshotBoundary({
199
+ authority: admitted.coordinate.native,
200
+ component: BrowserRemote,
201
+ contextKeys,
202
+ timeoutMs: timeout,
203
+ async snapshot(request, signal) {
204
+ const response = await worker.transport.request({
205
+ remote: remoteName,
206
+ expose,
207
+ locale: language,
208
+ payload: serializeExternalSnapshotRequest(request),
209
+ signal,
210
+ });
211
+ return decodeExternalSnapshot(response.payload);
212
+ },
213
+ });
214
+ const Loaded: ComponentBody<P> = props => {
215
+ // A Worker server does not execute BrowserRemote. Register the
216
+ // exact admitted expose's extracted CSS in this active native
217
+ // render; nested snapshots carry these hints through native head.
218
+ for (const href of admitted.styles) {
219
+ preinit(href, {
220
+ as: 'style',
221
+ precedence: 'federated',
222
+ ...(context.nonce === undefined
223
+ ? {}
224
+ : { nonce: context.nonce }),
225
+ });
226
+ }
227
+ return createElement(boundary, props);
228
+ };
229
+ return { default: Loaded };
230
+ }
231
+ const { module, assets } = await withTimeout(
107
232
  Promise.resolve().then(() =>
108
233
  id
109
234
  ? loadFederatedModule<FederatedModule<P>>(context.instance, id)
110
- : (remote as () => Promise<FederatedModule<P>>)(),
235
+ : (remote as () => Promise<FederatedModule<P>>)().then(
236
+ module => ({ module, assets: undefined }),
237
+ ),
111
238
  ),
112
239
  options.timeout ?? (context.server ? 3000 : undefined),
113
240
  id ?? 'loader',
@@ -117,17 +244,34 @@ export function federatedComponent<P extends object = Record<string, never>>(
117
244
  'A federated Octane module must default-export a native component',
118
245
  );
119
246
  }
120
- const assets =
121
- id && context.instance
122
- ? remoteBrowserAssets(context.instance, id)
123
- : undefined;
124
247
  if (context.server && !assets) {
125
248
  throw new Error(
126
249
  `Cannot server-render ${id}: the host has no browser assets for this remote`,
127
250
  );
128
251
  }
129
- const component = module.default;
252
+ // Native publisher ownership admits this exact compiled component's
253
+ // signal generations. Keep the boundary in the context-scoped lazy
254
+ // cache so props and compatible hot updates retain its live owner.
255
+ const component = publisherBoundary(module.default, {
256
+ publisherKey: id ?? 'ultramodern:browser-loader',
257
+ });
130
258
  const Loaded: ComponentBody<P> = props => {
259
+ // A development runtime installs this hook on the actual owning
260
+ // host. Registration follows the native mounted lifetime; routing
261
+ // alone grants no replacement authority to a remote name.
262
+ useEffect(
263
+ () => {
264
+ if (context.server || !id || !context.instance) return;
265
+ const register = Reflect.get(
266
+ context.instance,
267
+ Symbol.for('ultramodern.federation.browser-dev-boundary.v1'),
268
+ );
269
+ if (typeof register !== 'function') return;
270
+ return register(id, module.default, component);
271
+ },
272
+ [],
273
+ publisherRegistrationEffectSlot,
274
+ );
131
275
  // Resource APIs require an active native render. Late styles travel
132
276
  // in Octane's resource carrier ahead of the remote's HTML segment.
133
277
  for (const href of assets?.css ?? []) {
@@ -144,11 +288,11 @@ export function federatedComponent<P extends object = Record<string, never>>(
144
288
  return { default: Loaded };
145
289
  } catch (error) {
146
290
  // A later mount or ErrorBoundary reset may retry a failed load.
147
- if (!context.server) components.delete(context);
291
+ if (!context.server) components.delete(owner);
148
292
  throw error;
149
293
  }
150
294
  });
151
- components.set(context, Remote);
295
+ components.set(owner, Remote);
152
296
  }
153
297
  return createElement(Suspense, {
154
298
  fallback: options.fallback,
@@ -1,10 +1,10 @@
1
1
  import {
2
- canonicalPath,
3
2
  configuredLanguage,
4
3
  localizePath,
5
4
  } from '@modern-js/i18n-runtime-extensions/paths';
6
5
  import { createElement, hookSlots, type OctaneNode, useContext } from 'octane';
7
6
  import {
7
+ type AnyRouter,
8
8
  Link,
9
9
  type LinkComponentProps,
10
10
  useLocation,
@@ -14,41 +14,23 @@ import { I18nContext } from './context';
14
14
 
15
15
  // Plain TypeScript is not rewritten by the Octane compiler: router hooks take
16
16
  // their stable slot explicitly.
17
- const locationSlot = Symbol(hookSlots(1));
17
+ const targetLocationSlot = Symbol(hookSlots(1));
18
18
 
19
- export interface LocalizedLinkProps {
19
+ export interface LocalizedLinkProps
20
+ extends Omit<
21
+ LinkComponentProps<'a', AnyRouter, string, string>,
22
+ 'to' | 'mask' | 'href' | 'publicHref' | '_fromLocation'
23
+ > {
20
24
  /** A canonical, language-agnostic target, e.g. `/products/red-shoe`. */
21
25
  to: string;
22
26
  /** Localize for a specific language instead of the current one. */
23
27
  language?: string;
24
- children?: unknown;
25
- class?: string;
26
- activeProps?: LinkComponentProps<'a'>['activeProps'];
27
- inactiveProps?: LinkComponentProps<'a'>['inactiveProps'];
28
- activeOptions?: LinkComponentProps<'a'>['activeOptions'];
29
- replace?: boolean;
30
- preload?: LinkComponentProps<'a'>['preload'];
31
28
  }
32
29
 
33
30
  /**
34
- * A thin wrapper over the native Octane router `Link`
35
- * (`@octanejs/tanstack-router`'s precompiled `Link.tsrx`) that localizes a
36
- * canonical `to` for the current (or an explicit) language before handing it
37
- * to the real link — the same contract as `@modern-js/plugin-i18n`'s React
38
- * `Link`. It resolves the href itself via the shared pathname helpers, so it
39
- * works with or without the router's i18n `rewrite`. Every link, including
40
- * one to another language, is the native `Link`: navigation, preloading and
41
- * active state stay with the router.
42
- *
43
- * Under the i18n `rewrite` the router matches canonical paths and localizes
44
- * outgoing locations to the current language, so a link to another language
45
- * routes to the canonical `to` and publishes the target language's URL as a
46
- * route mask (the rewrite keeps a mask's explicit language). The entry's
47
- * router language synchronization (`syncWithRouter`) switches the i18next
48
- * instance from the navigated URL, as it does for history navigation.
49
- *
50
- * No JSX: built with `createElement`, matching this package's existing
51
- * style (see `src/routes.ts`).
31
+ * The native router owns client navigation, preloading and active state. Its mask
32
+ * keeps the canonical route while publishing the selected language URL;
33
+ * the entry's i18n router subscription synchronizes the instance on navigation.
52
34
  */
53
35
  export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
54
36
  const context = useContext(I18nContext);
@@ -58,55 +40,47 @@ export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
58
40
  );
59
41
  }
60
42
  const router = useRouter();
61
-
62
- // An explicit language must be configured: it becomes the locale prefix.
43
+ const location = useLocation(undefined, targetLocationSlot);
44
+ const { language: requestedLanguage, to, children, ...options } = props;
63
45
  const language =
64
- props.language === undefined
46
+ requestedLanguage === undefined
65
47
  ? context.language
66
- : configuredLanguage(props.language, context.languages);
67
- const href = localizePath(props.to, language, {
68
- languages: [...context.languages],
69
- localisedUrls: context.localisedUrls,
70
- });
71
- const crossLanguage = language !== context.language;
72
- const rewrite = Boolean(router.options.rewrite);
48
+ : configuredLanguage(requestedLanguage, context.languages);
49
+ const externalTarget = to.startsWith('//') || URL.canParse(to);
50
+ const pathname = externalTarget
51
+ ? to
52
+ : localizePath(
53
+ router.buildLocation({
54
+ to,
55
+ from: options.from,
56
+ params: options.params,
57
+ unsafeRelative: options.unsafeRelative,
58
+ _fromLocation: location,
59
+ }).pathname,
60
+ language,
61
+ {
62
+ languages: [...context.languages],
63
+ localisedUrls: context.localisedUrls,
64
+ },
65
+ );
73
66
 
74
67
  return createElement(
75
- Link,
68
+ Link<AnyRouter, string, string, string, string>,
76
69
  {
77
- // Octane's `Link` builds its location from `to` alone. Under the i18n
78
- // router rewrite the router matches canonical paths and localizes the
79
- // public href itself; without it, the localized path is the route path.
80
- to: rewrite ? props.to : href,
81
- ...(rewrite && crossLanguage ? { mask: { to: href } } : {}),
82
- ...(crossLanguage ? { hreflang: language } : {}),
83
- class: props.class,
84
- activeProps: props.activeProps,
85
- inactiveProps: props.inactiveProps,
86
- activeOptions: props.activeOptions,
87
- replace: props.replace,
88
- preload: props.preload,
70
+ ...options,
71
+ to: router.options.rewrite ? to : pathname,
72
+ ...(router.options.rewrite && !externalTarget
73
+ ? {
74
+ mask: {
75
+ to: pathname,
76
+ inheritSearch: true,
77
+ hash: options.hash,
78
+ state: options.state,
79
+ },
80
+ }
81
+ : {}),
82
+ ...(language === context.language ? {} : { hreflang: language }),
89
83
  },
90
- props.children,
91
- );
92
- }
93
-
94
- /**
95
- * Language-invariant active check: `to="/products"` is active whether the
96
- * current location is `/en/products` or `/produkty` under a mapped locale.
97
- */
98
- export function useIsLocalizedActive(canonicalTarget: string): boolean {
99
- const context = useContext(I18nContext);
100
- if (!context) {
101
- throw new Error(
102
- '[renderer-octane] useIsLocalizedActive was used outside of an I18nProvider.',
103
- );
104
- }
105
- const location = useLocation(undefined, locationSlot);
106
- return (
107
- canonicalPath(location.pathname, {
108
- languages: [...context.languages],
109
- localisedUrls: context.localisedUrls,
110
- }) === canonicalTarget
84
+ children,
111
85
  );
112
86
  }
@@ -6,7 +6,7 @@ export type { I18nContextValue, I18nInstanceLike } from './types';
6
6
  /**
7
7
  * Octane's `createContext<T>(defaultValue: T)` requires a default (unlike
8
8
  * Solid 2's default-less form), so a missing provider is detected by
9
- * checking for `null` in `useI18n`/`useIsLocalizedActive` instead — the same
9
+ * checking for `null` in `useI18n`/`LocalizedLink` instead — the same
10
10
  * guard shape `@modern-js/plugin-i18n`'s React `ModernI18nContext` uses.
11
11
  */
12
12
  export const I18nContext = createContext<I18nContextValue | null>(null);
package/src/i18n/index.ts CHANGED
@@ -22,11 +22,7 @@ export {
22
22
  } from '@modern-js/i18n-runtime-extensions/urlRewrite';
23
23
  export { I18nContext } from './context';
24
24
  export { I18nProvider, type I18nProviderProps } from './I18nProvider';
25
- export {
26
- LocalizedLink,
27
- type LocalizedLinkProps,
28
- useIsLocalizedActive,
29
- } from './LocalizedLink';
25
+ export { LocalizedLink, type LocalizedLinkProps } from './LocalizedLink';
30
26
  export {
31
27
  type OctaneLatestLanguageSyncOptions,
32
28
  useLatestLanguageSync,