@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.10

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/bin/octane-tsc.mjs +4 -0
  3. package/dist/cjs/bootstrap.js +68 -0
  4. package/dist/cjs/client.js +421 -0
  5. package/dist/cjs/entry-application.js +88 -0
  6. package/dist/cjs/entry-client.js +95 -0
  7. package/dist/cjs/entry-server.js +135 -0
  8. package/dist/cjs/federation-context.js +72 -0
  9. package/dist/cjs/federation.js +205 -0
  10. package/dist/cjs/i18n/I18nProvider.js +59 -0
  11. package/dist/cjs/i18n/LocalizedLink.js +80 -0
  12. package/dist/cjs/i18n/context.js +41 -0
  13. package/dist/cjs/i18n/index.js +83 -0
  14. package/dist/cjs/i18n/languageSync.js +99 -0
  15. package/dist/cjs/i18n/types.js +18 -0
  16. package/dist/cjs/i18n/useI18n.js +110 -0
  17. package/dist/cjs/i18n-external-snapshot.js +645 -0
  18. package/dist/cjs/index.js +46 -0
  19. package/dist/cjs/manifest.js +167 -0
  20. package/dist/cjs/plugin/compiler.js +122 -0
  21. package/dist/cjs/plugin/create.js +266 -0
  22. package/dist/cjs/plugin/index.js +151 -0
  23. package/dist/cjs/plugin/profile.js +91 -0
  24. package/dist/cjs/plugin/source.js +56 -0
  25. package/dist/cjs/router-client.js +139 -0
  26. package/dist/cjs/router-external-snapshot.js +291 -0
  27. package/dist/cjs/router-handler.js +171 -0
  28. package/dist/cjs/router-server-cleanup.js +46 -0
  29. package/dist/cjs/router-server-snapshot.js +405 -0
  30. package/dist/cjs/router-server.js +100 -0
  31. package/dist/cjs/router-snapshot.js +270 -0
  32. package/dist/cjs/router.js +218 -0
  33. package/dist/cjs/routes.js +190 -0
  34. package/dist/cjs/server.js +245 -0
  35. package/dist/cjs/typecheck.js +615 -0
  36. package/dist/cjs/worker-federation-context.js +63 -0
  37. package/dist/cjs/worker-federation-contexts.js +97 -0
  38. package/dist/cjs/worker-federation.js +97 -0
  39. package/dist/esm/bootstrap.mjs +24 -0
  40. package/dist/esm/client.mjs +377 -0
  41. package/dist/esm/entry-application.mjs +44 -0
  42. package/dist/esm/entry-client.mjs +57 -0
  43. package/dist/esm/entry-server.mjs +91 -0
  44. package/dist/esm/federation-context.mjs +25 -0
  45. package/dist/esm/federation.mjs +167 -0
  46. package/dist/esm/i18n/I18nProvider.mjs +21 -0
  47. package/dist/esm/i18n/LocalizedLink.mjs +42 -0
  48. package/dist/esm/i18n/context.mjs +3 -0
  49. package/dist/esm/i18n/index.mjs +8 -0
  50. package/dist/esm/i18n/languageSync.mjs +61 -0
  51. package/dist/esm/i18n/types.mjs +0 -0
  52. package/dist/esm/i18n/useI18n.mjs +72 -0
  53. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  54. package/dist/esm/index.mjs +8 -0
  55. package/dist/esm/manifest.mjs +117 -0
  56. package/dist/esm/router-client.mjs +98 -0
  57. package/dist/esm/router-external-snapshot.mjs +244 -0
  58. package/dist/esm/router-handler.mjs +133 -0
  59. package/dist/esm/router-server-cleanup.mjs +8 -0
  60. package/dist/esm/router-server-snapshot.mjs +367 -0
  61. package/dist/esm/router-server.mjs +11 -0
  62. package/dist/esm/router-snapshot.mjs +199 -0
  63. package/dist/esm/router.mjs +94 -0
  64. package/dist/esm/routes.mjs +134 -0
  65. package/dist/esm/server.mjs +204 -0
  66. package/dist/esm/worker-federation-context.mjs +19 -0
  67. package/dist/esm/worker-federation-contexts.mjs +47 -0
  68. package/dist/esm/worker-federation.mjs +59 -0
  69. package/dist/esm-node/bootstrap.mjs +24 -0
  70. package/dist/esm-node/manifest.mjs +117 -0
  71. package/dist/esm-node/plugin/compiler.mjs +67 -0
  72. package/dist/esm-node/plugin/create.mjs +228 -0
  73. package/dist/esm-node/plugin/index.mjs +103 -0
  74. package/dist/esm-node/plugin/profile.mjs +53 -0
  75. package/dist/esm-node/plugin/source.mjs +7 -0
  76. package/dist/esm-node/typecheck.mjs +560 -0
  77. package/dist/types/bootstrap.d.ts +10 -0
  78. package/dist/types/client.d.ts +40 -0
  79. package/dist/types/entry-application.d.ts +19 -0
  80. package/dist/types/entry-client.d.ts +10 -0
  81. package/dist/types/entry-server.d.ts +14 -0
  82. package/dist/types/federation-context.d.ts +28 -0
  83. package/dist/types/federation.d.ts +20 -0
  84. package/dist/types/i18n/I18nProvider.d.ts +8 -0
  85. package/dist/types/i18n/LocalizedLink.d.ts +14 -0
  86. package/dist/types/i18n/context.d.ts +9 -0
  87. package/dist/types/i18n/index.d.ts +9 -0
  88. package/dist/types/i18n/languageSync.d.ts +26 -0
  89. package/dist/types/i18n/types.d.ts +34 -0
  90. package/dist/types/i18n/useI18n.d.ts +21 -0
  91. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  92. package/dist/types/index.d.ts +9 -0
  93. package/dist/types/manifest.d.ts +34 -0
  94. package/dist/types/plugin/compiler-manifest.d.cts +10 -0
  95. package/dist/types/plugin/compiler.d.ts +5 -0
  96. package/dist/types/plugin/create.d.ts +23 -0
  97. package/dist/types/plugin/index.d.ts +3 -0
  98. package/dist/types/plugin/profile.d.ts +2 -0
  99. package/dist/types/plugin/source.d.ts +1 -0
  100. package/dist/types/plugin/svg-component-template.d.cts +6 -0
  101. package/dist/types/router-client.d.ts +12 -0
  102. package/dist/types/router-external-snapshot.d.ts +10 -0
  103. package/dist/types/router-handler.d.ts +13 -0
  104. package/dist/types/router-server-cleanup.d.ts +3 -0
  105. package/dist/types/router-server-snapshot.d.ts +15 -0
  106. package/dist/types/router-server.d.ts +11 -0
  107. package/dist/types/router-snapshot.d.ts +14 -0
  108. package/dist/types/router.d.ts +30 -0
  109. package/dist/types/routes.d.ts +20 -0
  110. package/dist/types/server.d.ts +47 -0
  111. package/dist/types/typecheck.d.ts +31 -0
  112. package/dist/types/worker-federation-context.d.ts +34 -0
  113. package/dist/types/worker-federation-contexts.d.ts +25 -0
  114. package/dist/types/worker-federation.d.ts +20 -0
  115. package/package.json +278 -4
  116. package/src/bootstrap.ts +60 -0
  117. package/src/client.ts +568 -0
  118. package/src/entry-application.ts +76 -0
  119. package/src/entry-client.ts +90 -0
  120. package/src/entry-server.ts +153 -0
  121. package/src/federation-context.ts +53 -0
  122. package/src/federation.ts +307 -0
  123. package/src/i18n/I18nProvider.ts +47 -0
  124. package/src/i18n/LocalizedLink.ts +86 -0
  125. package/src/i18n/context.ts +12 -0
  126. package/src/i18n/index.ts +31 -0
  127. package/src/i18n/languageSync.ts +132 -0
  128. package/src/i18n/types.ts +36 -0
  129. package/src/i18n/useI18n.ts +119 -0
  130. package/src/i18n-external-snapshot.ts +803 -0
  131. package/src/index.ts +10 -0
  132. package/src/manifest.ts +253 -0
  133. package/src/plugin/compiler-manifest.cjs +204 -0
  134. package/src/plugin/compiler.ts +104 -0
  135. package/src/plugin/create.ts +273 -0
  136. package/src/plugin/index.ts +107 -0
  137. package/src/plugin/profile.ts +50 -0
  138. package/src/plugin/source.ts +16 -0
  139. package/src/plugin/svg-component-template.cjs +17 -0
  140. package/src/router-client.ts +144 -0
  141. package/src/router-external-snapshot.ts +363 -0
  142. package/src/router-handler.ts +185 -0
  143. package/src/router-server-cleanup.ts +11 -0
  144. package/src/router-server-snapshot.ts +564 -0
  145. package/src/router-server.ts +21 -0
  146. package/src/router-snapshot.ts +251 -0
  147. package/src/router.ts +192 -0
  148. package/src/routes.ts +274 -0
  149. package/src/server.ts +340 -0
  150. package/src/typecheck.ts +723 -0
  151. package/src/worker-federation-context.ts +51 -0
  152. package/src/worker-federation-contexts.ts +91 -0
  153. package/src/worker-federation.ts +120 -0
  154. package/README.md +0 -3
