@bleedingdev/modern-js-renderer-octane 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 (100) 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 +13 -5
  5. package/dist/cjs/federation.js +108 -6
  6. package/dist/cjs/i18n/I18nProvider.js +6 -14
  7. package/dist/cjs/i18n/LocalizedLink.js +24 -33
  8. package/dist/cjs/i18n/index.js +0 -3
  9. package/dist/cjs/i18n-external-snapshot.js +645 -0
  10. package/dist/cjs/index.js +4 -4
  11. package/dist/cjs/manifest.js +2 -2
  12. package/dist/cjs/plugin/compiler.js +2 -5
  13. package/dist/cjs/plugin/create.js +27 -26
  14. package/dist/cjs/plugin/index.js +2 -2
  15. package/dist/cjs/plugin/profile.js +12 -10
  16. package/dist/cjs/router-client.js +1 -1
  17. package/dist/cjs/router-external-snapshot.js +291 -0
  18. package/dist/cjs/router-handler.js +11 -8
  19. package/dist/cjs/router-server.js +3 -11
  20. package/dist/cjs/router.js +1 -1
  21. package/dist/cjs/routes.js +20 -17
  22. package/dist/cjs/server.js +28 -7
  23. package/dist/cjs/worker-federation-context.js +63 -0
  24. package/dist/cjs/worker-federation-contexts.js +97 -0
  25. package/dist/cjs/worker-federation.js +97 -0
  26. package/dist/esm/client.mjs +56 -6
  27. package/dist/esm/entry-application.mjs +7 -2
  28. package/dist/esm/entry-client.mjs +3 -0
  29. package/dist/esm/entry-server.mjs +11 -6
  30. package/dist/esm/federation.mjs +110 -8
  31. package/dist/esm/i18n/I18nProvider.mjs +7 -15
  32. package/dist/esm/i18n/LocalizedLink.mjs +24 -30
  33. package/dist/esm/i18n/index.mjs +1 -1
  34. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  35. package/dist/esm/index.mjs +4 -4
  36. package/dist/esm/manifest.mjs +2 -2
  37. package/dist/esm/router-client.mjs +1 -1
  38. package/dist/esm/router-external-snapshot.mjs +244 -0
  39. package/dist/esm/router-handler.mjs +11 -8
  40. package/dist/esm/router-server.mjs +0 -1
  41. package/dist/esm/router.mjs +2 -2
  42. package/dist/esm/routes.mjs +18 -18
  43. package/dist/esm/server.mjs +24 -3
  44. package/dist/esm/worker-federation-context.mjs +19 -0
  45. package/dist/esm/worker-federation-contexts.mjs +47 -0
  46. package/dist/esm/worker-federation.mjs +59 -0
  47. package/dist/esm-node/manifest.mjs +2 -2
  48. package/dist/esm-node/plugin/compiler.mjs +2 -5
  49. package/dist/esm-node/plugin/create.mjs +27 -26
  50. package/dist/esm-node/plugin/index.mjs +2 -2
  51. package/dist/esm-node/plugin/profile.mjs +12 -10
  52. package/dist/types/client.d.ts +5 -1
  53. package/dist/types/entry-client.d.ts +2 -0
  54. package/dist/types/entry-server.d.ts +4 -1
  55. package/dist/types/federation.d.ts +2 -0
  56. package/dist/types/i18n/LocalizedLink.d.ts +5 -32
  57. package/dist/types/i18n/context.d.ts +1 -1
  58. package/dist/types/i18n/index.d.ts +1 -1
  59. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  60. package/dist/types/index.d.ts +4 -4
  61. package/dist/types/manifest.d.ts +2 -2
  62. package/dist/types/router-external-snapshot.d.ts +10 -0
  63. package/dist/types/router-server.d.ts +0 -2
  64. package/dist/types/routes.d.ts +2 -0
  65. package/dist/types/server.d.ts +6 -0
  66. package/dist/types/worker-federation-context.d.ts +34 -0
  67. package/dist/types/worker-federation-contexts.d.ts +25 -0
  68. package/dist/types/worker-federation.d.ts +20 -0
  69. package/package.json +84 -22
  70. package/src/client.ts +99 -16
  71. package/src/entry-application.ts +7 -3
  72. package/src/entry-client.ts +10 -0
  73. package/src/entry-server.ts +20 -9
  74. package/src/federation.ts +164 -15
  75. package/src/i18n/I18nProvider.ts +11 -20
  76. package/src/i18n/LocalizedLink.ts +46 -72
  77. package/src/i18n/context.ts +1 -1
  78. package/src/i18n/index.ts +1 -5
  79. package/src/i18n-external-snapshot.ts +803 -0
  80. package/src/index.ts +4 -4
  81. package/src/manifest.ts +2 -2
  82. package/src/plugin/compiler-manifest.cjs +27 -1
  83. package/src/plugin/compiler.ts +6 -14
  84. package/src/plugin/create.ts +27 -26
  85. package/src/plugin/index.ts +4 -2
  86. package/src/plugin/profile.ts +14 -10
  87. package/src/router-client.ts +1 -1
  88. package/src/router-external-snapshot.ts +363 -0
  89. package/src/router-handler.ts +11 -8
  90. package/src/router-server.ts +0 -2
  91. package/src/router.ts +2 -2
  92. package/src/routes.ts +37 -26
  93. package/src/server.ts +45 -5
  94. package/src/worker-federation-context.ts +51 -0
  95. package/src/worker-federation-contexts.ts +91 -0
  96. package/src/worker-federation.ts +120 -0
  97. package/dist/cjs/router-injection.js +0 -139
  98. package/dist/esm/router-injection.mjs +0 -101
  99. package/dist/types/router-injection.d.ts +0 -8
  100. package/src/router-injection.ts +0 -145
