@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.7 → 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.
- package/dist/cjs/client.js +56 -6
- package/dist/cjs/entry-application.js +7 -2
- package/dist/cjs/entry-client.js +3 -0
- package/dist/cjs/entry-server.js +13 -5
- package/dist/cjs/federation.js +108 -6
- package/dist/cjs/i18n/I18nProvider.js +6 -14
- package/dist/cjs/i18n/LocalizedLink.js +24 -33
- package/dist/cjs/i18n/index.js +0 -3
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/manifest.js +2 -2
- package/dist/cjs/plugin/compiler.js +2 -5
- package/dist/cjs/plugin/index.js +2 -2
- package/dist/cjs/plugin/profile.js +12 -10
- package/dist/cjs/router-client.js +1 -1
- package/dist/cjs/router-external-snapshot.js +263 -0
- package/dist/cjs/router-handler.js +11 -8
- package/dist/cjs/router-server.js +3 -11
- package/dist/cjs/router.js +1 -1
- package/dist/cjs/routes.js +11 -17
- package/dist/cjs/server.js +28 -7
- package/dist/cjs/worker-federation-context.js +63 -0
- package/dist/cjs/worker-federation-contexts.js +97 -0
- package/dist/cjs/worker-federation.js +97 -0
- package/dist/esm/client.mjs +56 -6
- package/dist/esm/entry-application.mjs +7 -2
- package/dist/esm/entry-client.mjs +3 -0
- package/dist/esm/entry-server.mjs +11 -6
- package/dist/esm/federation.mjs +110 -8
- package/dist/esm/i18n/I18nProvider.mjs +7 -15
- package/dist/esm/i18n/LocalizedLink.mjs +24 -30
- package/dist/esm/i18n/index.mjs +1 -1
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +4 -4
- package/dist/esm/manifest.mjs +2 -2
- package/dist/esm/router-client.mjs +1 -1
- package/dist/esm/router-external-snapshot.mjs +216 -0
- package/dist/esm/router-handler.mjs +11 -8
- package/dist/esm/router-server.mjs +0 -1
- package/dist/esm/router.mjs +2 -2
- package/dist/esm/routes.mjs +11 -17
- package/dist/esm/server.mjs +24 -3
- package/dist/esm/worker-federation-context.mjs +19 -0
- package/dist/esm/worker-federation-contexts.mjs +47 -0
- package/dist/esm/worker-federation.mjs +59 -0
- package/dist/esm-node/manifest.mjs +2 -2
- package/dist/esm-node/plugin/compiler.mjs +2 -5
- package/dist/esm-node/plugin/index.mjs +2 -2
- package/dist/esm-node/plugin/profile.mjs +12 -10
- package/dist/types/client.d.ts +5 -1
- package/dist/types/entry-client.d.ts +2 -0
- package/dist/types/entry-server.d.ts +4 -1
- package/dist/types/federation.d.ts +2 -0
- package/dist/types/i18n/LocalizedLink.d.ts +5 -32
- package/dist/types/i18n/context.d.ts +1 -1
- package/dist/types/i18n/index.d.ts +1 -1
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/manifest.d.ts +2 -2
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-server.d.ts +0 -2
- package/dist/types/server.d.ts +6 -0
- package/dist/types/worker-federation-context.d.ts +34 -0
- package/dist/types/worker-federation-contexts.d.ts +25 -0
- package/dist/types/worker-federation.d.ts +20 -0
- package/package.json +84 -22
- package/src/client.ts +99 -16
- package/src/entry-application.ts +7 -3
- package/src/entry-client.ts +10 -0
- package/src/entry-server.ts +20 -9
- package/src/federation.ts +159 -15
- package/src/i18n/I18nProvider.ts +11 -20
- package/src/i18n/LocalizedLink.ts +46 -72
- package/src/i18n/context.ts +1 -1
- package/src/i18n/index.ts +1 -5
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +4 -4
- package/src/manifest.ts +2 -2
- package/src/plugin/compiler-manifest.cjs +27 -1
- package/src/plugin/compiler.ts +6 -14
- package/src/plugin/index.ts +4 -2
- package/src/plugin/profile.ts +14 -10
- package/src/router-client.ts +1 -1
- package/src/router-external-snapshot.ts +306 -0
- package/src/router-handler.ts +11 -8
- package/src/router-server.ts +0 -2
- package/src/router.ts +2 -2
- package/src/routes.ts +28 -26
- package/src/server.ts +45 -5
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/dist/cjs/router-injection.js +0 -139
- package/dist/esm/router-injection.mjs +0 -101
- package/dist/types/router-injection.d.ts +0 -8
- package/src/router-injection.ts +0 -145
package/src/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type { OctaneDocumentBootstrap } from './bootstrap';
|
|
|
3
3
|
|
|
4
4
|
export const OCTANE_RUNTIME_PROFILE = Object.freeze({
|
|
5
5
|
renderer: 'octane',
|
|
6
|
-
runtime: '0.
|
|
7
|
-
router: '0.
|
|
8
|
-
routerCore: '1.171.
|
|
9
|
-
history: '1.162.
|
|
6
|
+
runtime: '0.13.8',
|
|
7
|
+
router: '0.2.10',
|
|
8
|
+
routerCore: '1.171.34',
|
|
9
|
+
history: '1.162.4',
|
|
10
10
|
});
|
package/src/manifest.ts
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
assertOctaneIdentity,
|
|
10
10
|
} from './bootstrap';
|
|
11
11
|
|
|
12
|
-
export const OCTANE_RUNTIME_VERSION = '0.
|
|
13
|
-
export const OCTANE_COMPILER_VERSION = '0.
|
|
12
|
+
export const OCTANE_RUNTIME_VERSION = '0.13.8';
|
|
13
|
+
export const OCTANE_COMPILER_VERSION = '0.2.4';
|
|
14
14
|
|
|
15
15
|
export interface OctaneCompiledSource {
|
|
16
16
|
readonly resource: string;
|
|
@@ -107,6 +107,29 @@ class OctaneCompilerManifestPlugin {
|
|
|
107
107
|
sources.sort((left, right) =>
|
|
108
108
|
left.resource.localeCompare(right.resource),
|
|
109
109
|
);
|
|
110
|
+
// Module Federation publishes exposed native modules through a
|
|
111
|
+
// separate container entry in this same compilation. Its browser
|
|
112
|
+
// closure belongs to this application's publication, even when the
|
|
113
|
+
// document itself never imports an exposed component.
|
|
114
|
+
const containerChunks = new Set();
|
|
115
|
+
for (const [entryName, entry] of compilation.entries) {
|
|
116
|
+
if (
|
|
117
|
+
!entry.dependencies.some(
|
|
118
|
+
dependency => dependency.type === 'container entry',
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
continue;
|
|
122
|
+
const container = compilation.entrypoints.get(entryName);
|
|
123
|
+
if (!container)
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Octane compiler has no federation container entry for ${entryName}.`,
|
|
126
|
+
);
|
|
127
|
+
for (const chunk of container.chunks) {
|
|
128
|
+
containerChunks.add(chunk);
|
|
129
|
+
for (const referenced of chunk.getAllReferencedChunks())
|
|
130
|
+
containerChunks.add(referenced);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
110
133
|
const identities = this.options.rendererIdentities();
|
|
111
134
|
if (!identities || !Object.keys(identities).length) {
|
|
112
135
|
throw new Error(
|
|
@@ -129,7 +152,10 @@ class OctaneCompilerManifestPlugin {
|
|
|
129
152
|
throw new Error(
|
|
130
153
|
`Octane compiler has no client entry for ${entryName}.`,
|
|
131
154
|
);
|
|
132
|
-
const entryChunks = new Set(
|
|
155
|
+
const entryChunks = new Set([
|
|
156
|
+
...entrypoint.chunks,
|
|
157
|
+
...containerChunks,
|
|
158
|
+
]);
|
|
133
159
|
for (const chunk of entrypoint.chunks) {
|
|
134
160
|
for (const referenced of chunk.getAllReferencedChunks())
|
|
135
161
|
entryChunks.add(referenced);
|
package/src/plugin/compiler.ts
CHANGED
|
@@ -41,10 +41,15 @@ export function createOctaneCompilerPlugin(
|
|
|
41
41
|
} = require('./compiler-manifest.cjs');
|
|
42
42
|
// The application installs the Octane compiler; resolve it lazily so
|
|
43
43
|
// descriptor readers never load it.
|
|
44
|
+
const appRequire = createRequire(
|
|
45
|
+
path.join(api.context.rootPath, 'package.json'),
|
|
46
|
+
);
|
|
44
47
|
const {
|
|
45
48
|
inferRspackEnvironment,
|
|
46
49
|
OctaneRspackPlugin,
|
|
47
|
-
}: typeof import('@octanejs/rspack-plugin') =
|
|
50
|
+
}: typeof import('@octanejs/rspack-plugin') = appRequire(
|
|
51
|
+
'@octanejs/rspack-plugin',
|
|
52
|
+
);
|
|
48
53
|
api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) =>
|
|
49
54
|
mergeRsbuildConfig(config, {
|
|
50
55
|
source: {
|
|
@@ -60,19 +65,6 @@ export function createOctaneCompilerPlugin(
|
|
|
60
65
|
rsbuildEnvironment.name === options.workerEnvironmentName
|
|
61
66
|
? 'server'
|
|
62
67
|
: inferRspackEnvironment(config.target);
|
|
63
|
-
// Octane prepends '.tsrx' only when it is absent. Keep TypeScript
|
|
64
|
-
// modules first so an extensionless './link' cannot select a
|
|
65
|
-
// case-colliding Link.tsrx on case-insensitive file systems.
|
|
66
|
-
config.resolve ??= {};
|
|
67
|
-
const extensions = (config.resolve.extensions ?? []).filter(
|
|
68
|
-
extension => extension !== '.tsrx',
|
|
69
|
-
);
|
|
70
|
-
extensions.splice(
|
|
71
|
-
Math.max(extensions.indexOf('.ts'), extensions.indexOf('.tsx')) + 1,
|
|
72
|
-
0,
|
|
73
|
-
'.tsrx',
|
|
74
|
-
);
|
|
75
|
-
config.resolve.extensions = extensions;
|
|
76
68
|
config.plugins ??= [];
|
|
77
69
|
config.plugins.push(
|
|
78
70
|
new OctaneRspackPlugin({
|
package/src/plugin/index.ts
CHANGED
|
@@ -81,7 +81,7 @@ export const rendererAdapter: NativeRendererAdapter<'octane'> =
|
|
|
81
81
|
artifacts: compilerArtifacts,
|
|
82
82
|
assertSupportedSource: assertOctaneEntrySource,
|
|
83
83
|
create: {
|
|
84
|
-
templates: { federation: true, workers:
|
|
84
|
+
templates: { federation: true, workers: true },
|
|
85
85
|
dependencies: (profile, { typescriptVersion }) => ({
|
|
86
86
|
frameworkDependencies: [
|
|
87
87
|
'@modern-js/renderer-core',
|
|
@@ -89,7 +89,9 @@ export const rendererAdapter: NativeRendererAdapter<'octane'> =
|
|
|
89
89
|
],
|
|
90
90
|
dependencies: nativeRendererDependencies(profile),
|
|
91
91
|
devDependencies: {
|
|
92
|
-
[profile.compiler.name]:
|
|
92
|
+
[profile.compiler.name]:
|
|
93
|
+
profile.dependencies[profile.compiler.name] ??
|
|
94
|
+
profile.compiler.version,
|
|
93
95
|
typescript: typescriptVersion,
|
|
94
96
|
},
|
|
95
97
|
typecheckCommand: 'octane-tsc --noEmit --project tsconfig.json',
|
package/src/plugin/profile.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const octaneProfile: RendererBuildProfile<'octane'> = {
|
|
|
4
4
|
renderer: 'octane',
|
|
5
5
|
status: 'preview',
|
|
6
6
|
protocolVersion: 1,
|
|
7
|
-
minimumNode: '26.
|
|
7
|
+
minimumNode: '26.12.0',
|
|
8
8
|
hmr: {
|
|
9
9
|
editedBoundary: 'may-reset',
|
|
10
10
|
unaffectedComponents: 'preserved',
|
|
@@ -12,28 +12,32 @@ export const octaneProfile: RendererBuildProfile<'octane'> = {
|
|
|
12
12
|
roots: 'single',
|
|
13
13
|
cleanup: 'exactly-once',
|
|
14
14
|
},
|
|
15
|
-
compiler: { name: '@octanejs/rspack-plugin', version: '0.
|
|
15
|
+
compiler: { name: '@octanejs/rspack-plugin', version: '0.2.4' },
|
|
16
16
|
hydration: {
|
|
17
17
|
name: 'octane',
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.13.8',
|
|
19
19
|
},
|
|
20
20
|
router: {
|
|
21
21
|
name: '@octanejs/tanstack-router',
|
|
22
|
-
version: '0.
|
|
22
|
+
version: '0.2.10',
|
|
23
23
|
coreName: '@tanstack/router-core',
|
|
24
|
-
coreVersion: '1.171.
|
|
24
|
+
coreVersion: '1.171.38',
|
|
25
25
|
},
|
|
26
26
|
sourceExtensions: ['.tsx', '.tsrx', '.ts', '.js'],
|
|
27
27
|
jsxImportSource: 'octane',
|
|
28
28
|
dependencies: {
|
|
29
|
+
'@tsrx/oxc': '0.20.0',
|
|
29
30
|
octane:
|
|
30
|
-
'https://github.com/
|
|
31
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octane-0.13.8.tgz',
|
|
31
32
|
'@octanejs/tanstack-router':
|
|
32
|
-
'https://github.com/
|
|
33
|
-
'@octanejs/rspack-plugin':
|
|
33
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-tanstack-router-0.2.10.tgz',
|
|
34
|
+
'@octanejs/rspack-plugin':
|
|
35
|
+
'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-rspack-plugin-0.2.4.tgz',
|
|
36
|
+
'@tanstack/router-core':
|
|
37
|
+
'https://github.com/BleedingDev/router/releases/download/ultramodern-router-core-585d3233f7f5/tanstack-router-core-1.171.38.tgz',
|
|
34
38
|
'@modern-js/renderer-octane': '3.8.3',
|
|
35
|
-
seroval: '1.6.
|
|
36
|
-
'seroval-plugins': '1.6.
|
|
39
|
+
seroval: '1.6.9',
|
|
40
|
+
'seroval-plugins': '1.6.9',
|
|
37
41
|
},
|
|
38
42
|
capabilities: {
|
|
39
43
|
worker: true,
|
package/src/router-client.ts
CHANGED
|
@@ -64,7 +64,7 @@ function waitForCommittedMatches(
|
|
|
64
64
|
signal?: AbortSignal,
|
|
65
65
|
): Promise<void> {
|
|
66
66
|
signal?.throwIfAborted();
|
|
67
|
-
const matchesId = router.stores.
|
|
67
|
+
const matchesId = router.stores.ids;
|
|
68
68
|
if (matchesId.get().length > 0) return Promise.resolve();
|
|
69
69
|
if (!isReactiveMatchIds(matchesId)) {
|
|
70
70
|
throw new Error(
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
import {
|
|
3
|
+
type AnyRouter,
|
|
4
|
+
assertDefaultRouterRenderHost,
|
|
5
|
+
associateRouterRenderProfile,
|
|
6
|
+
captureRouterRenderSnapshot,
|
|
7
|
+
createRouterRenderRouteTree,
|
|
8
|
+
defaultParseSearch,
|
|
9
|
+
defaultStringifySearch,
|
|
10
|
+
disposeRouterRenderSnapshot,
|
|
11
|
+
getRouterRenderProfile,
|
|
12
|
+
matchContext,
|
|
13
|
+
type RouterRenderJsonRecord,
|
|
14
|
+
type RouterRenderSnapshot,
|
|
15
|
+
registerRouterRenderProfile,
|
|
16
|
+
restoreRouterRenderSnapshot,
|
|
17
|
+
routerContext,
|
|
18
|
+
} from '@octanejs/tanstack-router';
|
|
19
|
+
import {
|
|
20
|
+
getOctaneWorkerRouterRewrite,
|
|
21
|
+
OCTANE_WORKER_CONTEXT_KEYS,
|
|
22
|
+
registerOctaneWorkerContext,
|
|
23
|
+
} from './worker-federation-contexts';
|
|
24
|
+
|
|
25
|
+
export const OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE = Object.freeze({
|
|
26
|
+
key: 'ultramodern.file-routes',
|
|
27
|
+
version: '1',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
interface GeneratedRouterOrigin {
|
|
31
|
+
readonly publicContext: RouterRenderJsonRecord;
|
|
32
|
+
readonly rewrite: AnyRouter['options']['rewrite'];
|
|
33
|
+
}
|
|
34
|
+
const origins = new WeakMap<AnyRouter, GeneratedRouterOrigin>();
|
|
35
|
+
let profileRegistered = false;
|
|
36
|
+
|
|
37
|
+
function plainRecord(value: unknown): value is Record<string, unknown> {
|
|
38
|
+
return (
|
|
39
|
+
value !== null &&
|
|
40
|
+
typeof value === 'object' &&
|
|
41
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function record(
|
|
46
|
+
value: unknown,
|
|
47
|
+
fields?: readonly string[],
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
if (!plainRecord(value))
|
|
50
|
+
throw new TypeError('Worker router context requires a plain record');
|
|
51
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
52
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
53
|
+
if (
|
|
54
|
+
typeof key !== 'string' ||
|
|
55
|
+
(fields && !fields.includes(key)) ||
|
|
56
|
+
!descriptor ||
|
|
57
|
+
!descriptor.enumerable ||
|
|
58
|
+
!('value' in descriptor)
|
|
59
|
+
)
|
|
60
|
+
throw new TypeError('Unknown Worker router context field or accessor');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nonempty(value: unknown): string {
|
|
66
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 1024)
|
|
67
|
+
throw new TypeError('Invalid Worker router identity');
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Only the host entry's public logical identity crosses the Worker boundary. */
|
|
72
|
+
function publicContext(value: unknown): RouterRenderJsonRecord {
|
|
73
|
+
const context = record(value, ['ultramodern']);
|
|
74
|
+
const metadata = record(context.ultramodern, ['rendererIdentity']);
|
|
75
|
+
const identity = record(metadata.rendererIdentity, [
|
|
76
|
+
'renderer',
|
|
77
|
+
'appId',
|
|
78
|
+
'entryName',
|
|
79
|
+
'protocolVersion',
|
|
80
|
+
'buildId',
|
|
81
|
+
]);
|
|
82
|
+
if (identity.renderer !== 'octane' || identity.protocolVersion !== 1)
|
|
83
|
+
throw new TypeError('Unsupported Worker router identity');
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
ultramodern: Object.freeze({
|
|
86
|
+
rendererIdentity: Object.freeze({
|
|
87
|
+
renderer: 'octane',
|
|
88
|
+
appId: nonempty(identity.appId),
|
|
89
|
+
entryName: nonempty(identity.entryName),
|
|
90
|
+
protocolVersion: 1,
|
|
91
|
+
buildId: nonempty(identity.buildId),
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function localizedUrls(value: unknown): LocalisedUrlsOption | undefined {
|
|
98
|
+
if (value === undefined || typeof value === 'boolean') return value;
|
|
99
|
+
const input = record(value);
|
|
100
|
+
const result: Exclude<LocalisedUrlsOption, boolean> = {};
|
|
101
|
+
for (const [path, translations] of Object.entries(input)) {
|
|
102
|
+
const names: Record<string, string> = {};
|
|
103
|
+
for (const [language, target] of Object.entries(record(translations)))
|
|
104
|
+
names[nonempty(language)] = nonempty(target);
|
|
105
|
+
result[path] = names;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rewriteDescriptor(value: unknown):
|
|
111
|
+
| {
|
|
112
|
+
readonly language: string;
|
|
113
|
+
readonly languages: readonly string[];
|
|
114
|
+
readonly localisedUrls?: LocalisedUrlsOption;
|
|
115
|
+
}
|
|
116
|
+
| undefined {
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
const input = record(value, ['language', 'languages', 'localisedUrls']);
|
|
119
|
+
if (
|
|
120
|
+
!Array.isArray(input.languages) ||
|
|
121
|
+
!input.languages.length ||
|
|
122
|
+
input.languages.length > 64
|
|
123
|
+
)
|
|
124
|
+
throw new TypeError('Invalid Worker router i18n languages');
|
|
125
|
+
const languages = input.languages.map(nonempty);
|
|
126
|
+
const language = nonempty(input.language);
|
|
127
|
+
if (
|
|
128
|
+
new Set(languages).size !== languages.length ||
|
|
129
|
+
!languages.includes(language)
|
|
130
|
+
)
|
|
131
|
+
throw new TypeError('Invalid Worker router i18n language');
|
|
132
|
+
const urls = localizedUrls(input.localisedUrls);
|
|
133
|
+
return {
|
|
134
|
+
language,
|
|
135
|
+
languages,
|
|
136
|
+
...(urls === undefined ? {} : { localisedUrls: urls }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function nativeRewriteDescriptor(
|
|
141
|
+
router: AnyRouter,
|
|
142
|
+
): RouterRenderJsonRecord | undefined {
|
|
143
|
+
if (!router.options.rewrite) return undefined;
|
|
144
|
+
const known = getOctaneWorkerRouterRewrite()?.capture(router.options.rewrite);
|
|
145
|
+
if (!known)
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
'Custom router rewrite requires an exact Worker profile',
|
|
148
|
+
);
|
|
149
|
+
const descriptor = rewriteDescriptor(known);
|
|
150
|
+
if (!descriptor)
|
|
151
|
+
throw new TypeError('Missing native router rewrite provenance');
|
|
152
|
+
return {
|
|
153
|
+
language: descriptor.language,
|
|
154
|
+
languages: [...descriptor.languages],
|
|
155
|
+
...(descriptor.localisedUrls === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { localisedUrls: descriptor.localisedUrls }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateDefaultSnapshot(snapshot: RouterRenderSnapshot): void {
|
|
162
|
+
publicContext(snapshot.publicContext);
|
|
163
|
+
rewriteDescriptor(snapshot.i18n);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureDefaultProfile(): void {
|
|
167
|
+
if (profileRegistered) return;
|
|
168
|
+
registerRouterRenderProfile({
|
|
169
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
170
|
+
assertHost: (router, snapshot) => {
|
|
171
|
+
const origin = origins.get(router);
|
|
172
|
+
if (
|
|
173
|
+
!origin ||
|
|
174
|
+
origin.rewrite !== router.options.rewrite ||
|
|
175
|
+
JSON.stringify(publicContext(router.options.context)) !==
|
|
176
|
+
JSON.stringify(origin.publicContext)
|
|
177
|
+
)
|
|
178
|
+
throw new TypeError('Generated Worker router origin changed');
|
|
179
|
+
assertDefaultRouterRenderHost(router, snapshot.i18n !== undefined);
|
|
180
|
+
validateDefaultSnapshot(snapshot);
|
|
181
|
+
if (
|
|
182
|
+
JSON.stringify(nativeRewriteDescriptor(router)) !==
|
|
183
|
+
JSON.stringify(snapshot.i18n)
|
|
184
|
+
)
|
|
185
|
+
throw new TypeError(
|
|
186
|
+
'Worker router rewrite differs from its native provenance',
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
createRoutingTree: snapshot => {
|
|
190
|
+
validateDefaultSnapshot(snapshot);
|
|
191
|
+
return createRouterRenderRouteTree(snapshot.routes);
|
|
192
|
+
},
|
|
193
|
+
routingOptions: snapshot => {
|
|
194
|
+
const defaults = {
|
|
195
|
+
parseSearch: defaultParseSearch,
|
|
196
|
+
stringifySearch: defaultStringifySearch,
|
|
197
|
+
};
|
|
198
|
+
const i18n = rewriteDescriptor(snapshot.i18n);
|
|
199
|
+
if (!i18n) return defaults;
|
|
200
|
+
const codec = getOctaneWorkerRouterRewrite();
|
|
201
|
+
if (!codec)
|
|
202
|
+
throw new TypeError('Missing owning Worker router i18n codec');
|
|
203
|
+
return { ...defaults, rewrite: codec.restore(i18n) };
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
profileRegistered = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Associate the genuine router produced by the generated file-route factory. */
|
|
210
|
+
export function associateOctaneFileRouteSnapshot(router: AnyRouter): void {
|
|
211
|
+
ensureDefaultProfile();
|
|
212
|
+
const context = publicContext(router.options.context);
|
|
213
|
+
origins.set(router, {
|
|
214
|
+
publicContext: context,
|
|
215
|
+
rewrite: router.options.rewrite,
|
|
216
|
+
});
|
|
217
|
+
associateRouterRenderProfile(router, {
|
|
218
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
219
|
+
publicContext: context,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function captureOctaneRouterSnapshot(
|
|
224
|
+
router: AnyRouter,
|
|
225
|
+
): RouterRenderSnapshot {
|
|
226
|
+
const origin = origins.get(router);
|
|
227
|
+
const declared = getRouterRenderProfile(router);
|
|
228
|
+
if (
|
|
229
|
+
origin &&
|
|
230
|
+
declared?.key === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.key &&
|
|
231
|
+
declared.version === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.version
|
|
232
|
+
) {
|
|
233
|
+
const i18n = nativeRewriteDescriptor(router);
|
|
234
|
+
associateRouterRenderProfile(router, {
|
|
235
|
+
id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
|
|
236
|
+
publicContext: origin.publicContext,
|
|
237
|
+
...(i18n === undefined ? {} : { i18n }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return captureRouterRenderSnapshot(router);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let registrations = 0;
|
|
244
|
+
let releaseContexts: (() => void) | undefined;
|
|
245
|
+
|
|
246
|
+
/** Startup registration uses the actual physical native router/match Contexts. */
|
|
247
|
+
export function registerOctaneRouterWorkerContexts(): () => void {
|
|
248
|
+
ensureDefaultProfile();
|
|
249
|
+
if (!releaseContexts) {
|
|
250
|
+
const releaseRouter = registerOctaneWorkerContext(routerContext, {
|
|
251
|
+
key: OCTANE_WORKER_CONTEXT_KEYS.router,
|
|
252
|
+
encode: value => {
|
|
253
|
+
if (!value) throw new TypeError('Missing native router provider');
|
|
254
|
+
return captureOctaneRouterSnapshot(value);
|
|
255
|
+
},
|
|
256
|
+
decode: (value, signal) => restoreRouterRenderSnapshot(value, { signal }),
|
|
257
|
+
dispose: value => {
|
|
258
|
+
if (value) disposeRouterRenderSnapshot(value);
|
|
259
|
+
},
|
|
260
|
+
isProvided: value => value !== undefined,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const releaseMatch = registerOctaneWorkerContext(matchContext, {
|
|
264
|
+
key: OCTANE_WORKER_CONTEXT_KEYS.match,
|
|
265
|
+
encode: nonempty,
|
|
266
|
+
decode: nonempty,
|
|
267
|
+
validate: (value, readContext) => {
|
|
268
|
+
const selected = readContext(routerContext);
|
|
269
|
+
const router = selected.present ? selected.value : undefined;
|
|
270
|
+
// Native Match and useMatch share the active route store key, not
|
|
271
|
+
// the location-specific match identity (which includes parameters).
|
|
272
|
+
const current = router?.state.matches.find(
|
|
273
|
+
match => match.routeId === value,
|
|
274
|
+
);
|
|
275
|
+
if (
|
|
276
|
+
!router ||
|
|
277
|
+
!current ||
|
|
278
|
+
typeof value !== 'string' ||
|
|
279
|
+
router.stores.getMatchStore(value).get() !== current
|
|
280
|
+
)
|
|
281
|
+
throw new TypeError(
|
|
282
|
+
'Worker match context requires its selected native router and a current match',
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
isProvided: value => value !== undefined,
|
|
286
|
+
});
|
|
287
|
+
releaseContexts = () => {
|
|
288
|
+
releaseMatch();
|
|
289
|
+
releaseRouter();
|
|
290
|
+
};
|
|
291
|
+
} catch (error) {
|
|
292
|
+
releaseRouter();
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
registrations++;
|
|
297
|
+
let released = false;
|
|
298
|
+
return () => {
|
|
299
|
+
if (released) return;
|
|
300
|
+
released = true;
|
|
301
|
+
if (--registrations === 0) {
|
|
302
|
+
releaseContexts?.();
|
|
303
|
+
releaseContexts = undefined;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
package/src/router-handler.ts
CHANGED
|
@@ -91,8 +91,9 @@ function requestHeaders(router: AnyRouter): Headers {
|
|
|
91
91
|
);
|
|
92
92
|
appendHeaders(headers, field?.value);
|
|
93
93
|
}
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
94
|
+
const result = router._serverResult;
|
|
95
|
+
if (result?.type === 'redirect')
|
|
96
|
+
appendHeaders(headers, result.redirect.headers);
|
|
96
97
|
return headers;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -117,7 +118,6 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
117
118
|
...serialization,
|
|
118
119
|
forbiddenValues: [request, ...(serialization?.forbiddenValues ?? [])],
|
|
119
120
|
});
|
|
120
|
-
session.registerCleanup(() => router.cancelMatches());
|
|
121
121
|
|
|
122
122
|
const { url } = getNormalizedURL(request.url, 'http://localhost');
|
|
123
123
|
const href = url.href.replace(url.origin, '');
|
|
@@ -126,14 +126,14 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
126
126
|
history: createMemoryHistory({ initialEntries: [href] }),
|
|
127
127
|
origin: router.options.origin ?? getOrigin(request),
|
|
128
128
|
});
|
|
129
|
-
await withAbort(router.load(), session.signal);
|
|
129
|
+
await withAbort(router.load({ _signal: session.signal }), session.signal);
|
|
130
130
|
guard.assertActive();
|
|
131
131
|
|
|
132
132
|
// A native redirect is a terminal HTTP Response. It must never enter
|
|
133
133
|
// the public serializer as a match error or document payload.
|
|
134
|
-
const
|
|
135
|
-
if (redirect) {
|
|
136
|
-
const resolved =
|
|
134
|
+
const loadResult = router._serverResult;
|
|
135
|
+
if (loadResult?.type === 'redirect') {
|
|
136
|
+
const resolved = loadResult.redirect;
|
|
137
137
|
const headers = requestHeaders(router);
|
|
138
138
|
return new Response(null, {
|
|
139
139
|
status: resolved.status,
|
|
@@ -146,7 +146,10 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
|
|
|
146
146
|
const serverSsr = router.serverSsr;
|
|
147
147
|
if (!serverSsr)
|
|
148
148
|
throw new Error('Native Octane router SSR state is required');
|
|
149
|
-
await withAbort(
|
|
149
|
+
await withAbort(
|
|
150
|
+
serverSsr.dehydrate({ signal: session.signal }),
|
|
151
|
+
session.signal,
|
|
152
|
+
);
|
|
150
153
|
guard.assertActive();
|
|
151
154
|
const responseHeaders = requestHeaders(router);
|
|
152
155
|
// A callback that ignores cancellation must not hold the router SSR
|
package/src/router-server.ts
CHANGED
|
@@ -5,8 +5,6 @@ import { createElement, type ServerRenderNode } from 'octane/server';
|
|
|
5
5
|
export * from '@octanejs/tanstack-router/ssr/server';
|
|
6
6
|
export type { OctaneRequestHandlerOptions } from './router-handler';
|
|
7
7
|
export { createOctaneRequestHandler } from './router-handler';
|
|
8
|
-
export type { OctaneRouterInjectionOptions } from './router-injection';
|
|
9
|
-
export { createOctaneRouterInjection } from './router-injection';
|
|
10
8
|
export type {
|
|
11
9
|
OctaneRouterSerializationGuard,
|
|
12
10
|
OctaneRouterSerializationOptions,
|
package/src/router.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useMatch,
|
|
10
10
|
useRouter,
|
|
11
11
|
} from '@octanejs/tanstack-router';
|
|
12
|
-
import { createSubSlot, useCallback,
|
|
12
|
+
import { createSubSlot, useCallback, useLayoutEffect, useRef } from 'octane';
|
|
13
13
|
import { assertOctaneIdentity } from './bootstrap';
|
|
14
14
|
import { resolveRouteData } from './routes';
|
|
15
15
|
|
|
@@ -161,7 +161,7 @@ export function useOctaneRouteAction(...args: unknown[]) {
|
|
|
161
161
|
const identity = useApplicationIdentity();
|
|
162
162
|
const routeId = useApplicationRouteId();
|
|
163
163
|
const owner = useRef<AbortController | null>(null, actionSlot(slot, 'owner'));
|
|
164
|
-
|
|
164
|
+
useLayoutEffect(
|
|
165
165
|
() => {
|
|
166
166
|
const controller = new AbortController();
|
|
167
167
|
owner.current = controller;
|
package/src/routes.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function resolveRouteData(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
interface ReactiveNativeMatch {
|
|
62
|
-
get(): AnyRouteMatch;
|
|
62
|
+
get(): AnyRouteMatch | undefined;
|
|
63
63
|
subscribe(callback: () => void): { unsubscribe(): void };
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -75,24 +75,18 @@ function isReactiveNativeMatch(value: unknown): value is ReactiveNativeMatch {
|
|
|
75
75
|
/** A transport failure belongs to the native generation that fetched it. */
|
|
76
76
|
function observeRouteCompletion(
|
|
77
77
|
router: AnyRouter,
|
|
78
|
+
routeId: string,
|
|
78
79
|
controller: AbortController,
|
|
79
80
|
signal: AbortSignal,
|
|
80
81
|
completion: Promise<void>,
|
|
81
82
|
) {
|
|
82
83
|
void completion.catch(error => {
|
|
83
84
|
if (signal.aborted) return;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!match) return;
|
|
90
|
-
const store = [
|
|
91
|
-
router.stores.pendingMatchStores.get(match.id),
|
|
92
|
-
router.stores.matchStores.get(match.id),
|
|
93
|
-
router.stores.cachedMatchStores.get(match.id),
|
|
94
|
-
].find(candidate => candidate?.get().abortController === controller);
|
|
95
|
-
if (!store) return;
|
|
85
|
+
const route = Object.values(router.routesById).find(
|
|
86
|
+
candidate => candidate.options.staticData?.ultramodernRouteId === routeId,
|
|
87
|
+
);
|
|
88
|
+
if (!route) return;
|
|
89
|
+
const store = router.stores.getMatchStore(route.id);
|
|
96
90
|
let subscription: { unsubscribe(): void } | undefined;
|
|
97
91
|
let settled = false;
|
|
98
92
|
const cleanup = () => {
|
|
@@ -103,9 +97,10 @@ function observeRouteCompletion(
|
|
|
103
97
|
settled = true;
|
|
104
98
|
cleanup();
|
|
105
99
|
};
|
|
106
|
-
const publish = () => {
|
|
100
|
+
const publish = async () => {
|
|
107
101
|
if (settled) return;
|
|
108
102
|
const current = store.get();
|
|
103
|
+
if (!current) return;
|
|
109
104
|
if (
|
|
110
105
|
current.abortController !== controller ||
|
|
111
106
|
signal.aborted ||
|
|
@@ -121,23 +116,29 @@ function observeRouteCompletion(
|
|
|
121
116
|
const route = router.routesById[current.routeId];
|
|
122
117
|
let failure = error;
|
|
123
118
|
try {
|
|
119
|
+
await route?.options.errorComponent?.preload?.();
|
|
120
|
+
if (signal.aborted || store.get()?.abortController !== controller)
|
|
121
|
+
return;
|
|
124
122
|
route?.options.onError?.(failure);
|
|
125
123
|
} catch (onErrorFailure) {
|
|
126
124
|
failure = onErrorFailure;
|
|
127
125
|
}
|
|
128
|
-
//
|
|
126
|
+
// The error component is ready before publishing a late transport failure.
|
|
129
127
|
if (signal.aborted) return;
|
|
130
|
-
router.
|
|
131
|
-
previous
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
router.stores.setMatches(
|
|
129
|
+
router.stores.matches.get().map(previous =>
|
|
130
|
+
previous.routeId === route.id &&
|
|
131
|
+
previous.abortController === controller &&
|
|
132
|
+
previous.status === 'success' &&
|
|
133
|
+
!previous.isFetching
|
|
134
|
+
? {
|
|
135
|
+
...previous,
|
|
136
|
+
error: failure,
|
|
137
|
+
status: 'error',
|
|
138
|
+
isFetching: false,
|
|
139
|
+
}
|
|
140
|
+
: previous,
|
|
141
|
+
),
|
|
141
142
|
);
|
|
142
143
|
};
|
|
143
144
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -203,6 +204,7 @@ export function createFileSystemRouteTree<Context = unknown>(
|
|
|
203
204
|
if (failed) throw failure;
|
|
204
205
|
observeRouteCompletion(
|
|
205
206
|
router,
|
|
207
|
+
route.id,
|
|
206
208
|
match.abortController,
|
|
207
209
|
signal,
|
|
208
210
|
outcome.completion,
|