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

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 (138) 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 +371 -0
  5. package/dist/cjs/entry-application.js +83 -0
  6. package/dist/cjs/entry-client.js +92 -0
  7. package/dist/cjs/entry-server.js +127 -0
  8. package/dist/cjs/federation-context.js +72 -0
  9. package/dist/cjs/federation.js +103 -0
  10. package/dist/cjs/i18n/I18nProvider.js +67 -0
  11. package/dist/cjs/i18n/LocalizedLink.js +89 -0
  12. package/dist/cjs/i18n/context.js +41 -0
  13. package/dist/cjs/i18n/index.js +86 -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/index.js +46 -0
  18. package/dist/cjs/manifest.js +167 -0
  19. package/dist/cjs/plugin/compiler.js +125 -0
  20. package/dist/cjs/plugin/create.js +265 -0
  21. package/dist/cjs/plugin/index.js +151 -0
  22. package/dist/cjs/plugin/profile.js +89 -0
  23. package/dist/cjs/plugin/source.js +56 -0
  24. package/dist/cjs/router-client.js +139 -0
  25. package/dist/cjs/router-handler.js +168 -0
  26. package/dist/cjs/router-injection.js +139 -0
  27. package/dist/cjs/router-server-cleanup.js +46 -0
  28. package/dist/cjs/router-server-snapshot.js +405 -0
  29. package/dist/cjs/router-server.js +108 -0
  30. package/dist/cjs/router-snapshot.js +270 -0
  31. package/dist/cjs/router.js +218 -0
  32. package/dist/cjs/routes.js +187 -0
  33. package/dist/cjs/server.js +224 -0
  34. package/dist/cjs/typecheck.js +615 -0
  35. package/dist/esm/bootstrap.mjs +24 -0
  36. package/dist/esm/client.mjs +327 -0
  37. package/dist/esm/entry-application.mjs +39 -0
  38. package/dist/esm/entry-client.mjs +54 -0
  39. package/dist/esm/entry-server.mjs +86 -0
  40. package/dist/esm/federation-context.mjs +25 -0
  41. package/dist/esm/federation.mjs +65 -0
  42. package/dist/esm/i18n/I18nProvider.mjs +29 -0
  43. package/dist/esm/i18n/LocalizedLink.mjs +48 -0
  44. package/dist/esm/i18n/context.mjs +3 -0
  45. package/dist/esm/i18n/index.mjs +8 -0
  46. package/dist/esm/i18n/languageSync.mjs +61 -0
  47. package/dist/esm/i18n/types.mjs +0 -0
  48. package/dist/esm/i18n/useI18n.mjs +72 -0
  49. package/dist/esm/index.mjs +8 -0
  50. package/dist/esm/manifest.mjs +117 -0
  51. package/dist/esm/router-client.mjs +98 -0
  52. package/dist/esm/router-handler.mjs +130 -0
  53. package/dist/esm/router-injection.mjs +101 -0
  54. package/dist/esm/router-server-cleanup.mjs +8 -0
  55. package/dist/esm/router-server-snapshot.mjs +367 -0
  56. package/dist/esm/router-server.mjs +12 -0
  57. package/dist/esm/router-snapshot.mjs +199 -0
  58. package/dist/esm/router.mjs +94 -0
  59. package/dist/esm/routes.mjs +134 -0
  60. package/dist/esm/server.mjs +183 -0
  61. package/dist/esm-node/bootstrap.mjs +24 -0
  62. package/dist/esm-node/manifest.mjs +117 -0
  63. package/dist/esm-node/plugin/compiler.mjs +70 -0
  64. package/dist/esm-node/plugin/create.mjs +227 -0
  65. package/dist/esm-node/plugin/index.mjs +103 -0
  66. package/dist/esm-node/plugin/profile.mjs +51 -0
  67. package/dist/esm-node/plugin/source.mjs +7 -0
  68. package/dist/esm-node/typecheck.mjs +560 -0
  69. package/dist/types/bootstrap.d.ts +10 -0
  70. package/dist/types/client.d.ts +36 -0
  71. package/dist/types/entry-application.d.ts +19 -0
  72. package/dist/types/entry-client.d.ts +8 -0
  73. package/dist/types/entry-server.d.ts +11 -0
  74. package/dist/types/federation-context.d.ts +28 -0
  75. package/dist/types/federation.d.ts +18 -0
  76. package/dist/types/i18n/I18nProvider.d.ts +8 -0
  77. package/dist/types/i18n/LocalizedLink.d.ts +41 -0
  78. package/dist/types/i18n/context.d.ts +9 -0
  79. package/dist/types/i18n/index.d.ts +9 -0
  80. package/dist/types/i18n/languageSync.d.ts +26 -0
  81. package/dist/types/i18n/types.d.ts +34 -0
  82. package/dist/types/i18n/useI18n.d.ts +21 -0
  83. package/dist/types/index.d.ts +9 -0
  84. package/dist/types/manifest.d.ts +34 -0
  85. package/dist/types/plugin/compiler-manifest.d.cts +10 -0
  86. package/dist/types/plugin/compiler.d.ts +5 -0
  87. package/dist/types/plugin/create.d.ts +23 -0
  88. package/dist/types/plugin/index.d.ts +3 -0
  89. package/dist/types/plugin/profile.d.ts +2 -0
  90. package/dist/types/plugin/source.d.ts +1 -0
  91. package/dist/types/plugin/svg-component-template.d.cts +6 -0
  92. package/dist/types/router-client.d.ts +12 -0
  93. package/dist/types/router-handler.d.ts +13 -0
  94. package/dist/types/router-injection.d.ts +8 -0
  95. package/dist/types/router-server-cleanup.d.ts +3 -0
  96. package/dist/types/router-server-snapshot.d.ts +15 -0
  97. package/dist/types/router-server.d.ts +13 -0
  98. package/dist/types/router-snapshot.d.ts +14 -0
  99. package/dist/types/router.d.ts +30 -0
  100. package/dist/types/routes.d.ts +18 -0
  101. package/dist/types/server.d.ts +41 -0
  102. package/dist/types/typecheck.d.ts +31 -0
  103. package/package.json +216 -4
  104. package/src/bootstrap.ts +60 -0
  105. package/src/client.ts +485 -0
  106. package/src/entry-application.ts +72 -0
  107. package/src/entry-client.ts +80 -0
  108. package/src/entry-server.ts +142 -0
  109. package/src/federation-context.ts +53 -0
  110. package/src/federation.ts +158 -0
  111. package/src/i18n/I18nProvider.ts +56 -0
  112. package/src/i18n/LocalizedLink.ts +112 -0
  113. package/src/i18n/context.ts +12 -0
  114. package/src/i18n/index.ts +35 -0
  115. package/src/i18n/languageSync.ts +132 -0
  116. package/src/i18n/types.ts +36 -0
  117. package/src/i18n/useI18n.ts +119 -0
  118. package/src/index.ts +10 -0
  119. package/src/manifest.ts +253 -0
  120. package/src/plugin/compiler-manifest.cjs +178 -0
  121. package/src/plugin/compiler.ts +112 -0
  122. package/src/plugin/create.ts +272 -0
  123. package/src/plugin/index.ts +105 -0
  124. package/src/plugin/profile.ts +46 -0
  125. package/src/plugin/source.ts +16 -0
  126. package/src/plugin/svg-component-template.cjs +17 -0
  127. package/src/router-client.ts +144 -0
  128. package/src/router-handler.ts +182 -0
  129. package/src/router-injection.ts +145 -0
  130. package/src/router-server-cleanup.ts +11 -0
  131. package/src/router-server-snapshot.ts +564 -0
  132. package/src/router-server.ts +23 -0
  133. package/src/router-snapshot.ts +251 -0
  134. package/src/router.ts +192 -0
  135. package/src/routes.ts +263 -0
  136. package/src/server.ts +300 -0
  137. package/src/typecheck.ts +723 -0
  138. package/README.md +0 -3