@@ -0,0 +1,244 @@
1
+ import { associateRouterRenderProfile, captureRouterRenderSnapshot, createRouterRenderRouteTree, defaultParseSearch, defaultStringifySearch, disposeRouterRenderSnapshot, getRouterRenderProfile, matchContext, registerRouterRenderProfile, restoreRouterRenderSnapshot, routerContext } from "@octanejs/tanstack-router";
2
+ import { hasUnchangedOctaneFileRouteHead } from "./routes.mjs";
3
+ import { OCTANE_WORKER_CONTEXT_KEYS, getOctaneWorkerRouterRewrite, registerOctaneWorkerContext } from "./worker-federation-contexts.mjs";
4
+ const OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE = Object.freeze({
5
+ key: 'ultramodern.file-routes',
6
+ version: '1'
7
+ });
8
+ const origins = new WeakMap();
9
+ let profileRegistered = false;
10
+ function plainRecord(value) {
11
+ return null !== value && 'object' == typeof value && [
12
+ Object.prototype,
13
+ null
14
+ ].includes(Object.getPrototypeOf(value));
15
+ }
16
+ function record(value, fields) {
17
+ if (!plainRecord(value)) throw new TypeError('Worker router context requires a plain record');
18
+ for (const key of Reflect.ownKeys(value)){
19
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
20
+ if ('string' != typeof key || fields && !fields.includes(key) || !descriptor || !descriptor.enumerable || !('value' in descriptor)) throw new TypeError('Unknown Worker router context field or accessor');
21
+ }
22
+ return value;
23
+ }
24
+ function nonempty(value) {
25
+ if ('string' != typeof value || !value.trim() || value.length > 1024) throw new TypeError('Invalid Worker router identity');
26
+ return value;
27
+ }
28
+ function publicContext(value) {
29
+ const context = record(value, [
30
+ 'ultramodern'
31
+ ]);
32
+ const metadata = record(context.ultramodern, [
33
+ 'rendererIdentity'
34
+ ]);
35
+ const identity = record(metadata.rendererIdentity, [
36
+ 'renderer',
37
+ 'appId',
38
+ 'entryName',
39
+ 'protocolVersion',
40
+ 'buildId'
41
+ ]);
42
+ if ('octane' !== identity.renderer || 1 !== identity.protocolVersion) throw new TypeError('Unsupported Worker router identity');
43
+ return Object.freeze({
44
+ ultramodern: Object.freeze({
45
+ rendererIdentity: Object.freeze({
46
+ renderer: 'octane',
47
+ appId: nonempty(identity.appId),
48
+ entryName: nonempty(identity.entryName),
49
+ protocolVersion: 1,
50
+ buildId: nonempty(identity.buildId)
51
+ })
52
+ })
53
+ });
54
+ }
55
+ function localizedUrls(value) {
56
+ if (void 0 === value || 'boolean' == typeof value) return value;
57
+ const input = record(value);
58
+ const result = {};
59
+ for (const [path, translations] of Object.entries(input)){
60
+ const names = {};
61
+ for (const [language, target] of Object.entries(record(translations)))names[nonempty(language)] = nonempty(target);
62
+ result[path] = names;
63
+ }
64
+ return result;
65
+ }
66
+ function rewriteDescriptor(value) {
67
+ if (void 0 === value) return;
68
+ const input = record(value, [
69
+ 'language',
70
+ 'languages',
71
+ 'localisedUrls'
72
+ ]);
73
+ if (!Array.isArray(input.languages) || !input.languages.length || input.languages.length > 64) throw new TypeError('Invalid Worker router i18n languages');
74
+ const languages = input.languages.map(nonempty);
75
+ const language = nonempty(input.language);
76
+ if (new Set(languages).size !== languages.length || !languages.includes(language)) throw new TypeError('Invalid Worker router i18n language');
77
+ const urls = localizedUrls(input.localisedUrls);
78
+ return {
79
+ language,
80
+ languages,
81
+ ...void 0 === urls ? {} : {
82
+ localisedUrls: urls
83
+ }
84
+ };
85
+ }
86
+ function nativeRewriteDescriptor(router) {
87
+ if (!router.options.rewrite) return;
88
+ const known = getOctaneWorkerRouterRewrite()?.capture(router.options.rewrite);
89
+ if (!known) throw new TypeError('Custom router rewrite requires an exact Worker profile');
90
+ const descriptor = rewriteDescriptor(known);
91
+ if (!descriptor) throw new TypeError('Missing native router rewrite provenance');
92
+ return {
93
+ language: descriptor.language,
94
+ languages: [
95
+ ...descriptor.languages
96
+ ],
97
+ ...void 0 === descriptor.localisedUrls ? {} : {
98
+ localisedUrls: descriptor.localisedUrls
99
+ }
100
+ };
101
+ }
102
+ function validateDefaultSnapshot(snapshot) {
103
+ publicContext(snapshot.publicContext);
104
+ rewriteDescriptor(snapshot.i18n);
105
+ }
106
+ function assertFileRouteRenderHost(router, allowRewrite) {
107
+ if (router.options.parseSearch !== defaultParseSearch || router.options.stringifySearch !== defaultStringifySearch || !allowRewrite && router.options.rewrite || router.options.routeMasks?.length) throw new TypeError('Router render snapshot: authored router behavior requires an exact local profile');
108
+ const authoredRouting = [
109
+ 'caseSensitive',
110
+ 'validateSearch',
111
+ 'loaderDeps',
112
+ 'beforeLoad',
113
+ 'context',
114
+ 'onEnter',
115
+ 'onStay',
116
+ 'onLeave',
117
+ 'lazyFn',
118
+ "scripts",
119
+ 'headers',
120
+ 'onError',
121
+ 'parseParams',
122
+ 'stringifyParams'
123
+ ];
124
+ const seen = new Set();
125
+ const visit = (route, depth)=>{
126
+ if (seen.has(route) || seen.size >= 4096 || depth > 64) throw new TypeError('Router render snapshot: route geometry must be bounded and acyclic');
127
+ seen.add(route);
128
+ if (authoredRouting.some((key)=>void 0 !== Reflect.get(route.options, key)) || route.options.params?.parse || route.options.params?.stringify || route.options.search?.middlewares?.length || route.lazyFn || 'path' in route.options && 'id' in route.options || !hasUnchangedOctaneFileRouteHead(route)) throw new TypeError('Router render snapshot: authored route behavior requires an exact local profile');
129
+ for (const child of route.children ?? [])visit(child, depth + 1);
130
+ };
131
+ visit(router.options.routeTree, 0);
132
+ }
133
+ function ensureDefaultProfile() {
134
+ if (profileRegistered) return;
135
+ registerRouterRenderProfile({
136
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
137
+ assertHost: (router, snapshot)=>{
138
+ const origin = origins.get(router);
139
+ if (!origin || origin.rewrite !== router.options.rewrite || JSON.stringify(publicContext(router.options.context)) !== JSON.stringify(origin.publicContext)) throw new TypeError('Generated Worker router origin changed');
140
+ assertFileRouteRenderHost(router, void 0 !== snapshot.i18n);
141
+ validateDefaultSnapshot(snapshot);
142
+ if (JSON.stringify(nativeRewriteDescriptor(router)) !== JSON.stringify(snapshot.i18n)) throw new TypeError('Worker router rewrite differs from its native provenance');
143
+ },
144
+ createRoutingTree: (snapshot)=>{
145
+ validateDefaultSnapshot(snapshot);
146
+ return createRouterRenderRouteTree(snapshot.routes);
147
+ },
148
+ routingOptions: (snapshot)=>{
149
+ const defaults = {
150
+ parseSearch: defaultParseSearch,
151
+ stringifySearch: defaultStringifySearch
152
+ };
153
+ const i18n = rewriteDescriptor(snapshot.i18n);
154
+ if (!i18n) return defaults;
155
+ const codec = getOctaneWorkerRouterRewrite();
156
+ if (!codec) throw new TypeError('Missing owning Worker router i18n codec');
157
+ return {
158
+ ...defaults,
159
+ rewrite: codec.restore(i18n)
160
+ };
161
+ }
162
+ });
163
+ profileRegistered = true;
164
+ }
165
+ function associateOctaneFileRouteSnapshot(router) {
166
+ ensureDefaultProfile();
167
+ const context = publicContext(router.options.context);
168
+ origins.set(router, {
169
+ publicContext: context,
170
+ rewrite: router.options.rewrite
171
+ });
172
+ associateRouterRenderProfile(router, {
173
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
174
+ publicContext: context
175
+ });
176
+ }
177
+ function captureOctaneRouterSnapshot(router) {
178
+ const origin = origins.get(router);
179
+ const declared = getRouterRenderProfile(router);
180
+ if (origin && declared?.key === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.key && declared.version === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.version) {
181
+ const i18n = nativeRewriteDescriptor(router);
182
+ associateRouterRenderProfile(router, {
183
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
184
+ publicContext: origin.publicContext,
185
+ ...void 0 === i18n ? {} : {
186
+ i18n
187
+ }
188
+ });
189
+ }
190
+ return captureRouterRenderSnapshot(router);
191
+ }
192
+ let registrations = 0;
193
+ let releaseContexts;
194
+ function registerOctaneRouterWorkerContexts() {
195
+ ensureDefaultProfile();
196
+ if (!releaseContexts) {
197
+ const releaseRouter = registerOctaneWorkerContext(routerContext, {
198
+ key: OCTANE_WORKER_CONTEXT_KEYS.router,
199
+ encode: (value)=>{
200
+ if (!value) throw new TypeError('Missing native router provider');
201
+ return captureOctaneRouterSnapshot(value);
202
+ },
203
+ decode: (value, signal)=>restoreRouterRenderSnapshot(value, {
204
+ signal
205
+ }),
206
+ dispose: (value)=>{
207
+ if (value) disposeRouterRenderSnapshot(value);
208
+ },
209
+ isProvided: (value)=>void 0 !== value
210
+ });
211
+ try {
212
+ const releaseMatch = registerOctaneWorkerContext(matchContext, {
213
+ key: OCTANE_WORKER_CONTEXT_KEYS.match,
214
+ encode: nonempty,
215
+ decode: nonempty,
216
+ validate: (value, readContext)=>{
217
+ const selected = readContext(routerContext);
218
+ const router = selected.present ? selected.value : void 0;
219
+ const current = router?.state.matches.find((match)=>match.routeId === value);
220
+ if (!router || !current || 'string' != typeof value || router.stores.getMatchStore(value).get() !== current) throw new TypeError('Worker match context requires its selected native router and a current match');
221
+ },
222
+ isProvided: (value)=>void 0 !== value
223
+ });
224
+ releaseContexts = ()=>{
225
+ releaseMatch();
226
+ releaseRouter();
227
+ };
228
+ } catch (error) {
229
+ releaseRouter();
230
+ throw error;
231
+ }
232
+ }
233
+ registrations++;
234
+ let released = false;
235
+ return ()=>{
236
+ if (released) return;
237
+ released = true;
238
+ if (0 === --registrations) {
239
+ releaseContexts?.();
240
+ releaseContexts = void 0;
241
+ }
242
+ };
243
+ }
244
+ export { OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE, associateOctaneFileRouteSnapshot, captureOctaneRouterSnapshot, registerOctaneRouterWorkerContexts };
@@ -52,8 +52,8 @@ function requestHeaders(router) {
52
52
  if (field?.get || field?.set) throw new TypeError('Native Octane response headers must be owned values');
53
53
  appendHeaders(headers, field?.value);
54
54
  }