@@ -0,0 +1,57 @@
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
+ disposeRouter: (router)=>router.history.destroy(),
13
+ async prepareRouter (router, hydrating, signal) {
14
+ if (hydrating) await prepareOctaneRouterHydration(router, {
15
+ signal
16
+ });
17
+ else await router.load();
18
+ },
19
+ async start ({ root, identity, bootstrap, signal, load }) {
20
+ const application = {
21
+ container: root,
22
+ identity,
23
+ nativeHydrationBuildId,
24
+ federation: options.federation,
25
+ workerFederation: options.workerFederation,
26
+ documentId: bootstrap?.documentId,
27
+ signal,
28
+ load: async ()=>{
29
+ const view = await load();
30
+ if ('component' === view.kind) return {
31
+ default: componentView(view.component, view.i18n)
32
+ };
33
+ return view.i18n ? {
34
+ default: LocalizedRouterRoot,
35
+ props: {
36
+ router: view.router,
37
+ i18n: view.i18n
38
+ }
39
+ } : {
40
+ default: OctaneRouterRoot,
41
+ props: {
42
+ router: view.router
43
+ }
44
+ };
45
+ }
46
+ };
47
+ const handle = bootstrap?.hydrating ? await hydrateOctaneApplication({
48
+ ...application,
49
+ documentIdentity: bootstrap.identity,
50
+ documentId: bootstrap.documentId,
51
+ documentNativeHydrationBuildId: bootstrap.nativeHydrationBuildId
52
+ }) : await mountOctaneApplication(application);
53
+ return ()=>handle.dispose();
54
+ }
55
+ });
56
+ }
57
+ export { startNativeClient };
@@ -0,0 +1,91 @@
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, createSsrStreamResponse, getSsrStatus } 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, workerFederation }, component, document, i18n)=>renderOctaneApplication({
38
+ session,
39
+ App: componentView(component, i18n),
40
+ document,
41
+ federation: options.federation,
42
+ workerFederation,
43
+ workerFederationLocale: options.workerFederationLocale
44
+ }),
45
+ async renderRoutes ({ request, context: { session, workerFederation }, router, outcomes, document, forbiddenValues, i18n }) {
46
+ const response = await createOctaneRequestHandler({
47
+ request,
48
+ session,
49
+ createRouter: ()=>router,
50
+ serialization: {
51
+ forbiddenValues
52
+ }
53
+ })(async ({ router, responseHeaders })=>{
54
+ const metadata = mergeDataResponseMetadata(outcomes, {
55
+ status: getSsrStatus(router)
56
+ });
57
+ const policy = dataMetadataToDocumentPolicy({
58
+ ...metadata,
59
+ headers: mergeHeaderFields(collectDataHeaders(responseHeaders), metadata.headers)
60
+ });
61
+ session.resolveResponse(withOctaneDocumentFields(policy, responseHeaders));
62
+ const rendered = await renderOctaneApplication({
63
+ session,
64
+ ...i18n ? {
65
+ App: LocalizedRouterServer,
66
+ props: {
67
+ router,
68
+ i18n
69
+ }
70
+ } : {
71
+ App: OctaneRouterServer,
72
+ props: {
73
+ router
74
+ }
75
+ },
76
+ document,
77
+ federation: options.federation,
78
+ workerFederation,
79
+ workerFederationLocale: options.workerFederationLocale,
80
+ router
81
+ });
82
+ return createSsrStreamResponse(router, rendered);
83
+ });
84
+ return session.committedPolicy ? response : mergeDataResponseIntoResponse(response, mergeDataResponseMetadata(outcomes, {
85
+ status: response.status
86
+ }));
87
+ }
88
+ });
89
+ }
90
+ export { createNativeWorkerFederationRuntime } from "./worker-federation.mjs";
91
+ 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,167 @@
1
+ import { loadFederatedModule } from "@modern-js/renderer-core/federation";
2
+ import { Suspense, createElement, decodeExternalSnapshot, externalSnapshotBoundary, hookSlots, lazy, preinit, publisherBoundary, serializeExternalSnapshotRequest, useContext, useEffect } from "octane";
3
+ import { FederationContext } from "./federation-context.mjs";
4
+ import { I18nContext } from "./i18n/context.mjs";
5
+ import { WorkerFederationContext } from "./worker-federation-context.mjs";
6
+ import { useOctaneWorkerContextKeys } from "./worker-federation-contexts.mjs";
7
+ function withTimeout(promise, timeout, label) {
8
+ if (void 0 === timeout) return promise;
9
+ return new Promise((resolve, reject)=>{
10
+ const timer = setTimeout(()=>reject(new Error(`Remote module ${label} did not load within ${timeout}ms`)), timeout);
11
+ timer.unref?.();
12
+ promise.then((value)=>{
13
+ clearTimeout(timer);
14
+ resolve(value);
15
+ }, (error)=>{
16
+ clearTimeout(timer);
17
+ reject(error);
18
+ });
19
+ });
20
+ }
21
+ const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
22
+ function federatedComponent(remote, options = {}) {
23
+ if ('string' == typeof remote ? !/^[^/]+\/.+/u.test(remote) : 'function' != typeof remote) throw new TypeError("federatedComponent requires a remote id such as 'remote/Widget' or a loader");
24
+ if (void 0 !== options.timeout && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new TypeError('federatedComponent timeout must be a positive number of milliseconds');
25
+ const id = 'string' == typeof remote ? remote : void 0;
26
+ const standalone = {
27
+ instance: void 0,
28
+ server: "u" < typeof document
29
+ };
30
+ const components = new WeakMap();
31
+ const workerOwners = new WeakMap();
32
+ return (props)=>{
33
+ const context = useContext(FederationContext) ?? standalone;
34
+ const worker = useContext(WorkerFederationContext);
35
+ const language = worker ? useContext(I18nContext)?.language ?? worker.locale : void 0;
36
+ const contextKeys = worker ? useOctaneWorkerContextKeys(options.contextKeys) : void 0;
37
+ let owner = context;
38
+ if (worker) {
39
+ let scopes = workerOwners.get(context);
40
+ if (!scopes) workerOwners.set(context, scopes = new WeakMap());
41
+ let variants = scopes.get(worker);
42
+ if (!variants) scopes.set(worker, variants = new Map());
43
+ const key = JSON.stringify([
44
+ context.server ? language : void 0,
45
+ contextKeys
46
+ ]);
47
+ let variant = variants.get(key);
48
+ if (!variant) variants.set(key, variant = {});
49
+ owner = variant;
50
+ }
51
+ let Remote = components.get(owner);
52
+ if (!Remote) {
53
+ Remote = lazy(async ()=>{
54
+ try {
55
+ if (context.server && !id) throw new Error('A federatedComponent loader renders in the browser only; use a remote id to server-render it');
56
+ if (worker) {
57
+ if (!id || !language) throw new Error('Worker federation requires a remote id and the owning entry locale');
58
+ const separator = id.indexOf('/');
59
+ const remoteName = id.slice(0, separator);
60
+ const expose = `./${id.slice(separator + 1)}`;
61
+ const controller = new AbortController();
62
+ const signal = worker.signal ? AbortSignal.any([
63
+ worker.signal,
64
+ controller.signal
65
+ ]) : controller.signal;
66
+ const timeout = options.timeout ?? (context.server ? 3000 : void 0);
67
+ let admitted;
68
+ try {
69
+ admitted = await withTimeout(worker.transport.admit({
70
+ remote: remoteName,
71
+ expose,
72
+ locale: language,
73
+ signal
74
+ }), timeout, id);
75
+ } finally{
76
+ controller.abort();
77
+ }
78
+ const BrowserRemote = lazy(async ()=>{
79
+ const { module, assets } = await loadFederatedModule(context.instance, id);
80
+ if ('function' != typeof module?.default) throw new TypeError('A federated Octane module must default-export a native component');
81
+ const component = module.default;
82
+ const Loaded = (props, scope, extra)=>{
83
+ for (const href of assets?.css ?? [])preinit(href, {
84
+ as: 'style',
85
+ precedence: 'federated',
86
+ ...void 0 === context.nonce ? {} : {
87
+ nonce: context.nonce
88
+ }
89
+ });
90
+ return component(props, scope, extra);
91
+ };
92
+ return {
93
+ default: Loaded
94
+ };
95
+ });
96
+ const boundary = externalSnapshotBoundary({
97
+ authority: admitted.coordinate.native,
98
+ component: BrowserRemote,
99
+ contextKeys,
100
+ timeoutMs: timeout,
101
+ async snapshot (request, signal) {
102
+ const response = await worker.transport.request({
103
+ remote: remoteName,
104
+ expose,
105
+ locale: language,
106
+ payload: serializeExternalSnapshotRequest(request),
107
+ signal
108
+ });
109
+ return decodeExternalSnapshot(response.payload);
110
+ }
111
+ });
112
+ const Loaded = (props)=>{
113
+ for (const href of admitted.styles)preinit(href, {
114
+ as: 'style',
115
+ precedence: 'federated',
116
+ ...void 0 === context.nonce ? {} : {
117
+ nonce: context.nonce
118
+ }
119
+ });
120
+ return createElement(boundary, props);
121
+ };
122
+ return {
123
+ default: Loaded
124
+ };
125
+ }
126
+ const { module, assets } = await withTimeout(Promise.resolve().then(()=>id ? loadFederatedModule(context.instance, id) : remote().then((module)=>({
127
+ module,
128
+ assets: void 0
129
+ }))), options.timeout ?? (context.server ? 3000 : void 0), id ?? 'loader');
130
+ if ('function' != typeof module?.default) throw new TypeError('A federated Octane module must default-export a native component');
131
+ if (context.server && !assets) throw new Error(`Cannot server-render ${id}: the host has no browser assets for this remote`);
132
+ const component = publisherBoundary(module.default, {
133
+ publisherKey: id ?? 'ultramodern:browser-loader'
134
+ });
135
+ const Loaded = (props)=>{
136
+ useEffect(()=>{
137
+ if (context.server || !id || !context.instance) return;
138
+ const register = Reflect.get(context.instance, Symbol.for('ultramodern.federation.browser-dev-boundary.v1'));
139
+ if ('function' != typeof register) return;
140
+ return register(id, module.default, component);
141
+ }, [], publisherRegistrationEffectSlot);
142
+ for (const href of assets?.css ?? [])preinit(href, {
143
+ as: 'style',
144
+ precedence: 'federated',
145
+ ...void 0 === context.nonce ? {} : {
146
+ nonce: context.nonce
147
+ }
148
+ });
149
+ return createElement(component, props);
150
+ };
151
+ return {
152
+ default: Loaded
153
+ };
154
+ } catch (error) {
155
+ if (!context.server) components.delete(owner);
156
+ throw error;
157
+ }
158
+ });
159
+ components.set(owner, Remote);
160
+ }
161
+ return createElement(Suspense, {
162
+ fallback: options.fallback,
163
+ children: createElement(Remote, props)
164
+ });
165
+ };
166
+ }
167
+ export { federatedComponent };
@@ -0,0 +1,21 @@
1
+ import { createElement, hookSlots, useSyncExternalStore } from "octane";
2
+ import { I18nContext } from "./context.mjs";
3
+ const languageStoreSlot = Symbol(hookSlots(1));
4
+ function I18nProvider(props) {
5
+ const instance = props.instance;
6
+ const language = useSyncExternalStore((notify)=>{
7
+ instance.on?.('languageChanged', notify);
8
+ return ()=>instance.off?.('languageChanged', notify);
9
+ }, ()=>instance.language, ()=>instance.language, languageStoreSlot);
10
+ const value = {
11
+ instance: props.instance,
12
+ languages: props.languages,
13
+ localisedUrls: props.localisedUrls,
14
+ language
15
+ };
16
+ return createElement(I18nContext, {
17
+ value,
18
+ children: props.children
19
+ });
20
+ }
21
+ export { I18nProvider };
@@ -0,0 +1,42 @@
1
+ import { 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 targetLocationSlot = 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 location = useLocation(void 0, targetLocationSlot);
11
+ const { language: requestedLanguage, to, children, ...options } = props;
12
+ const language = void 0 === requestedLanguage ? context.language : configuredLanguage(requestedLanguage, context.languages);
13
+ const externalTarget = to.startsWith('//') || URL.canParse(to);
14
+ const pathname = externalTarget ? to : localizePath(router.buildLocation({
15
+ to,
16
+ from: options.from,
17
+ params: options.params,
18
+ unsafeRelative: options.unsafeRelative,
19
+ _fromLocation: location
20
+ }).pathname, language, {
21
+ languages: [
22
+ ...context.languages
23
+ ],
24
+ localisedUrls: context.localisedUrls
25
+ });
26
+ return createElement(Link, {
27
+ ...options,
28
+ to: router.options.rewrite ? to : pathname,
29
+ ...router.options.rewrite && !externalTarget ? {
30
+ mask: {
31
+ to: pathname,
32
+ inheritSearch: true,
33
+ hash: options.hash,
34
+ state: options.state
35
+ }
36
+ } : {},
37
+ ...language === context.language ? {} : {
38
+ hreflang: language
39
+ }
40
+ }, children);
41
+ }
42
+ export { LocalizedLink };
@@ -0,0 +1,3 @@
1
+ import { createContext } from "octane";
2
+ const I18nContext = createContext(null);
3
+ export { I18nContext };
@@ -0,0 +1,8 @@
1
+ export { createRequestLanguageRedirect, detectRequestLanguage, languageFromPathname, resolveRequestLanguage } from "@modern-js/i18n-runtime-extensions/request-language";
2
+ export { I18N_SSR_HANDOFF_ELEMENT_ID, createI18nSsrHandoffInlineData, readI18nSsrHandoff } from "@modern-js/i18n-runtime-extensions/ssrLanguageHandoff";
3
+ export { createI18nUrlRewrite } from "@modern-js/i18n-runtime-extensions/urlRewrite";
4
+ export { I18nContext } from "./context.mjs";
5
+ export { I18nProvider } from "./I18nProvider.mjs";
6
+ export { LocalizedLink } from "./LocalizedLink.mjs";
7
+ export { useLatestLanguageSync } from "./languageSync.mjs";
8
+ export { useI18n } from "./useI18n.mjs";
@@ -0,0 +1,61 @@
1
+ import { createLatestLanguageSyncBinding } from "@modern-js/i18n-runtime-extensions/language-sync/controller";
2
+ import { hookSlots, useCallback, useEffect, useRef } from "octane";
3
+ const bindingSlot = Symbol(hookSlots(1));
4
+ const callbacksSlot = Symbol(hookSlots(1));
5
+ const activationSlot = Symbol(hookSlots(1));
6
+ const requestSlot = Symbol(hookSlots(1));
7
+ const retrySlot = Symbol(hookSlots(1));
8
+ const requestCallbackSlot = Symbol(hookSlots(1));
9
+ const useLatestLanguageSync = ({ changeLanguage, commitLanguage, desiredLanguage, enabled = true, policy, readLanguage, reportFailure, target })=>{
10
+ const bindingRef = useRef(null, bindingSlot);
11
+ if (null === bindingRef.current) bindingRef.current = createLatestLanguageSyncBinding(policy);
12
+ const binding = bindingRef.current;
13
+ useEffect(()=>{
14
+ binding.updateCallbacks({
15
+ changeLanguage,
16
+ commitLanguage,
17
+ readLanguage,
18
+ reportFailure
19
+ });
20
+ }, void 0, callbacksSlot);
21
+ useEffect(()=>{
22
+ if (!enabled || !target) return;
23
+ binding.activate(target);
24
+ return ()=>binding.deactivate();
25
+ }, [
26
+ binding,
27
+ enabled,
28
+ target
29
+ ], activationSlot);
30
+ useEffect(()=>{
31
+ if (!enabled || !target || !desiredLanguage) return void binding.clearRequest();
32
+ binding.request(desiredLanguage);
33
+ }, [
34
+ binding,
35
+ desiredLanguage,
36
+ enabled,
37
+ target
38
+ ], requestSlot);
39
+ useEffect(()=>{
40
+ if (!enabled || !target || !desiredLanguage || "u" < typeof window) return;
41
+ const retryCurrentIntent = ()=>binding.request(desiredLanguage);
42
+ const retryVisibleIntent = ()=>{
43
+ if ('visible' === document.visibilityState) retryCurrentIntent();
44
+ };
45
+ window.addEventListener('online', retryCurrentIntent);
46
+ document.addEventListener('visibilitychange', retryVisibleIntent);
47
+ return ()=>{
48
+ window.removeEventListener('online', retryCurrentIntent);
49
+ document.removeEventListener('visibilitychange', retryVisibleIntent);
50
+ };
51
+ }, [
52
+ binding,
53
+ desiredLanguage,
54
+ enabled,
55
+ target
56
+ ], retrySlot);
57
+ return useCallback((language)=>binding.request(language), [
58
+ binding
59
+ ], requestCallbackSlot);
60
+ };
61
+ export { useLatestLanguageSync };
File without changes
@@ -0,0 +1,72 @@
1
+ import { configuredLanguage, localizePath } from "@modern-js/i18n-runtime-extensions/paths";
2
+ import { languageFromPathname } from "@modern-js/i18n-runtime-extensions/request-language";
3
+ import { useContext } from "octane";
4
+ import { useRouter } from "../router.mjs";
5
+ import { I18nContext } from "./context.mjs";
6
+ const languageSwitches = new WeakMap();
7
+ function useI18n() {
8
+ const context = useContext(I18nContext);
9
+ if (!context) throw new Error('[renderer-octane] useI18n() was called outside of an I18nProvider.');
10
+ const { instance, languages, localisedUrls, language } = context;
11
+ const t = (key, options)=>instance.t(key, options);
12
+ const router = useRouter({
13
+ warn: false
14
+ });
15
+ const changeLanguage = (requested)=>{
16
+ let nextLanguage;
17
+ try {
18
+ nextLanguage = configuredLanguage(requested, languages);
19
+ } catch (error) {
20
+ return Promise.reject(error);
21
+ }
22
+ const state = languageSwitches.get(instance) ?? {
23
+ latest: 0,
24
+ queue: Promise.resolve()
25
+ };
26
+ languageSwitches.set(instance, state);
27
+ const generation = ++state.latest;
28
+ const run = state.queue.then(async ()=>{
29
+ if (state.latest !== generation) return;
30
+ const committed = instance.language;
31
+ const publicLanguage = ()=>router ? languageFromPathname(new URL(router.state.location.publicHref, 'http://localhost').pathname, languages, router.options.basepath) : void 0;
32
+ const reconcile = async ()=>{
33
+ const language = publicLanguage() ?? committed;
34
+ if (instance.language !== language) await instance.changeLanguage?.(language);
35
+ };
36
+ try {
37
+ await instance.changeLanguage?.(nextLanguage);
38
+ } catch (error) {
39
+ await reconcile();
40
+ throw error;
41
+ }
42
+ if (!router) return;
43
+ const href = localizePath(router.state.location.href, nextLanguage, {
44
+ languages: [
45
+ ...languages
46
+ ],
47
+ localisedUrls
48
+ });
49
+ try {
50
+ await router.navigate({
51
+ to: '.',
52
+ href,
53
+ replace: true
54
+ });
55
+ } catch (error) {
56
+ await reconcile();
57
+ throw error;
58
+ }
59
+ if (publicLanguage() !== nextLanguage) await reconcile();
60
+ });
61
+ state.queue = run.catch(()=>{});
62
+ return run;
63
+ };
64
+ return {
65
+ t,
66
+ language,
67
+ languages,
68
+ instance,
69
+ changeLanguage
70
+ };
71
+ }
72
+ export { useI18n };