@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +371 -0
- package/dist/cjs/entry-application.js +83 -0
- package/dist/cjs/entry-client.js +92 -0
- package/dist/cjs/entry-server.js +127 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +103 -0
- package/dist/cjs/i18n/I18nProvider.js +67 -0
- package/dist/cjs/i18n/LocalizedLink.js +89 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +86 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +125 -0
- package/dist/cjs/plugin/create.js +265 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +89 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-handler.js +168 -0
- package/dist/cjs/router-injection.js +139 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +108 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +187 -0
- package/dist/cjs/server.js +224 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +327 -0
- package/dist/esm/entry-application.mjs +39 -0
- package/dist/esm/entry-client.mjs +54 -0
- package/dist/esm/entry-server.mjs +86 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +65 -0
- package/dist/esm/i18n/I18nProvider.mjs +29 -0
- package/dist/esm/i18n/LocalizedLink.mjs +48 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-handler.mjs +130 -0
- package/dist/esm/router-injection.mjs +101 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +12 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +183 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +70 -0
- package/dist/esm-node/plugin/create.mjs +227 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +51 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +36 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +8 -0
- package/dist/types/entry-server.d.ts +11 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +18 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +41 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-injection.d.ts +8 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +13 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +18 -0
- package/dist/types/server.d.ts +41 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/package.json +216 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +485 -0
- package/src/entry-application.ts +72 -0
- package/src/entry-client.ts +80 -0
- package/src/entry-server.ts +142 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +158 -0
- package/src/i18n/I18nProvider.ts +56 -0
- package/src/i18n/LocalizedLink.ts +112 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +35 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +178 -0
- package/src/plugin/compiler.ts +112 -0
- package/src/plugin/create.ts +272 -0
- package/src/plugin/index.ts +105 -0
- package/src/plugin/profile.ts +46 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-handler.ts +182 -0
- package/src/router-injection.ts +145 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +23 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +263 -0
- package/src/server.ts +300 -0
- package/src/typecheck.ts +723 -0
- package/README.md +0 -3
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { assertRendererIdentity } from "@modern-js/renderer-core/identity";
|
|
2
|
+
import { createRoot, hydrateRoot } from "octane";
|
|
3
|
+
import { bootstrapStreamedSignalHydration } from "octane/hydration/streamed-signals";
|
|
4
|
+
import { assertNativeHydrationBuildId, assertOctaneIdentity, readOctaneDocumentBootstrap } from "./bootstrap.mjs";
|
|
5
|
+
import { FederationApplication, createFederationScope } from "./federation-context.mjs";
|
|
6
|
+
const roots = new WeakMap();
|
|
7
|
+
const documents = new WeakMap();
|
|
8
|
+
const moduleResources = new WeakMap();
|
|
9
|
+
function claimModuleResources(application) {
|
|
10
|
+
const dispose = application.dispose;
|
|
11
|
+
if (!dispose) return ()=>{};
|
|
12
|
+
let resources = moduleResources.get(dispose);
|
|
13
|
+
if (resources?.retired) throw new Error('A retired Octane application requires a fresh module resource instance.');
|
|
14
|
+
if (!resources) {
|
|
15
|
+
resources = {
|
|
16
|
+
users: 0,
|
|
17
|
+
retired: false
|
|
18
|
+
};
|
|
19
|
+
moduleResources.set(dispose, resources);
|
|
20
|
+
}
|
|
21
|
+
resources.users++;
|
|
22
|
+
let released = false;
|
|
23
|
+
return ()=>{
|
|
24
|
+
if (released) return;
|
|
25
|
+
released = true;
|
|
26
|
+
resources.users--;
|
|
27
|
+
if (0 === resources.users && !resources.retired) {
|
|
28
|
+
resources.retired = true;
|
|
29
|
+
dispose();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function retireAbandonedModule(application) {
|
|
34
|
+
if (application.dispose && moduleResources.get(application.dispose)?.users) return;
|
|
35
|
+
if (application.dispose && moduleResources.get(application.dispose)?.retired) return;
|
|
36
|
+
claimModuleResources(application)();
|
|
37
|
+
}
|
|
38
|
+
function observeRootErrors(options) {
|
|
39
|
+
let failed = false;
|
|
40
|
+
let failure;
|
|
41
|
+
let handle;
|
|
42
|
+
return {
|
|
43
|
+
options: {
|
|
44
|
+
...options,
|
|
45
|
+
onUncaughtError (error) {
|
|
46
|
+
if (failed) return;
|
|
47
|
+
failed = true;
|
|
48
|
+
failure = error;
|
|
49
|
+
if (handle) {
|
|
50
|
+
const failedHandle = handle;
|
|
51
|
+
queueMicrotask(()=>{
|
|
52
|
+
try {
|
|
53
|
+
failedHandle.dispose();
|
|
54
|
+
} catch (cleanupError) {
|
|
55
|
+
failure = new AggregateError([
|
|
56
|
+
error,
|
|
57
|
+
cleanupError
|
|
58
|
+
], 'Octane rendering and cleanup failed.');
|
|
59
|
+
}
|
|
60
|
+
if (options?.onUncaughtError) options.onUncaughtError(failure);
|
|
61
|
+
else throw failure;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
attach (next) {
|
|
67
|
+
handle = next;
|
|
68
|
+
},
|
|
69
|
+
assertInitialRender () {
|
|
70
|
+
if (failed) {
|
|
71
|
+
options?.onUncaughtError?.(failure);
|
|
72
|
+
throw failure;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function claimRoot(container) {
|
|
78
|
+
if (9 === container.nodeType) throw new Error('An Octane application requires a subtree root in its framework-owned document.');
|
|
79
|
+
if (roots.has(container)) throw new Error('An Octane application already owns this root container.');
|
|
80
|
+
const owner = Symbol('Octane root owner');
|
|
81
|
+
roots.set(container, owner);
|
|
82
|
+
return ()=>{
|
|
83
|
+
if (roots.get(container) === owner) roots.delete(container);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function cleanupResources(cleanups) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
for (const cleanup of cleanups)try {
|
|
89
|
+
cleanup();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
errors.push(error);
|
|
92
|
+
}
|
|
93
|
+
if (errors.length) throw new AggregateError(errors, 'Octane application cleanup failed.');
|
|
94
|
+
}
|
|
95
|
+
function reportCleanupError(input, error) {
|
|
96
|
+
if (input.options?.onUncaughtError) input.options.onUncaughtError(error);
|
|
97
|
+
else queueMicrotask(()=>{
|
|
98
|
+
throw error;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function loadApplication(input, abandon) {
|
|
102
|
+
const acquire = (application)=>{
|
|
103
|
+
try {
|
|
104
|
+
validateModule(application);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (application && 'function' == typeof application.dispose) retireAbandonedModule(application);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
application,
|
|
111
|
+
releaseModule: claimModuleResources(application)
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const { signal } = input;
|
|
115
|
+
if (!signal) return input.load().then(acquire);
|
|
116
|
+
return new Promise((resolve, reject)=>{
|
|
117
|
+
let settled = false;
|
|
118
|
+
let abandoned = false;
|
|
119
|
+
const onAbort = ()=>{
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
abandoned = true;
|
|
123
|
+
signal.removeEventListener('abort', onAbort);
|
|
124
|
+
try {
|
|
125
|
+
abandon();
|
|
126
|
+
reject(signal.reason);
|
|
127
|
+
} catch (cleanupError) {
|
|
128
|
+
reject(new AggregateError([
|
|
129
|
+
signal.reason,
|
|
130
|
+
cleanupError
|
|
131
|
+
], 'Octane startup cancellation failed.'));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
signal.addEventListener('abort', onAbort, {
|
|
135
|
+
once: true
|
|
136
|
+
});
|
|
137
|
+
if (signal.aborted) return void onAbort();
|
|
138
|
+
let loading;
|
|
139
|
+
try {
|
|
140
|
+
loading = input.load();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
settled = true;
|
|
143
|
+
signal.removeEventListener('abort', onAbort);
|
|
144
|
+
reject(error);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
loading.then((application)=>{
|
|
148
|
+
if (abandoned) return void queueMicrotask(()=>{
|
|
149
|
+
try {
|
|
150
|
+
retireAbandonedModule(application);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
reportCleanupError(input, error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
settled = true;
|
|
156
|
+
signal.removeEventListener('abort', onAbort);
|
|
157
|
+
try {
|
|
158
|
+
resolve(acquire(application));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
reject(error);
|
|
161
|
+
}
|
|
162
|
+
}, (error)=>{
|
|
163
|
+
if (settled) return;
|
|
164
|
+
settled = true;
|
|
165
|
+
signal.removeEventListener('abort', onAbort);
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function validateModule(application) {
|
|
171
|
+
if (!application || 'function' != typeof application.default) throw new TypeError('An Octane application module must default-export a native component.');
|
|
172
|
+
if (void 0 !== application.dispose && 'function' != typeof application.dispose) throw new TypeError('An Octane application disposer must be a function.');
|
|
173
|
+
}
|
|
174
|
+
function createHandle(input) {
|
|
175
|
+
let releaseModule = input.releaseModule;
|
|
176
|
+
let disposed = false;
|
|
177
|
+
const handle = {
|
|
178
|
+
root: input.root,
|
|
179
|
+
identity: Object.freeze({
|
|
180
|
+
...input.identity
|
|
181
|
+
}),
|
|
182
|
+
update (next) {
|
|
183
|
+
if (disposed) throw new Error('Cannot update a disposed Octane application.');
|
|
184
|
+
validateModule(next);
|
|
185
|
+
const previousRelease = releaseModule;
|
|
186
|
+
const nextRelease = claimModuleResources(next);
|
|
187
|
+
releaseModule = nextRelease;
|
|
188
|
+
try {
|
|
189
|
+
input.federation ? input.root.render(FederationApplication, {
|
|
190
|
+
scope: input.federation,
|
|
191
|
+
application: next
|
|
192
|
+
}) : input.root.render(next.default, next.props);
|
|
193
|
+
previousRelease();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
cleanupResources([
|
|
196
|
+
previousRelease,
|
|
197
|
+
()=>handle.dispose()
|
|
198
|
+
]);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
dispose () {
|
|
203
|
+
if (disposed) return;
|
|
204
|
+
disposed = true;
|
|
205
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
206
|
+
cleanupResources([
|
|
207
|
+
()=>input.root.unmount(),
|
|
208
|
+
()=>releaseModule(),
|
|
209
|
+
()=>input.bridge?.dispose(),
|
|
210
|
+
input.release
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function onAbort() {
|
|
215
|
+
queueMicrotask(()=>{
|
|
216
|
+
try {
|
|
217
|
+
handle.dispose();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
input.onCleanupError?.(error);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
input.signal?.addEventListener('abort', onAbort, {
|
|
224
|
+
once: true
|
|
225
|
+
});
|
|
226
|
+
return handle;
|
|
227
|
+
}
|
|
228
|
+
async function startApplication(input, release, hydration) {
|
|
229
|
+
const errors = observeRootErrors(input.options);
|
|
230
|
+
let bridge;
|
|
231
|
+
let root;
|
|
232
|
+
let releaseModule;
|
|
233
|
+
let handle;
|
|
234
|
+
let cleaned = false;
|
|
235
|
+
const cleanup = ()=>{
|
|
236
|
+
if (handle) return void handle.dispose();
|
|
237
|
+
if (cleaned) return;
|
|
238
|
+
cleaned = true;
|
|
239
|
+
cleanupResources([
|
|
240
|
+
()=>root?.unmount(),
|
|
241
|
+
()=>releaseModule?.(),
|
|
242
|
+
()=>bridge?.dispose(),
|
|
243
|
+
release
|
|
244
|
+
]);
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
const federation = input.federation ? createFederationScope(input.federation, false) : void 0;
|
|
248
|
+
if (hydration) {
|
|
249
|
+
const { document, documentId } = hydration;
|
|
250
|
+
bridge = bootstrapStreamedSignalHydration({
|
|
251
|
+
buildId: input.nativeHydrationBuildId,
|
|
252
|
+
documentId,
|
|
253
|
+
...null === document.defaultView ? {} : {
|
|
254
|
+
target: document.defaultView
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const loaded = await loadApplication(input, cleanup);
|
|
259
|
+
const application = loaded.application;
|
|
260
|
+
releaseModule = loaded.releaseModule;
|
|
261
|
+
input.signal?.throwIfAborted();
|
|
262
|
+
if (bridge) root = hydrateRoot(input.container, federation ? FederationApplication : application.default, federation ? {
|
|
263
|
+
scope: federation,
|
|
264
|
+
application
|
|
265
|
+
} : application.props, {
|
|
266
|
+
...errors.options,
|
|
267
|
+
signalOwner: bridge.signalOwner
|
|
268
|
+
});
|
|
269
|
+
else {
|
|
270
|
+
root = createRoot(input.container, errors.options);
|
|
271
|
+
federation ? root.render(FederationApplication, {
|
|
272
|
+
scope: federation,
|
|
273
|
+
application
|
|
274
|
+
}) : root.render(application.default, application.props);
|
|
275
|
+
}
|
|
276
|
+
errors.assertInitialRender();
|
|
277
|
+
input.signal?.throwIfAborted();
|
|
278
|
+
handle = createHandle({
|
|
279
|
+
root,
|
|
280
|
+
identity: input.identity,
|
|
281
|
+
releaseModule,
|
|
282
|
+
release,
|
|
283
|
+
bridge,
|
|
284
|
+
federation,
|
|
285
|
+
...void 0 === input.signal ? {} : {
|
|
286
|
+
signal: input.signal
|
|
287
|
+
},
|
|
288
|
+
onCleanupError: (error)=>reportCleanupError(input, error)
|
|
289
|
+
});
|
|
290
|
+
errors.attach(handle);
|
|
291
|
+
return handle;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
cleanup();
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function mountOctaneApplication(input) {
|
|
298
|
+
assertOctaneIdentity(input.identity);
|
|
299
|
+
assertNativeHydrationBuildId(input.nativeHydrationBuildId);
|
|
300
|
+
input.signal?.throwIfAborted();
|
|
301
|
+
return startApplication(input, claimRoot(input.container));
|
|
302
|
+
}
|
|
303
|
+
async function hydrateOctaneApplication(input) {
|
|
304
|
+
assertOctaneIdentity(input.identity);
|
|
305
|
+
assertNativeHydrationBuildId(input.nativeHydrationBuildId);
|
|
306
|
+
assertNativeHydrationBuildId(input.documentNativeHydrationBuildId);
|
|
307
|
+
if (input.nativeHydrationBuildId !== input.documentNativeHydrationBuildId) throw new Error('Octane hydration bytes belong to a different native client compilation.');
|
|
308
|
+
assertRendererIdentity(input.documentIdentity, input.identity);
|
|
309
|
+
input.signal?.throwIfAborted();
|
|
310
|
+
if ('string' != typeof input.documentId || 0 === input.documentId.trim().length) throw new Error('Octane hydration requires a nonempty document identity.');
|
|
311
|
+
const document = input.container.ownerDocument;
|
|
312
|
+
if (!document) throw new Error('An Octane hydration root requires a document.');
|
|
313
|
+
if (document !== globalThis.document) throw new Error('Octane hydration requires a root in the active browser document.');
|
|
314
|
+
if (documents.has(document)) throw new Error('An Octane signal bridge already owns this document.');
|
|
315
|
+
const releaseRoot = claimRoot(input.container);
|
|
316
|
+
const documentOwner = Symbol(input.documentId);
|
|
317
|
+
documents.set(document, documentOwner);
|
|
318
|
+
const release = ()=>{
|
|
319
|
+
releaseRoot();
|
|
320
|
+
if (documents.get(document) === documentOwner) documents.delete(document);
|
|
321
|
+
};
|
|
322
|
+
return startApplication(input, release, {
|
|
323
|
+
document,
|
|
324
|
+
documentId: input.documentId
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export { hydrateOctaneApplication, mountOctaneApplication, readOctaneDocumentBootstrap };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createMemoryHistory, createRouter } from "@octanejs/tanstack-router";
|
|
2
|
+
import { createElement } from "octane";
|
|
3
|
+
import { I18nProvider } from "./i18n/I18nProvider.mjs";
|
|
4
|
+
import { createFileSystemRouteTree } from "./routes.mjs";
|
|
5
|
+
function componentView(component, i18n) {
|
|
6
|
+
const App = component;
|
|
7
|
+
if (!i18n) return App;
|
|
8
|
+
return ()=>createElement(I18nProvider, {
|
|
9
|
+
instance: i18n.instance,
|
|
10
|
+
languages: i18n.languages,
|
|
11
|
+
localisedUrls: i18n.localisedUrls,
|
|
12
|
+
children: createElement(App)
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const octaneRouterFactory = {
|
|
16
|
+
routeTree: (routes, modules, options)=>createFileSystemRouteTree(routes, modules, options),
|
|
17
|
+
router: ({ location, ...options })=>createRouter({
|
|
18
|
+
...options,
|
|
19
|
+
...location ? {
|
|
20
|
+
origin: location.origin,
|
|
21
|
+
history: createMemoryHistory({
|
|
22
|
+
initialEntries: [
|
|
23
|
+
location.href
|
|
24
|
+
]
|
|
25
|
+
})
|
|
26
|
+
} : {}
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
function localizedRouterRoot(Root) {
|
|
30
|
+
return ({ router, i18n })=>createElement(I18nProvider, {
|
|
31
|
+
instance: i18n.instance,
|
|
32
|
+
languages: i18n.languages,
|
|
33
|
+
localisedUrls: i18n.localisedUrls,
|
|
34
|
+
children: createElement(Root, {
|
|
35
|
+
router
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export { componentView, localizedRouterRoot, octaneRouterFactory };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { startNativeClientEntry } from "@modern-js/renderer-core/entry-client";
|
|
2
|
+
import { createNativeRouter } from "@modern-js/renderer-core/router";
|
|
3
|
+
import { hydrateOctaneApplication, mountOctaneApplication, readOctaneDocumentBootstrap } from "./client.mjs";
|
|
4
|
+
import { componentView, localizedRouterRoot, octaneRouterFactory } from "./entry-application.mjs";
|
|
5
|
+
import { OctaneRouterRoot, prepareOctaneRouterHydration } from "./router-client.mjs";
|
|
6
|
+
const LocalizedRouterRoot = localizedRouterRoot(OctaneRouterRoot);
|
|
7
|
+
function startNativeClient(options) {
|
|
8
|
+
const { nativeHydrationBuildId } = options;
|
|
9
|
+
startNativeClientEntry(options, {
|
|
10
|
+
readBootstrap: (document, identity)=>readOctaneDocumentBootstrap(document, identity, nativeHydrationBuildId),
|
|
11
|
+
createRouter: (application, routerOptions)=>createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
12
|
+
async prepareRouter (router, hydrating, signal) {
|
|
13
|
+
if (hydrating) await prepareOctaneRouterHydration(router, {
|
|
14
|
+
signal
|
|
15
|
+
});
|
|
16
|
+
else await router.load();
|
|
17
|
+
},
|
|
18
|
+
async start ({ root, identity, bootstrap, signal, load }) {
|
|
19
|
+
const application = {
|
|
20
|
+
container: root,
|
|
21
|
+
identity,
|
|
22
|
+
nativeHydrationBuildId,
|
|
23
|
+
federation: options.federation,
|
|
24
|
+
signal,
|
|
25
|
+
load: async ()=>{
|
|
26
|
+
const view = await load();
|
|
27
|
+
if ('component' === view.kind) return {
|
|
28
|
+
default: componentView(view.component, view.i18n)
|
|
29
|
+
};
|
|
30
|
+
return view.i18n ? {
|
|
31
|
+
default: LocalizedRouterRoot,
|
|
32
|
+
props: {
|
|
33
|
+
router: view.router,
|
|
34
|
+
i18n: view.i18n
|
|
35
|
+
}
|
|
36
|
+
} : {
|
|
37
|
+
default: OctaneRouterRoot,
|
|
38
|
+
props: {
|
|
39
|
+
router: view.router
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const handle = bootstrap?.hydrating ? await hydrateOctaneApplication({
|
|
45
|
+
...application,
|
|
46
|
+
documentIdentity: bootstrap.identity,
|
|
47
|
+
documentId: bootstrap.documentId,
|
|
48
|
+
documentNativeHydrationBuildId: bootstrap.nativeHydrationBuildId
|
|
49
|
+
}) : await mountOctaneApplication(application);
|
|
50
|
+
return ()=>handle.dispose();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export { startNativeClient };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { collectDataHeaders, dataMetadataToDocumentPolicy, mergeDataResponseIntoResponse, mergeDataResponseMetadata, mergeHeaderFields } from "@modern-js/renderer-core/data";
|
|
2
|
+
import { createNativeServerEntry } from "@modern-js/renderer-core/entry-server";
|
|
3
|
+
import { createNativeRouter } from "@modern-js/renderer-core/router";
|
|
4
|
+
import { componentView, localizedRouterRoot, octaneRouterFactory } from "./entry-application.mjs";
|
|
5
|
+
import { validateOctaneModuleManifest } from "./manifest.mjs";
|
|
6
|
+
import { OctaneRouterServer, createOctaneRequestHandler, createOctaneRouterInjection, createSsrStreamResponse } from "./router-server.mjs";
|
|
7
|
+
import { renderOctaneApplication, renderOctaneCSRDocument } from "./server.mjs";
|
|
8
|
+
const OCTANE_DOCUMENT_FIELDS = new Set([
|
|
9
|
+
'content-language',
|
|
10
|
+
'content-location',
|
|
11
|
+
'content-disposition'
|
|
12
|
+
]);
|
|
13
|
+
function withOctaneDocumentFields(policy, responseHeaders) {
|
|
14
|
+
const fields = collectDataHeaders(responseHeaders).filter(([name])=>OCTANE_DOCUMENT_FIELDS.has(name.toLowerCase()));
|
|
15
|
+
return fields.length ? {
|
|
16
|
+
...policy,
|
|
17
|
+
headers: [
|
|
18
|
+
...policy.headers,
|
|
19
|
+
...fields
|
|
20
|
+
]
|
|
21
|
+
} : policy;
|
|
22
|
+
}
|
|
23
|
+
const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
|
|
24
|
+
function entry_server_createNativeServerEntry(options) {
|
|
25
|
+
return createNativeServerEntry(options, {
|
|
26
|
+
document: (document, nativeManifest, identity)=>({
|
|
27
|
+
...document,
|
|
28
|
+
nativeHydrationBuildId: validateOctaneModuleManifest(nativeManifest, identity).nativeHydrationBuildId
|
|
29
|
+
}),
|
|
30
|
+
run: (_session, callback)=>callback(),
|
|
31
|
+
respond: (_session, response)=>response,
|
|
32
|
+
createRouter: (application, routerOptions)=>createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
33
|
+
renderCSR: ({ session }, document)=>renderOctaneCSRDocument({
|
|
34
|
+
session,
|
|
35
|
+
document
|
|
36
|
+
}),
|
|
37
|
+
renderComponent: ({ session }, component, document, i18n)=>renderOctaneApplication({
|
|
38
|
+
session,
|
|
39
|
+
App: componentView(component, i18n),
|
|
40
|
+
document,
|
|
41
|
+
federation: options.federation
|
|
42
|
+
}),
|
|
43
|
+
async renderRoutes ({ request, context: { session }, router, outcomes, document, forbiddenValues, i18n }) {
|
|
44
|
+
const response = await createOctaneRequestHandler({
|
|
45
|
+
request,
|
|
46
|
+
session,
|
|
47
|
+
createRouter: ()=>router,
|
|
48
|
+
serialization: {
|
|
49
|
+
forbiddenValues
|
|
50
|
+
}
|
|
51
|
+
})(async ({ router, responseHeaders })=>{
|
|
52
|
+
const metadata = mergeDataResponseMetadata(outcomes, {
|
|
53
|
+
status: router.state.statusCode ?? 200
|
|
54
|
+
});
|
|
55
|
+
const policy = dataMetadataToDocumentPolicy({
|
|
56
|
+
...metadata,
|
|
57
|
+
headers: mergeHeaderFields(collectDataHeaders(responseHeaders), metadata.headers)
|
|
58
|
+
});
|
|
59
|
+
session.resolveResponse(withOctaneDocumentFields(policy, responseHeaders));
|
|
60
|
+
const rendered = await renderOctaneApplication({
|
|
61
|
+
session,
|
|
62
|
+
...i18n ? {
|
|
63
|
+
App: LocalizedRouterServer,
|
|
64
|
+
props: {
|
|
65
|
+
router,
|
|
66
|
+
i18n
|
|
67
|
+
}
|
|
68
|
+
} : {
|
|
69
|
+
App: OctaneRouterServer,
|
|
70
|
+
props: {
|
|
71
|
+
router
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
document,
|
|
75
|
+
federation: options.federation,
|
|
76
|
+
injection: createOctaneRouterInjection(router, session)
|
|
77
|
+
});
|
|
78
|
+
return createSsrStreamResponse(router, rendered);
|
|
79
|
+
});
|
|
80
|
+
return session.committedPolicy ? response : mergeDataResponseIntoResponse(response, mergeDataResponseMetadata(outcomes, {
|
|
81
|
+
status: response.status
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export { entry_server_createNativeServerEntry as createNativeServerEntry, withOctaneDocumentFields };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getFederationHost } from "@modern-js/renderer-core/federation";
|
|
2
|
+
import { createContext, createElement } from "octane";
|
|
3
|
+
const FederationContext = createContext(null);
|
|
4
|
+
function createFederationScope(binding, server, nonce) {
|
|
5
|
+
return {
|
|
6
|
+
instance: getFederationHost(binding),
|
|
7
|
+
server,
|
|
8
|
+
...void 0 === nonce ? {} : {
|
|
9
|
+
nonce
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function FederationRoot(props) {
|
|
14
|
+
return createElement(FederationContext, {
|
|
15
|
+
value: props.scope,
|
|
16
|
+
children: props.children
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function FederationApplication(props) {
|
|
20
|
+
return createElement(FederationRoot, {
|
|
21
|
+
scope: props.scope,
|
|
22
|
+
children: createElement(props.application.default, props.application.props)
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export { FederationApplication, FederationContext, FederationRoot, createFederationScope };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { loadFederatedModule, remoteBrowserAssets } from "@modern-js/renderer-core/federation";
|
|
2
|
+
import { Suspense, createElement, lazy, preinit, useContext } from "octane";
|
|
3
|
+
import { FederationContext } from "./federation-context.mjs";
|
|
4
|
+
function withTimeout(promise, timeout, label) {
|
|
5
|
+
if (void 0 === timeout) return promise;
|
|
6
|
+
return new Promise((resolve, reject)=>{
|
|
7
|
+
const timer = setTimeout(()=>reject(new Error(`Remote module ${label} did not load within ${timeout}ms`)), timeout);
|
|
8
|
+
timer.unref?.();
|
|
9
|
+
promise.then((value)=>{
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
resolve(value);
|
|
12
|
+
}, (error)=>{
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
reject(error);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function federatedComponent(remote, options = {}) {
|
|
19
|
+
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
|
+
if (void 0 !== options.timeout && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new TypeError('federatedComponent timeout must be a positive number of milliseconds');
|
|
21
|
+
const id = 'string' == typeof remote ? remote : void 0;
|
|
22
|
+
const standalone = {
|
|
23
|
+
instance: void 0,
|
|
24
|
+
server: "u" < typeof document
|
|
25
|
+
};
|
|
26
|
+
const components = new WeakMap();
|
|
27
|
+
return (props)=>{
|
|
28
|
+
const context = useContext(FederationContext) ?? standalone;
|
|
29
|
+
let Remote = components.get(context);
|
|
30
|
+
if (!Remote) {
|
|
31
|
+
Remote = lazy(async ()=>{
|
|
32
|
+
try {
|
|
33
|
+
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');
|
|
35
|
+
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
|
+
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;
|
|
39
|
+
const Loaded = (props)=>{
|
|
40
|
+
for (const href of assets?.css ?? [])preinit(href, {
|
|
41
|
+
as: 'style',
|
|
42
|
+
precedence: 'federated',
|
|
43
|
+
...void 0 === context.nonce ? {} : {
|
|
44
|
+
nonce: context.nonce
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return createElement(component, props);
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
default: Loaded
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!context.server) components.delete(context);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
components.set(context, Remote);
|
|
58
|
+
}
|
|
59
|
+
return createElement(Suspense, {
|
|
60
|
+
fallback: options.fallback,
|
|
61
|
+
children: createElement(Remote, props)
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export { federatedComponent };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createElement, hookSlots, useEffect, useState } from "octane";
|
|
2
|
+
import { I18nContext } from "./context.mjs";
|
|
3
|
+
const languageStateSlot = Symbol(hookSlots(1));
|
|
4
|
+
const languageSubscriptionSlot = Symbol(hookSlots(1));
|
|
5
|
+
function I18nProvider(props) {
|
|
6
|
+
const [language, setLanguage] = useState(()=>props.instance.language, languageStateSlot);
|
|
7
|
+
useEffect(()=>{
|
|
8
|
+
const instance = props.instance;
|
|
9
|
+
setLanguage(instance.language);
|
|
10
|
+
const handleLanguageChanged = (lng)=>setLanguage(lng);
|
|
11
|
+
instance.on?.('languageChanged', handleLanguageChanged);
|
|
12
|
+
return ()=>{
|
|
13
|
+
instance.off?.('languageChanged', handleLanguageChanged);
|
|
14
|
+
};
|
|
15
|
+
}, [
|
|
16
|
+
props.instance
|
|
17
|
+
], languageSubscriptionSlot);
|
|
18
|
+
const value = {
|
|
19
|
+
instance: props.instance,
|
|
20
|
+
languages: props.languages,
|
|
21
|
+
localisedUrls: props.localisedUrls,
|
|
22
|
+
language
|
|
23
|
+
};
|
|
24
|
+
return createElement(I18nContext, {
|
|
25
|
+
value,
|
|
26
|
+
children: props.children
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export { I18nProvider };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { canonicalPath, configuredLanguage, localizePath } from "@modern-js/i18n-runtime-extensions/paths";
|
|
2
|
+
import { createElement, hookSlots, useContext } from "octane";
|
|
3
|
+
import { Link, useLocation, useRouter } from "../router.mjs";
|
|
4
|
+
import { I18nContext } from "./context.mjs";
|
|
5
|
+
const locationSlot = Symbol(hookSlots(1));
|
|
6
|
+
function LocalizedLink(props) {
|
|
7
|
+
const context = useContext(I18nContext);
|
|
8
|
+
if (!context) throw new Error('[renderer-octane] LocalizedLink was used outside of an I18nProvider.');
|
|
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, {
|
|
12
|
+
languages: [
|
|
13
|
+
...context.languages
|
|
14
|
+
],
|
|
15
|
+
localisedUrls: context.localisedUrls
|
|
16
|
+
});
|
|
17
|
+
const crossLanguage = language !== context.language;
|
|
18
|
+
const rewrite = Boolean(router.options.rewrite);
|
|
19
|
+
return createElement(Link, {
|
|
20
|
+
to: rewrite ? props.to : href,
|
|
21
|
+
...rewrite && crossLanguage ? {
|
|
22
|
+
mask: {
|
|
23
|
+
to: href
|
|
24
|
+
}
|
|
25
|
+
} : {},
|
|
26
|
+
...crossLanguage ? {
|
|
27
|
+
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;
|
|
47
|
+
}
|
|
48
|
+
export { LocalizedLink, useIsLocalizedActive };
|