55
- const redirect = router.stores.redirect.get();
56
- if (redirect) appendHeaders(headers, redirect.headers);
55
+ const result = router._serverResult;
56
+ if (result?.type === 'redirect') appendHeaders(headers, result.redirect.headers);
57
57
  return headers;
58
58
  }
59
59
  function createOctaneRequestHandler({ createRouter, request, session, getRouterManifest, serialization }) {
@@ -74,7 +74,6 @@ function createOctaneRequestHandler({ createRouter, request, session, getRouterM
74
74
  ...serialization?.forbiddenValues ?? []
75
75
  ]
76
76
  });
77
- session.registerCleanup(()=>router.cancelMatches());
78
77
  const { url } = getNormalizedURL(request.url, 'http://localhost');
79
78
  const href = url.href.replace(url.origin, '');
80
79
  router.update({
@@ -86,11 +85,13 @@ function createOctaneRequestHandler({ createRouter, request, session, getRouterM
86
85
  }),
87
86
  origin: router.options.origin ?? getOrigin(request)
88
87
  });
89
- await withAbort(router.load(), session.signal);
88
+ await withAbort(router.load({
89
+ _signal: session.signal
90
+ }), session.signal);
90
91
  guard.assertActive();
91
- const redirect = router.stores.redirect.get();
92
- if (redirect) {
93
- const resolved = router.resolveRedirect(redirect);
92
+ const loadResult = router._serverResult;
93
+ if (loadResult?.type === 'redirect') {
94
+ const resolved = loadResult.redirect;
94
95
  const headers = requestHeaders(router);
95
96
  return new Response(null, {
96
97
  status: resolved.status,
@@ -101,7 +102,9 @@ function createOctaneRequestHandler({ createRouter, request, session, getRouterM
101
102
  guard.assertMatches();
102
103
  const serverSsr = router.serverSsr;
103
104
  if (!serverSsr) throw new Error('Native Octane router SSR state is required');
104
- await withAbort(serverSsr.dehydrate(), session.signal);
105
+ await withAbort(serverSsr.dehydrate({
106
+ signal: session.signal
107
+ }), session.signal);
105
108
  guard.assertActive();
106
109
  const responseHeaders = requestHeaders(router);
107
110
  const rendered = Promise.resolve(callback({
@@ -7,6 +7,5 @@ function OctaneRouterServer({ router }) {
7
7
  });
8
8
  }
9
9
  export { createOctaneRequestHandler } from "./router-handler.mjs";
10
- export { createOctaneRouterInjection } from "./router-injection.mjs";
11
10
  export { prepareOctaneRouterSerialization } from "./router-server-snapshot.mjs";
12
11
  export { OctaneRouterServer };
@@ -1,6 +1,6 @@
1
1
  import { createDataClient } from "@modern-js/renderer-core/data";
2
2
  import { RouterProvider, createRouter, redirect, useMatch, useRouter } from "@octanejs/tanstack-router";
3
- import { createSubSlot, useCallback, useEffect, useRef } from "octane";
3
+ import { createSubSlot, useCallback, useLayoutEffect, useRef } from "octane";
4
4
  import { assertOctaneIdentity } from "./bootstrap.mjs";
5
5
  import { RouteDataError, createFileSystemRouteTree, matchApplicationRouteIds, matchApplicationRoutes, resolveRouteData, selectApplicationDataRoute } from "./routes.mjs";
6
6
  export * from "@octanejs/tanstack-router";
@@ -69,7 +69,7 @@ function useOctaneRouteAction(...args) {
69
69
  const identity = useApplicationIdentity();
70
70
  const routeId = useApplicationRouteId();
71
71
  const owner = useRef(null, actionSlot(slot, 'owner'));
72
- useEffect(()=>{
72
+ useLayoutEffect(()=>{
73
73
  const controller = new AbortController();
74
74
  owner.current = controller;
75
75
  return ()=>controller.abort(new DOMException('The route action owner unmounted', 'AbortError'));
@@ -1,6 +1,10 @@
1
1
  import { RouteDataError, loadFileSystemRoute, matchApplicationRouteIds, matchApplicationRoutes, nativeRoutePath, resolveRouteData, selectApplicationDataRoute, splitFileSystemRoutes } from "@modern-js/renderer-core/router";
2
2
  import { ErrorComponent, HeadContent, Outlet, Scripts, createRootRoute, createRoute, notFound, redirect } from "@octanejs/tanstack-router";
3
3
  import { Fragment, createElement } from "octane";
4
+ const fileRouteHeads = new WeakMap();
5
+ function hasUnchangedOctaneFileRouteHead(route) {
6
+ return fileRouteHeads.get(route) === route.options.head;
7
+ }
4
8
  function routes_resolveRouteData(routeId, outcome) {
5
9
  return resolveRouteData({
6
10
  redirect: redirect,
@@ -10,21 +14,12 @@ function routes_resolveRouteData(routeId, outcome) {
10
14
  function isReactiveNativeMatch(value) {
11
15
  return 'object' == typeof value && null !== value && 'function' == typeof Reflect.get(value, 'get') && 'function' == typeof Reflect.get(value, 'subscribe');
12
16
  }
13
- function observeRouteCompletion(router, controller, signal, completion) {
17
+ function observeRouteCompletion(router, routeId, controller, signal, completion) {
14
18
  completion.catch((error)=>{
15
19
  if (signal.aborted) return;
16
- const match = [
17
- ...router.stores.pendingMatches.get(),
18
- ...router.stores.matches.get(),
19
- ...router.stores.cachedMatches.get()
20
- ].find((candidate)=>candidate.abortController === controller);
21
- if (!match) return;
22
- const store = [
23
- router.stores.pendingMatchStores.get(match.id),
24
- router.stores.matchStores.get(match.id),
25
- router.stores.cachedMatchStores.get(match.id)
26
- ].find((candidate)=>candidate?.get().abortController === controller);
27
- if (!store) return;
20
+ const route = Object.values(router.routesById).find((candidate)=>candidate.options.staticData?.ultramodernRouteId === routeId);
21
+ if (!route) return;
22
+ const store = router.stores.getMatchStore(route.id);
28
23
  let subscription;
29
24
  let settled = false;
30
25
  const cleanup = ()=>{
@@ -35,9 +30,10 @@ function observeRouteCompletion(router, controller, signal, completion) {
35
30
  settled = true;
36
31
  cleanup();
37
32
  };
38
- const publish = ()=>{
33
+ const publish = async ()=>{
39
34
  if (settled) return;
40
35
  const current = store.get();
36
+ if (!current) return;
41
37
  if (current.abortController !== controller || signal.aborted || 'pending' !== current.status && 'success' !== current.status) return void onAbort();
42
38
  if ('success' !== current.status || current.isFetching) return;
43
39
  settled = true;
@@ -45,17 +41,19 @@ function observeRouteCompletion(router, controller, signal, completion) {
45
41
  const route = router.routesById[current.routeId];
46
42
  let failure = error;
47
43
  try {
44
+ await route?.options.errorComponent?.preload?.();
45
+ if (signal.aborted || store.get()?.abortController !== controller) return;
48
46
  route?.options.onError?.(failure);
49
47
  } catch (onErrorFailure) {
50
48
  failure = onErrorFailure;
51
49
  }
52
50
  if (signal.aborted) return;
53
- router.updateMatch(match.id, (previous)=>previous.abortController !== controller || 'success' !== previous.status || previous.isFetching ? previous : {
51
+ router.stores.setMatches(router.stores.matches.get().map((previous)=>previous.routeId !== route.id || previous.abortController !== controller || 'success' !== previous.status || previous.isFetching ? previous : {
54
52
  ...previous,
55
53
  error: failure,
56
54
  status: 'error',
57
55
  isFetching: false
58
- });
56
+ }));
59
57
  };
60
58
  signal.addEventListener('abort', onAbort, {
61
59
  once: true
@@ -99,7 +97,7 @@ function createFileSystemRouteTree(routes, modules, options = {}) {
99
97
  await Promise.resolve();
100
98
  signal.throwIfAborted();
101
99
  if (failed) throw failure;
102
- observeRouteCompletion(router, match.abortController, signal, outcome.completion);
100
+ observeRouteCompletion(router, route.id, match.abortController, signal, outcome.completion);
103
101
  }
104
102
  options.onOutcome?.(route.id, outcome);
105
103
  return routes_resolveRouteData(route.id, outcome);
@@ -120,6 +118,7 @@ function createFileSystemRouteTree(routes, modules, options = {}) {
120
118
  ...rootBindings,
121
119
  component: OctaneDocumentRoot
122
120
  });
121
+ fileRouteHeads.set(root, root.options.head);
123
122
  function bind(route, parent, inheritedErrorComponent) {
124
123
  const errorComponent = modules[route.id]?.errorComponent ?? inheritedErrorComponent;
125
124
  const native = createRoute({
@@ -127,8 +126,9 @@ function createFileSystemRouteTree(routes, modules, options = {}) {
127
126
  ...nativeRoutePath(route),
128
127
  ...bindings(route, errorComponent)
129
128
  });
129
+ fileRouteHeads.set(native, native.options.head);
130
130
  return native.addChildren(route.children.map((child)=>bind(child, native, errorComponent)));
131
131
  }
132
132
  return root.addChildren(children.map((route)=>bind(route, root, rootErrorComponent)));
133
133
  }
134
- export { RouteDataError, createFileSystemRouteTree, matchApplicationRouteIds, matchApplicationRoutes, routes_resolveRouteData as resolveRouteData, selectApplicationDataRoute };
134
+ export { RouteDataError, createFileSystemRouteTree, hasUnchangedOctaneFileRouteHead, matchApplicationRouteIds, matchApplicationRoutes, routes_resolveRouteData as resolveRouteData, selectApplicationDataRoute };
@@ -1,8 +1,10 @@
1
1
  import { prepareDocument } from "@modern-js/renderer-core/document";
2
2
  import { untilAborted } from "@modern-js/renderer-core/session";
3
+ import { transformReadableStreamWithRouter } from "@octanejs/tanstack-router/ssr/server";
3
4
  import { createElement, earlySignalBootstrapScript, renderToReadableStream } from "octane/server";
4
5
  import { assertNativeHydrationBuildId, assertOctaneIdentity } from "./bootstrap.mjs";
5
6
  import { FederationRoot, createFederationScope } from "./federation-context.mjs";
7
+ import { WorkerFederationRoot } from "./worker-federation-context.mjs";
6
8
  function prepareOctaneDocument(session, document, hydrating) {
7
9
  assertOctaneIdentity(session.identity);
8
10
  if ('node' !== session.platform.kind && 'worker' !== session.platform.kind) throw new Error('An Octane document requires a Node or worker request platform.');
@@ -73,12 +75,21 @@ async function renderOctaneApplication(input) {
73
75
  if (session.responsePolicy?.kind === 'terminal') return session.respond(null);
74
76
  session.startRendering();
75
77
  let head = '';
76
- const federation = input.federation ? createFederationScope(input.federation, true, document.nonce) : void 0;
78
+ const federation = input.federation || input.workerFederation ? createFederationScope(input.federation, true, document.nonce) : void 0;
77
79
  const App = federation ? ()=>createElement(FederationRoot, {
78
80
  scope: federation,
79
81
  children: 'function' == typeof input.App ? createElement(input.App, input.props) : input.App
80
82
  }) : input.App;
81
- const nativeStream = await renderToReadableStream(App, input.props, {
83
+ const workerFederation = input.workerFederation ? {
84
+ transport: input.workerFederation,
85
+ locale: input.workerFederationLocale ?? 'default',
86
+ signal: session.signal
87
+ } : void 0;
88
+ const NativeApp = workerFederation ? ()=>createElement(WorkerFederationRoot, {
89
+ scope: workerFederation,
90
+ children: 'function' == typeof App ? createElement(App, input.props) : App
91
+ }) : App;
92
+ const nativeStream = await renderToReadableStream(NativeApp, input.props, {
82
93
  signal: session.signal,
83
94
  ...void 0 === document.nonce ? {} : {
84
95
  nonce: document.nonce
@@ -91,6 +102,9 @@ async function renderOctaneApplication(input) {
91
102
  buildId: document.nativeHydrationBuildId,
92
103
  documentId: document.documentId
93
104
  },
105
+ externalSnapshots: {
106
+ documentId: document.documentId
107
+ },
94
108
  headChannel: 'separate',
95
109
  onHeadReady (value) {
96
110
  head = value;
@@ -174,7 +188,14 @@ async function renderOctaneApplication(input) {
174
188
  }, {
175
189
  highWaterMark: 0
176
190
  });
177
- return session.respond(body);
191
+ return session.respond(input.router ? transformReadableStreamWithRouter(input.router, body, {
192
+ signal: session.signal,
193
+ rendererSafePoint: "script-close",
194
+ timeoutMs: input.routerSerializationTimeoutMs,
195
+ onAbort: (reason)=>{
196
+ session.fail(reason);
197
+ }
198
+ }) : body);
178
199
  } catch (error) {
179
200
  await session.fail(error);
180
201
  throw error;
@@ -0,0 +1,19 @@
1
+ import { createContext, createElement } from "octane";
2
+ import { FederationApplication } from "./federation-context.mjs";
3
+ const WorkerFederationContext = createContext(null);
4
+ function WorkerFederationRoot(props) {
5
+ return createElement(WorkerFederationContext, {
6
+ value: props.scope,
7
+ children: props.children
8
+ });
9
+ }
10
+ function WorkerFederationApplication(props) {
11
+ return createElement(WorkerFederationRoot, {
12
+ scope: props.scope,
13
+ children: props.federation ? createElement(FederationApplication, {
14
+ scope: props.federation,
15
+ application: props.application
16
+ }) : createElement(props.application.default, props.application.props)
17
+ });
18
+ }
19
+ export { WorkerFederationApplication, WorkerFederationContext, WorkerFederationRoot };
@@ -0,0 +1,47 @@
1
+ import { registerExternalSnapshotContext, useContext } from "octane";
2
+ const OCTANE_WORKER_CONTEXT_KEYS = Object.freeze({
3
+ router: 'ultramodern.octane.router-context.v1',
4
+ match: 'ultramodern.octane.match-context.v1',
5
+ i18n: 'ultramodern.octane.i18n-context.v1'
6
+ });
7
+ const builtinKeys = Object.values(OCTANE_WORKER_CONTEXT_KEYS);
8
+ const contexts = new Map();
9
+ function registerOctaneWorkerContext(context, codec) {
10
+ const { key, isProvided } = codec;
11
+ if (!builtinKeys.includes(key) || 'function' != typeof isProvided) throw new TypeError('Invalid Octane Worker builtin context codec');
12
+ const unregister = registerExternalSnapshotContext(context, codec);
13
+ const hasProvider = ()=>isProvided(useContext(context));
14
+ contexts.set(key, hasProvider);
15
+ return ()=>{
16
+ if (contexts.get(key) !== hasProvider) return;
17
+ unregister();
18
+ contexts.delete(key);
19
+ };
20
+ }
21
+ function useOctaneWorkerContextKeys(explicit = []) {
22
+ const selected = [];
23
+ for (const key of builtinKeys){
24
+ const hasProvider = contexts.get(key);
25
+ if (hasProvider?.() && !explicit.includes(key)) selected.push(key);
26
+ }
27
+ return [
28
+ ...selected,
29
+ ...explicit
30
+ ];
31
+ }
32
+ let routerRewrite;
33
+ function registerOctaneWorkerRouterRewrite(codec) {
34
+ if (routerRewrite || 'function' != typeof codec.capture || 'function' != typeof codec.restore) throw new TypeError('Invalid or duplicate Octane Worker router rewrite codec');
35
+ const captured = Object.freeze({
36
+ capture: codec.capture,
37
+ restore: codec.restore
38
+ });
39
+ routerRewrite = captured;
40
+ return ()=>{
41
+ if (routerRewrite === captured) routerRewrite = void 0;
42
+ };
43
+ }
44
+ function getOctaneWorkerRouterRewrite() {
45
+ return routerRewrite;
46
+ }
47
+ export { OCTANE_WORKER_CONTEXT_KEYS, getOctaneWorkerRouterRewrite, registerOctaneWorkerContext, registerOctaneWorkerRouterRewrite, useOctaneWorkerContextKeys };
@@ -0,0 +1,59 @@
1
+ import { createWorkerFederationTransport } from "@modern-js/renderer-core/worker-federation";
2
+ import { prepareExternalSnapshotRequest, releasePreparedExternalSnapshotRequest, renderExternalSnapshot, serializeExternalSnapshot } from "octane/server";
3
+ import { FederationContext, createFederationScope } from "./federation-context.mjs";
4
+ import { WorkerFederationContext } from "./worker-federation-context.mjs";
5
+ function isNativeWorkerComponent(value) {
6
+ return 'function' == typeof value;
7
+ }
8
+ function createNativeWorkerFederationRuntime(options, compiler) {
9
+ const { resources } = options;
10
+ const bindings = options.bindings;
11
+ const transport = resources.workerFederationServices?.length ? createWorkerFederationTransport({
12
+ admitRendererContract: compiler.admitRendererContract,
13
+ services: resources.workerFederationServices.map((service)=>{
14
+ const binding = bindings && Reflect.get(bindings, service.binding);
15
+ if (!binding || 'object' != typeof binding && 'function' != typeof binding || !('fetch' in binding) || 'function' != typeof binding.fetch) throw new Error(`Native Worker federation requires service binding ${service.binding}.`);
16
+ return {
17
+ ...service,
18
+ fetch: binding.fetch.bind(binding)
19
+ };
20
+ })
21
+ }) : void 0;
22
+ let handle;
23
+ if (resources.workerFederation) {
24
+ if (!compiler.registry || !resources.workerFederationManifestUrl) throw new Error('Native Worker federation requires its compiled expose registry and pinned manifest URL.');
25
+ handle = compiler.registry.initialize({
26
+ publication: resources.workerFederation.worker,
27
+ manifestUrl: resources.workerFederationManifestUrl,
28
+ admitRendererContract: compiler.admitRendererContract,
29
+ admitNativeRequest (payload, { admitted, signal }) {
30
+ const initializeContexts = transport ? (provide, request)=>{
31
+ provide(FederationContext, createFederationScope(void 0, true, request.nonce));
32
+ provide(WorkerFederationContext, {
33
+ transport,
34
+ locale: admitted.locale,
35
+ signal
36
+ });
37
+ } : void 0;
38
+ return prepareExternalSnapshotRequest(payload, {
39
+ authority: admitted.coordinate.native,
40
+ signal,
41
+ ...initializeContexts ? {
42
+ initializeContexts
43
+ } : {}
44
+ });
45
+ },
46
+ releaseNativeRequest: releasePreparedExternalSnapshotRequest,
47
+ async renderNativeSnapshot (module, request, { admitted }) {
48
+ const component = module && 'object' == typeof module && 'default' in module ? module.default : void 0;
49
+ if (!isNativeWorkerComponent(component)) throw new TypeError(`Native Worker expose ${admitted.expose} requires a default Octane component.`);
50
+ return serializeExternalSnapshot(await renderExternalSnapshot(component, request));
51
+ }
52
+ });
53
+ }
54
+ return {
55
+ transport,
56
+ handleRequest: handle ?? (()=>Promise.resolve(void 0))
57
+ };
58
+ }
59
+ export { createNativeWorkerFederationRuntime };
@@ -1,7 +1,7 @@
1
1
  import { nativeModuleManifestFilename, readRendererIdentity } from "@modern-js/renderer-core/identity";
2
2
  import { assertNativeHydrationBuildId, assertOctaneIdentity } from "./bootstrap.mjs";
3
- const OCTANE_RUNTIME_VERSION = '0.7.1+ultramodern.1331985ea3b0';
4
- const OCTANE_COMPILER_VERSION = '0.1.55';
3
+ const OCTANE_RUNTIME_VERSION = '0.13.8';
4
+ const OCTANE_COMPILER_VERSION = '0.2.4';
5
5
  function octaneModuleManifestFileName(entryName) {
6
6
  return nativeModuleManifestFilename('octane', entryName);
7
7
  }
@@ -20,7 +20,8 @@ function createOctaneCompilerPlugin(options) {
20
20
  const directory = pluginSourceDirectory();
21
21
  const require = createRequire(node_path.join(directory, 'index.cjs'));
22
22
  const { OctaneCompilerManifestPlugin } = require('./compiler-manifest.cjs');
23
- const { inferRspackEnvironment, OctaneRspackPlugin } = require('@octanejs/rspack-plugin');
23
+ const appRequire = createRequire(node_path.join(api.context.rootPath, 'package.json'));
24
+ const { inferRspackEnvironment, OctaneRspackPlugin } = appRequire('@octanejs/rspack-plugin');
24
25
  api.modifyRsbuildConfig((config, { mergeRsbuildConfig })=>mergeRsbuildConfig(config, {
25
26
  source: {
26
27
  include: [
@@ -30,10 +31,6 @@ function createOctaneCompilerPlugin(options) {
30
31
  }));
31
32
  api.modifyRspackConfig((config, { environment: rsbuildEnvironment })=>{
32
33
  const environment = rsbuildEnvironment.name === options.workerEnvironmentName ? 'server' : inferRspackEnvironment(config.target);
33
- config.resolve ??= {};
34
- const extensions = (config.resolve.extensions ?? []).filter((extension)=>'.tsrx' !== extension);
35
- extensions.splice(Math.max(extensions.indexOf('.ts'), extensions.indexOf('.tsx')) + 1, 0, '.tsrx');
36
- config.resolve.extensions = extensions;
37
34
  config.plugins ??= [];
38
35
  config.plugins.push(new OctaneRspackPlugin({
39
36
  root: api.context.rootPath,