@@ -0,0 +1,142 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
2
+ import {
3
+ collectDataHeaders,
4
+ dataMetadataToDocumentPolicy,
5
+ mergeDataResponseIntoResponse,
6
+ mergeDataResponseMetadata,
7
+ mergeHeaderFields,
8
+ } from '@modern-js/renderer-core/data';
9
+ import {
10
+ createNativeServerEntry as createServerEntry,
11
+ type NativeI18nInstance,
12
+ type NativeServerEntry,
13
+ type NativeServerEntryOptions,
14
+ } from '@modern-js/renderer-core/entry-server';
15
+ import { createNativeRouter } from '@modern-js/renderer-core/router';
16
+ import type { ResponsePolicy } from '@modern-js/renderer-core/session';
17
+ import type { ServerRenderNode } from 'octane/server';
18
+ import {
19
+ componentView,
20
+ localizedRouterRoot,
21
+ octaneRouterFactory,
22
+ } from './entry-application';
23
+ import { validateOctaneModuleManifest } from './manifest';
24
+ import {
25
+ createOctaneRequestHandler,
26
+ createOctaneRouterInjection,
27
+ createSsrStreamResponse,
28
+ OctaneRouterServer,
29
+ } from './router-server';
30
+ import { renderOctaneApplication, renderOctaneCSRDocument } from './server';
31
+
32
+ /** Document-describing fields an Octane route's `headers()` may set. */
33
+ const OCTANE_DOCUMENT_FIELDS = new Set([
34
+ 'content-language',
35
+ 'content-location',
36
+ 'content-disposition',
37
+ ]);
38
+
39
+ /**
40
+ * The loader projection strips representation fields; the ones an Octane route
41
+ * declares for the document itself are kept, as Solid document headers are.
42
+ */
43
+ export function withOctaneDocumentFields(
44
+ policy: ResponsePolicy,
45
+ responseHeaders: Headers,
46
+ ): ResponsePolicy {
47
+ const fields = collectDataHeaders(responseHeaders).filter(([name]) =>
48
+ OCTANE_DOCUMENT_FIELDS.has(name.toLowerCase()),
49
+ );
50
+ return fields.length
51
+ ? { ...policy, headers: [...policy.headers, ...fields] }
52
+ : policy;
53
+ }
54
+
55
+ export type OctaneServerEntryOptions = NativeServerEntryOptions<
56
+ NativeI18nInstance,
57
+ LocalisedUrlsOption
58
+ >;
59
+
60
+ const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
61
+
62
+ /** The native transport handlers of a generated Octane server entry. */
63
+ export function createNativeServerEntry(
64
+ options: OctaneServerEntryOptions,
65
+ ): NativeServerEntry {
66
+ return createServerEntry(options, {
67
+ document: (document, nativeManifest, identity) => ({
68
+ ...document,
69
+ nativeHydrationBuildId: validateOctaneModuleManifest(
70
+ nativeManifest,
71
+ identity,
72
+ ).nativeHydrationBuildId,
73
+ }),
74
+ run: (_session, callback) => callback(),
75
+ respond: (_session, response) => response,
76
+ createRouter: (application, routerOptions) =>
77
+ createNativeRouter(application, routerOptions, octaneRouterFactory),
78
+ renderCSR: ({ session }, document) =>
79
+ renderOctaneCSRDocument({ session, document }),
80
+ renderComponent: ({ session }, component, document, i18n) =>
81
+ renderOctaneApplication({
82
+ session,
83
+ App: componentView(component, i18n) as ServerRenderNode,
84
+ document,
85
+ federation: options.federation,
86
+ }),
87
+ async renderRoutes({
88
+ request,
89
+ context: { session },
90
+ router,
91
+ outcomes,
92
+ document,
93
+ forbiddenValues,
94
+ i18n,
95
+ }) {
96
+ const response = await createOctaneRequestHandler({
97
+ request,
98
+ session,
99
+ createRouter: () => router,
100
+ serialization: { forbiddenValues },
101
+ })(async ({ router, responseHeaders }) => {
102
+ const metadata = mergeDataResponseMetadata(outcomes, {
103
+ status: router.state.statusCode ?? 200,
104
+ });
105
+ // Route headers() take precedence over loader singleton fields; list
106
+ // fields keep both.
107
+ const policy = dataMetadataToDocumentPolicy({
108
+ ...metadata,
109
+ headers: mergeHeaderFields(
110
+ collectDataHeaders(responseHeaders),
111
+ metadata.headers,
112
+ ),
113
+ });
114
+ session.resolveResponse(
115
+ withOctaneDocumentFields(policy, responseHeaders),
116
+ );
117
+ const rendered = await renderOctaneApplication({
118
+ session,
119
+ ...(i18n
120
+ ? {
121
+ App: LocalizedRouterServer as ServerRenderNode,
122
+ props: { router, i18n },
123
+ }
124
+ : {
125
+ App: OctaneRouterServer as ServerRenderNode,
126
+ props: { router },
127
+ }),
128
+ document,
129
+ federation: options.federation,
130
+ injection: createOctaneRouterInjection(router, session),
131
+ });
132
+ return createSsrStreamResponse(router, rendered);
133
+ });
134
+ return session.committedPolicy
135
+ ? response
136
+ : mergeDataResponseIntoResponse(
137
+ response,
138
+ mergeDataResponseMetadata(outcomes, { status: response.status }),
139
+ );
140
+ },
141
+ });
142
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ type FederationInstance,
3
+ getFederationHost,
4
+ type NativeFederationBinding,
5
+ } from '@modern-js/renderer-core/federation';
6
+ import { type ComponentBody, createContext, createElement } from 'octane';
7
+
8
+ /** One application owns its host runtime; each response owns its render scope. */
9
+ export interface OctaneFederationScope {
10
+ readonly instance: FederationInstance | undefined;
11
+ readonly server: boolean;
12
+ readonly nonce?: string;
13
+ }
14
+
15
+ export const FederationContext = createContext<OctaneFederationScope | null>(
16
+ null,
17
+ );
18
+
19
+ export function createFederationScope(
20
+ binding: NativeFederationBinding | undefined,
21
+ server: boolean,
22
+ nonce?: string,
23
+ ): OctaneFederationScope {
24
+ return {
25
+ instance: getFederationHost(binding),
26
+ server,
27
+ ...(nonce === undefined ? {} : { nonce }),
28
+ };
29
+ }
30
+
31
+ export function FederationRoot(props: {
32
+ readonly scope: OctaneFederationScope;
33
+ readonly children?: unknown;
34
+ }) {
35
+ return createElement(FederationContext, {
36
+ value: props.scope,
37
+ children: props.children,
38
+ });
39
+ }
40
+
41
+ /** A stable root component retains native state when its application updates. */
42
+ export function FederationApplication(props: {
43
+ readonly scope: OctaneFederationScope;
44
+ readonly application: {
45
+ readonly default: ComponentBody;
46
+ readonly props?: unknown;
47
+ };
48
+ }) {
49
+ return createElement(FederationRoot, {
50
+ scope: props.scope,
51
+ children: createElement(props.application.default, props.application.props),
52
+ });
53
+ }
@@ -0,0 +1,158 @@
1
+ import {
2
+ loadFederatedModule,
3
+ remoteBrowserAssets,
4
+ } from '@modern-js/renderer-core/federation';
5
+ import {
6
+ type ComponentBody,
7
+ createElement,
8
+ lazy,
9
+ type OctaneNode,
10
+ preinit,
11
+ Suspense,
12
+ useContext,
13
+ } from 'octane';
14
+ import {
15
+ FederationContext,
16
+ type OctaneFederationScope,
17
+ } from './federation-context';
18
+
19
+ /** The native component shape exposed by a same-renderer Octane remote. */
20
+ export interface FederatedModule<P extends object> {
21
+ readonly default: ComponentBody<P>;
22
+ }
23
+
24
+ export interface FederatedComponentOptions {
25
+ /** Native content shown while loading, or after a failed server load. */
26
+ readonly fallback?: () => OctaneNode;
27
+ /** A positive load limit; the server defaults to 3000ms. */
28
+ readonly timeout?: number;
29
+ }
30
+
31
+ function withTimeout<T>(
32
+ promise: Promise<T>,
33
+ timeout: number | undefined,
34
+ label: string,
35
+ ): Promise<T> {
36
+ if (timeout === undefined) return promise;
37
+ return new Promise<T>((resolve, reject) => {
38
+ const timer = setTimeout(
39
+ () =>
40
+ reject(
41
+ new Error(`Remote module ${label} did not load within ${timeout}ms`),
42
+ ),
43
+ timeout,
44
+ );
45
+ (timer as { unref?: () => void }).unref?.();
46
+ promise.then(
47
+ value => {
48
+ clearTimeout(timer);
49
+ resolve(value);
50
+ },
51
+ error => {
52
+ clearTimeout(timer);
53
+ reject(error);
54
+ },
55
+ );
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Render a native Octane remote by its `remote/Expose` id or a browser loader.
61
+ * Native Suspense owns server fallbacks, streamed styles and hydration adoption;
62
+ * browser failures reach the nearest native ErrorBoundary. A loader function is
63
+ * browser-only; use an id to render through the host's server federation runtime.
64
+ */
65
+ export function federatedComponent<P extends object = Record<string, never>>(
66
+ remote: string | (() => Promise<FederatedModule<P>>),
67
+ options: FederatedComponentOptions = {},
68
+ ): ComponentBody<P> {
69
+ if (
70
+ typeof remote === 'string'
71
+ ? !/^[^/]+\/.+/u.test(remote)
72
+ : typeof remote !== 'function'
73
+ ) {
74
+ throw new TypeError(
75
+ "federatedComponent requires a remote id such as 'remote/Widget' or a loader",
76
+ );
77
+ }
78
+ if (
79
+ options.timeout !== undefined &&
80
+ (!Number.isFinite(options.timeout) || options.timeout <= 0)
81
+ ) {
82
+ throw new TypeError(
83
+ 'federatedComponent timeout must be a positive number of milliseconds',
84
+ );
85
+ }
86
+ const id = typeof remote === 'string' ? remote : undefined;
87
+ const standalone: OctaneFederationScope = {
88
+ instance: undefined,
89
+ server: typeof document === 'undefined',
90
+ };
91
+ // Native lazy caches its module. Scope the cache to one application/response,
92
+ // so two hosts using the same component definition cannot share a remote.
93
+ const components = new WeakMap<OctaneFederationScope, ComponentBody<P>>();
94
+
95
+ return props => {
96
+ const context = useContext(FederationContext) ?? standalone;
97
+ let Remote = components.get(context);
98
+ if (!Remote) {
99
+ Remote = lazy(async () => {
100
+ try {
101
+ if (context.server && !id) {
102
+ throw new Error(
103
+ 'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
104
+ );
105
+ }
106
+ const module = await withTimeout(
107
+ Promise.resolve().then(() =>
108
+ id
109
+ ? loadFederatedModule<FederatedModule<P>>(context.instance, id)
110
+ : (remote as () => Promise<FederatedModule<P>>)(),
111
+ ),
112
+ options.timeout ?? (context.server ? 3000 : undefined),
113
+ id ?? 'loader',
114
+ );
115
+ if (typeof module?.default !== 'function') {
116
+ throw new TypeError(
117
+ 'A federated Octane module must default-export a native component',
118
+ );
119
+ }
120
+ const assets =
121
+ id && context.instance
122
+ ? remoteBrowserAssets(context.instance, id)
123
+ : undefined;
124
+ if (context.server && !assets) {
125
+ throw new Error(
126
+ `Cannot server-render ${id}: the host has no browser assets for this remote`,
127
+ );
128
+ }
129
+ const component = module.default;
130
+ const Loaded: ComponentBody<P> = props => {
131
+ // Resource APIs require an active native render. Late styles travel
132
+ // in Octane's resource carrier ahead of the remote's HTML segment.
133
+ for (const href of assets?.css ?? []) {
134
+ preinit(href, {
135
+ as: 'style',
136
+ precedence: 'federated',
137
+ ...(context.nonce === undefined
138
+ ? {}
139
+ : { nonce: context.nonce }),
140
+ });
141
+ }
142
+ return createElement(component, props);
143
+ };
144
+ return { default: Loaded };
145
+ } catch (error) {
146
+ // A later mount or ErrorBoundary reset may retry a failed load.
147
+ if (!context.server) components.delete(context);
148
+ throw error;
149
+ }
150
+ });
151
+ components.set(context, Remote);
152
+ }
153
+ return createElement(Suspense, {
154
+ fallback: options.fallback,
155
+ children: createElement(Remote, props),
156
+ });
157
+ };
158
+ }
@@ -0,0 +1,56 @@
1
+ import { createElement, hookSlots, useEffect, useState } from 'octane';
2
+ import { I18nContext } from './context';
3
+ import type { I18nContextValue, I18nInstanceLike } from './types';
4
+
5
+ export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
6
+ children?: unknown;
7
+ }
8
+
9
+ /**
10
+ * Provides a per-request i18next-shaped instance to the Octane tree. The
11
+ * instance must come from the caller (created with `cloneInstance` per
12
+ * request on the server, and the same instance reused on hydration) — this
13
+ * provider holds no module-level state of its own beyond the one reactive
14
+ * `language` state it derives from `instance` and shares with every
15
+ * consumer through context, matching `@modern-js/plugin-i18n`'s React
16
+ * `ModernI18nProvider` shape (one subscription, pushed through context,
17
+ * rather than every consumer subscribing to `languageChanged` itself).
18
+ *
19
+ * No JSX: built with `createElement`, matching this package's existing
20
+ * style (see `src/routes.ts`), and avoiding any dependency on Octane's
21
+ * JSX/compiler toolchain for a file that needs none of its features.
22
+ */
23
+ // Plain TypeScript is not rewritten by the Octane compiler, so each hook call
24
+ // site carries its own stable slot, like `OctaneRouterRoot`.
25
+ const languageStateSlot = Symbol(hookSlots(1));
26
+ const languageSubscriptionSlot = Symbol(hookSlots(1));
27
+
28
+ export function I18nProvider(props: I18nProviderProps) {
29
+ const [language, setLanguage] = useState(
30
+ () => props.instance.language,
31
+ languageStateSlot,
32
+ );
33
+
34
+ useEffect(
35
+ () => {
36
+ const instance: I18nInstanceLike = props.instance;
37
+ setLanguage(instance.language);
38
+ const handleLanguageChanged = (lng: string) => setLanguage(lng);
39
+ instance.on?.('languageChanged', handleLanguageChanged);
40
+ return () => {
41
+ instance.off?.('languageChanged', handleLanguageChanged);
42
+ };
43
+ },
44
+ [props.instance],
45
+ languageSubscriptionSlot,
46
+ );
47
+
48
+ const value: I18nContextValue = {
49
+ instance: props.instance,
50
+ languages: props.languages,
51
+ localisedUrls: props.localisedUrls,
52
+ language,
53
+ };
54
+
55
+ return createElement(I18nContext, { value, children: props.children });
56
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ canonicalPath,
3
+ configuredLanguage,
4
+ localizePath,
5
+ } from '@modern-js/i18n-runtime-extensions/paths';
6
+ import { createElement, hookSlots, type OctaneNode, useContext } from 'octane';
7
+ import {
8
+ Link,
9
+ type LinkComponentProps,
10
+ useLocation,
11
+ useRouter,
12
+ } from '../router';
13
+ import { I18nContext } from './context';
14
+
15
+ // Plain TypeScript is not rewritten by the Octane compiler: router hooks take
16
+ // their stable slot explicitly.
17
+ const locationSlot = Symbol(hookSlots(1));
18
+
19
+ export interface LocalizedLinkProps {
20
+ /** A canonical, language-agnostic target, e.g. `/products/red-shoe`. */
21
+ to: string;
22
+ /** Localize for a specific language instead of the current one. */
23
+ language?: string;
24
+ children?: unknown;
25
+ class?: string;
26
+ activeProps?: LinkComponentProps<'a'>['activeProps'];
27
+ inactiveProps?: LinkComponentProps<'a'>['inactiveProps'];
28
+ activeOptions?: LinkComponentProps<'a'>['activeOptions'];
29
+ replace?: boolean;
30
+ preload?: LinkComponentProps<'a'>['preload'];
31
+ }
32
+
33
+ /**
34
+ * A thin wrapper over the native Octane router `Link`
35
+ * (`@octanejs/tanstack-router`'s precompiled `Link.tsrx`) that localizes a
36
+ * canonical `to` for the current (or an explicit) language before handing it
37
+ * to the real link — the same contract as `@modern-js/plugin-i18n`'s React
38
+ * `Link`. It resolves the href itself via the shared pathname helpers, so it
39
+ * works with or without the router's i18n `rewrite`. Every link, including
40
+ * one to another language, is the native `Link`: navigation, preloading and
41
+ * active state stay with the router.
42
+ *
43
+ * Under the i18n `rewrite` the router matches canonical paths and localizes
44
+ * outgoing locations to the current language, so a link to another language
45
+ * routes to the canonical `to` and publishes the target language's URL as a
46
+ * route mask (the rewrite keeps a mask's explicit language). The entry's
47
+ * router language synchronization (`syncWithRouter`) switches the i18next
48
+ * instance from the navigated URL, as it does for history navigation.
49
+ *
50
+ * No JSX: built with `createElement`, matching this package's existing
51
+ * style (see `src/routes.ts`).
52
+ */
53
+ export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
54
+ const context = useContext(I18nContext);
55
+ if (!context) {
56
+ throw new Error(
57
+ '[renderer-octane] LocalizedLink was used outside of an I18nProvider.',
58
+ );
59
+ }
60
+ const router = useRouter();
61
+
62
+ // An explicit language must be configured: it becomes the locale prefix.
63
+ const language =
64
+ props.language === undefined
65
+ ? context.language
66
+ : configuredLanguage(props.language, context.languages);
67
+ const href = localizePath(props.to, language, {
68
+ languages: [...context.languages],
69
+ localisedUrls: context.localisedUrls,
70
+ });
71
+ const crossLanguage = language !== context.language;
72
+ const rewrite = Boolean(router.options.rewrite);
73
+
74
+ return createElement(
75
+ Link,
76
+ {
77
+ // Octane's `Link` builds its location from `to` alone. Under the i18n
78
+ // router rewrite the router matches canonical paths and localizes the
79
+ // public href itself; without it, the localized path is the route path.
80
+ to: rewrite ? props.to : href,
81
+ ...(rewrite && crossLanguage ? { mask: { to: href } } : {}),
82
+ ...(crossLanguage ? { hreflang: language } : {}),
83
+ class: props.class,
84
+ activeProps: props.activeProps,
85
+ inactiveProps: props.inactiveProps,
86
+ activeOptions: props.activeOptions,
87
+ replace: props.replace,
88
+ preload: props.preload,
89
+ },
90
+ props.children,
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Language-invariant active check: `to="/products"` is active whether the
96
+ * current location is `/en/products` or `/produkty` under a mapped locale.
97
+ */
98
+ export function useIsLocalizedActive(canonicalTarget: string): boolean {
99
+ const context = useContext(I18nContext);
100
+ if (!context) {
101
+ throw new Error(
102
+ '[renderer-octane] useIsLocalizedActive was used outside of an I18nProvider.',
103
+ );
104
+ }
105
+ const location = useLocation(undefined, locationSlot);
106
+ return (
107
+ canonicalPath(location.pathname, {
108
+ languages: [...context.languages],
109
+ localisedUrls: context.localisedUrls,
110
+ }) === canonicalTarget
111
+ );
112
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext } from 'octane';
2
+ import type { I18nContextValue } from './types';
3
+
4
+ export type { I18nContextValue, I18nInstanceLike } from './types';
5
+
6
+ /**
7
+ * Octane's `createContext<T>(defaultValue: T)` requires a default (unlike
8
+ * Solid 2's default-less form), so a missing provider is detected by
9
+ * checking for `null` in `useI18n`/`useIsLocalizedActive` instead — the same
10
+ * guard shape `@modern-js/plugin-i18n`'s React `ModernI18nContext` uses.
11
+ */
12
+ export const I18nContext = createContext<I18nContextValue | null>(null);
@@ -0,0 +1,35 @@
1
+ export {
2
+ createRequestLanguageRedirect,
3
+ detectRequestLanguage,
4
+ languageFromPathname,
5
+ type RequestLanguageDetection,
6
+ type ResolvedRequestLanguage,
7
+ type ResolveRequestLanguageOptions,
8
+ resolveRequestLanguage,
9
+ } from '@modern-js/i18n-runtime-extensions/request-language';
10
+ export {
11
+ createI18nSsrHandoffInlineData,
12
+ I18N_SSR_HANDOFF_ELEMENT_ID,
13
+ type I18nSsrHandoffPayload,
14
+ type I18nSsrHandoffResources,
15
+ readI18nSsrHandoff,
16
+ } from '@modern-js/i18n-runtime-extensions/ssrLanguageHandoff';
17
+ // Renderer-neutral pieces an application entry composes with these bindings.
18
+ export {
19
+ type CreateI18nUrlRewriteOptions,
20
+ createI18nUrlRewrite,
21
+ type I18nLocationRewrite,
22
+ } from '@modern-js/i18n-runtime-extensions/urlRewrite';
23
+ export { I18nContext } from './context';
24
+ export { I18nProvider, type I18nProviderProps } from './I18nProvider';
25
+ export {
26
+ LocalizedLink,
27
+ type LocalizedLinkProps,
28
+ useIsLocalizedActive,
29
+ } from './LocalizedLink';
30
+ export {
31
+ type OctaneLatestLanguageSyncOptions,
32
+ useLatestLanguageSync,
33
+ } from './languageSync';
34
+ export type { I18nContextValue, I18nInstanceLike } from './types';
35
+ export { type UseI18nReturn, useI18n } from './useI18n';