@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.
- 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,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLatestLanguageSyncBinding,
|
|
3
|
+
type LanguageSyncFailure,
|
|
4
|
+
type LanguageSyncPolicy,
|
|
5
|
+
} from '@modern-js/i18n-runtime-extensions/language-sync/controller';
|
|
6
|
+
import { hookSlots, useCallback, useEffect, useRef } from 'octane';
|
|
7
|
+
|
|
8
|
+
export interface OctaneLatestLanguageSyncOptions<TTarget extends object> {
|
|
9
|
+
changeLanguage: (
|
|
10
|
+
target: TTarget,
|
|
11
|
+
language: string,
|
|
12
|
+
) => PromiseLike<unknown> | unknown;
|
|
13
|
+
commitLanguage: (target: TTarget, language: string) => void;
|
|
14
|
+
desiredLanguage?: string;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
policy?: LanguageSyncPolicy;
|
|
17
|
+
readLanguage?: (target: TTarget) => string | undefined;
|
|
18
|
+
reportFailure?: (failure: LanguageSyncFailure) => void;
|
|
19
|
+
target?: TTarget;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Plain TypeScript is not rewritten by the Octane compiler, so each hook call
|
|
23
|
+
// site carries its own stable slot.
|
|
24
|
+
const bindingSlot = Symbol(hookSlots(1));
|
|
25
|
+
const callbacksSlot = Symbol(hookSlots(1));
|
|
26
|
+
const activationSlot = Symbol(hookSlots(1));
|
|
27
|
+
const requestSlot = Symbol(hookSlots(1));
|
|
28
|
+
const retrySlot = Symbol(hookSlots(1));
|
|
29
|
+
const requestCallbackSlot = Symbol(hookSlots(1));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Octane binding over the fork's renderer-neutral `Coordinator`/`Binding`
|
|
33
|
+
* pair (`@modern-js/i18n-runtime-extensions`'s `createLatestLanguageSyncBinding`):
|
|
34
|
+
* the same "latest intent wins, retry with backoff, report terminal failure"
|
|
35
|
+
* policy the React `useLatestLanguageSync` hook drives.
|
|
36
|
+
*
|
|
37
|
+
* Octane's own hook API (`useState`/`useEffect`/`useContext`/
|
|
38
|
+
* `useSyncExternalStore`, confirmed in `node_modules/octane/dist/runtime.d.ts`)
|
|
39
|
+
* is React-shaped, so this binding mirrors
|
|
40
|
+
* `i18n-extensions/src/language-sync/react.ts`'s `useLatestLanguageSync`
|
|
41
|
+
* directly — the Solid binding needed a `createEffect`/`onCleanup`
|
|
42
|
+
* reshaping for Solid 2's different (compute/effect-split, no
|
|
43
|
+
* `useEffect`-style dependency arrays) reactivity model; Octane does not.
|
|
44
|
+
*/
|
|
45
|
+
export const useLatestLanguageSync = <TTarget extends object>({
|
|
46
|
+
changeLanguage,
|
|
47
|
+
commitLanguage,
|
|
48
|
+
desiredLanguage,
|
|
49
|
+
enabled = true,
|
|
50
|
+
policy,
|
|
51
|
+
readLanguage,
|
|
52
|
+
reportFailure,
|
|
53
|
+
target,
|
|
54
|
+
}: OctaneLatestLanguageSyncOptions<TTarget>): ((language: string) => void) => {
|
|
55
|
+
const bindingRef = useRef<ReturnType<
|
|
56
|
+
typeof createLatestLanguageSyncBinding<TTarget>
|
|
57
|
+
> | null>(null, bindingSlot);
|
|
58
|
+
if (bindingRef.current === null) {
|
|
59
|
+
bindingRef.current = createLatestLanguageSyncBinding<TTarget>(policy);
|
|
60
|
+
}
|
|
61
|
+
const binding = bindingRef.current;
|
|
62
|
+
|
|
63
|
+
useEffect(
|
|
64
|
+
() => {
|
|
65
|
+
binding.updateCallbacks({
|
|
66
|
+
changeLanguage,
|
|
67
|
+
commitLanguage,
|
|
68
|
+
readLanguage,
|
|
69
|
+
reportFailure,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
undefined,
|
|
73
|
+
callbacksSlot,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(
|
|
77
|
+
() => {
|
|
78
|
+
if (!enabled || !target) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
binding.activate(target);
|
|
82
|
+
return () => binding.deactivate();
|
|
83
|
+
},
|
|
84
|
+
[binding, enabled, target],
|
|
85
|
+
activationSlot,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
useEffect(
|
|
89
|
+
() => {
|
|
90
|
+
if (!enabled || !target || !desiredLanguage) {
|
|
91
|
+
binding.clearRequest();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
binding.request(desiredLanguage);
|
|
95
|
+
},
|
|
96
|
+
[binding, desiredLanguage, enabled, target],
|
|
97
|
+
requestSlot,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useEffect(
|
|
101
|
+
() => {
|
|
102
|
+
if (
|
|
103
|
+
!enabled ||
|
|
104
|
+
!target ||
|
|
105
|
+
!desiredLanguage ||
|
|
106
|
+
typeof window === 'undefined'
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const retryCurrentIntent = () => binding.request(desiredLanguage);
|
|
111
|
+
const retryVisibleIntent = () => {
|
|
112
|
+
if (document.visibilityState === 'visible') {
|
|
113
|
+
retryCurrentIntent();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
window.addEventListener('online', retryCurrentIntent);
|
|
117
|
+
document.addEventListener('visibilitychange', retryVisibleIntent);
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener('online', retryCurrentIntent);
|
|
120
|
+
document.removeEventListener('visibilitychange', retryVisibleIntent);
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
[binding, desiredLanguage, enabled, target],
|
|
124
|
+
retrySlot,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return useCallback(
|
|
128
|
+
(language: string) => binding.request(language),
|
|
129
|
+
[binding],
|
|
130
|
+
requestCallbackSlot,
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The slice of an i18next-shaped instance this binding needs. Deliberately
|
|
5
|
+
* not `@modern-js/plugin-i18n`'s `I18nInstance`: that type is intentionally
|
|
6
|
+
* narrowed (no index signature, non-overloaded methods — see plugin-i18n's
|
|
7
|
+
* fork notes) and, more importantly, the plugin package's peer dependencies
|
|
8
|
+
* require React/ReactDOM, which an Octane-only application must never need
|
|
9
|
+
* to install. `on`/`off` are real i18next `EventEmitter` methods this
|
|
10
|
+
* interface adds back for the reactive `languageChanged` subscription.
|
|
11
|
+
*/
|
|
12
|
+
export interface I18nInstanceLike {
|
|
13
|
+
language: string;
|
|
14
|
+
t(key: string | string[], options?: Record<string, unknown>): string;
|
|
15
|
+
changeLanguage?(lng?: string): Promise<unknown> | unknown;
|
|
16
|
+
on?(event: 'languageChanged', callback: (lng: string) => void): void;
|
|
17
|
+
off?(event: 'languageChanged', callback: (lng: string) => void): void;
|
|
18
|
+
/** SSR per-request isolation: the server clones a base instance per request. */
|
|
19
|
+
cloneInstance?(options?: Record<string, unknown>): I18nInstanceLike;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface I18nContextValue {
|
|
23
|
+
/** Per-request instance. Never a module-level singleton. */
|
|
24
|
+
instance: I18nInstanceLike;
|
|
25
|
+
languages: readonly string[];
|
|
26
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
27
|
+
/**
|
|
28
|
+
* Reactive current language, owned by `I18nProvider`'s own `useState` +
|
|
29
|
+
* one `languageChanged` subscription. `instance.language` is a plain
|
|
30
|
+
* mutable field, not itself reactive — Octane's context only re-renders
|
|
31
|
+
* consumers when the Provider commits a *new* `value` object (its
|
|
32
|
+
* `$$version` bumps), so every consumer reads this field off the context
|
|
33
|
+
* value instead of reaching into `instance.language` directly.
|
|
34
|
+
*/
|
|
35
|
+
language: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configuredLanguage,
|
|
3
|
+
localizePath,
|
|
4
|
+
} from '@modern-js/i18n-runtime-extensions/paths';
|
|
5
|
+
import { languageFromPathname } from '@modern-js/i18n-runtime-extensions/request-language';
|
|
6
|
+
import { useContext } from 'octane';
|
|
7
|
+
import { useRouter } from '../router';
|
|
8
|
+
import { I18nContext } from './context';
|
|
9
|
+
import type { I18nInstanceLike } from './types';
|
|
10
|
+
|
|
11
|
+
export interface UseI18nReturn {
|
|
12
|
+
t: I18nInstanceLike['t'];
|
|
13
|
+
/** Current language. Re-renders the calling component on `languageChanged`. */
|
|
14
|
+
language: string;
|
|
15
|
+
languages: readonly string[];
|
|
16
|
+
/** The raw per-request instance, for advanced use (namespaces, `exists`, ...). */
|
|
17
|
+
instance: I18nInstanceLike;
|
|
18
|
+
/**
|
|
19
|
+
* Switches the i18next instance's language. With a router, also navigates to
|
|
20
|
+
* the localized path so the URL and active language stay aligned.
|
|
21
|
+
*/
|
|
22
|
+
changeLanguage: (language: string) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Language switches per instance run one at a time: a switch that a newer
|
|
27
|
+
* one has superseded before it starts is skipped, so no two switches ever
|
|
28
|
+
* interleave their language loads and navigations.
|
|
29
|
+
*/
|
|
30
|
+
const languageSwitches = new WeakMap<
|
|
31
|
+
object,
|
|
32
|
+
{ latest: number; queue: Promise<void> }
|
|
33
|
+
>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Must render under `I18nProvider`; throws rather than silently reading a
|
|
37
|
+
* fallback instance when no provider is mounted (Octane's `createContext`
|
|
38
|
+
* requires a default value, so `I18nContext`'s default is `null` and this
|
|
39
|
+
* hook turns that into a loud error instead of a shared singleton).
|
|
40
|
+
*/
|
|
41
|
+
export function useI18n(): UseI18nReturn {
|
|
42
|
+
const context = useContext(I18nContext);
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'[renderer-octane] useI18n() was called outside of an I18nProvider.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const { instance, languages, localisedUrls, language } = context;
|
|
49
|
+
|
|
50
|
+
const t: I18nInstanceLike['t'] = (key, options) => instance.t(key, options);
|
|
51
|
+
|
|
52
|
+
const router = useRouter({ warn: false });
|
|
53
|
+
|
|
54
|
+
const changeLanguage = (requested: string): Promise<void> => {
|
|
55
|
+
// i18next would fall back silently and the URL would gain an unknown
|
|
56
|
+
// segment. An unsupported request is rejected before it supersedes any
|
|
57
|
+
// queued switch.
|
|
58
|
+
let nextLanguage: string;
|
|
59
|
+
try {
|
|
60
|
+
nextLanguage = configuredLanguage(requested, languages);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return Promise.reject(error);
|
|
63
|
+
}
|
|
64
|
+
const state = languageSwitches.get(instance) ?? {
|
|
65
|
+
latest: 0,
|
|
66
|
+
queue: Promise.resolve(),
|
|
67
|
+
};
|
|
68
|
+
languageSwitches.set(instance, state);
|
|
69
|
+
const generation = ++state.latest;
|
|
70
|
+
const run = state.queue.then(async () => {
|
|
71
|
+
if (state.latest !== generation) return;
|
|
72
|
+
// Earlier switches have settled, so this is the language the current
|
|
73
|
+
// URL was committed with.
|
|
74
|
+
const committed = instance.language;
|
|
75
|
+
// The public URL carries the locale. Under the i18n URL rewrite the
|
|
76
|
+
// router's own location is canonical and has none.
|
|
77
|
+
const publicLanguage = () =>
|
|
78
|
+
router
|
|
79
|
+
? languageFromPathname(
|
|
80
|
+
new URL(router.state.location.publicHref, 'http://localhost')
|
|
81
|
+
.pathname,
|
|
82
|
+
languages,
|
|
83
|
+
router.options.basepath,
|
|
84
|
+
)
|
|
85
|
+
: undefined;
|
|
86
|
+
// A failed, blocked or redirected switch leaves the page on some URL;
|
|
87
|
+
// the language follows that URL's locale, or the committed language on
|
|
88
|
+
// a URL without one.
|
|
89
|
+
const reconcile = async () => {
|
|
90
|
+
const language = publicLanguage() ?? committed;
|
|
91
|
+
if (instance.language !== language)
|
|
92
|
+
await instance.changeLanguage?.(language);
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
await instance.changeLanguage?.(nextLanguage);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
await reconcile();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (!router) return;
|
|
101
|
+
// The full href keeps the query and fragment across the language switch.
|
|
102
|
+
const href = localizePath(router.state.location.href, nextLanguage, {
|
|
103
|
+
languages: [...languages],
|
|
104
|
+
localisedUrls,
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
await router.navigate({ to: '.', href, replace: true });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
await reconcile();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (publicLanguage() !== nextLanguage) await reconcile();
|
|
113
|
+
});
|
|
114
|
+
state.queue = run.catch(() => {});
|
|
115
|
+
return run;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return { t, language, languages, instance, changeLanguage };
|
|
119
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { RendererIdentity } from '@modern-js/renderer-core/identity';
|
|
2
|
+
export type { OctaneDocumentBootstrap } from './bootstrap';
|
|
3
|
+
|
|
4
|
+
export const OCTANE_RUNTIME_PROFILE = Object.freeze({
|
|
5
|
+
renderer: 'octane',
|
|
6
|
+
runtime: '0.7.1+ultramodern.1331985ea3b0',
|
|
7
|
+
router: '0.1.60+ultramodern.0b9f76ee3003',
|
|
8
|
+
routerCore: '1.171.15',
|
|
9
|
+
history: '1.162.0',
|
|
10
|
+
});
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { NativeCompilerArtifacts } from '@modern-js/renderer-core/adapter';
|
|
2
|
+
import {
|
|
3
|
+
nativeModuleManifestFilename,
|
|
4
|
+
type RendererIdentity,
|
|
5
|
+
readRendererIdentity,
|
|
6
|
+
} from '@modern-js/renderer-core/identity';
|
|
7
|
+
import {
|
|
8
|
+
assertNativeHydrationBuildId,
|
|
9
|
+
assertOctaneIdentity,
|
|
10
|
+
} from './bootstrap';
|
|
11
|
+
|
|
12
|
+
export const OCTANE_RUNTIME_VERSION = '0.7.1+ultramodern.1331985ea3b0';
|
|
13
|
+
export const OCTANE_COMPILER_VERSION = '0.1.55';
|
|
14
|
+
|
|
15
|
+
export interface OctaneCompiledSource {
|
|
16
|
+
readonly resource: string;
|
|
17
|
+
readonly canonicalId: string;
|
|
18
|
+
readonly moduleId: string | number;
|
|
19
|
+
readonly transformKind: 'compile' | 'slots' | 'client-only-stub';
|
|
20
|
+
readonly emittedSourceSha256: string;
|
|
21
|
+
readonly assets: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OctaneModuleManifest {
|
|
25
|
+
readonly schemaVersion: 1;
|
|
26
|
+
readonly renderer: 'octane';
|
|
27
|
+
readonly runtimeVersion: typeof OCTANE_RUNTIME_VERSION;
|
|
28
|
+
readonly compilerVersion: typeof OCTANE_COMPILER_VERSION;
|
|
29
|
+
readonly rendererIdentity: Readonly<RendererIdentity>;
|
|
30
|
+
readonly nativeHydrationBuildId: string;
|
|
31
|
+
readonly sourceModules: readonly OctaneCompiledSource[];
|
|
32
|
+
readonly assets: readonly {
|
|
33
|
+
readonly file: string;
|
|
34
|
+
readonly sha256: string;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function octaneModuleManifestFileName(entryName: string): string {
|
|
39
|
+
return nativeModuleManifestFilename('octane', entryName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function record(
|
|
43
|
+
value: unknown,
|
|
44
|
+
fields: readonly string[],
|
|
45
|
+
): Record<string, unknown> {
|
|
46
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
throw new Error('Malformed Octane compiler manifest object.');
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(value).some(key => !fields.includes(key))) {
|
|
50
|
+
throw new Error('Octane compiler manifest contains an unknown field.');
|
|
51
|
+
}
|
|
52
|
+
return value as Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function digest(value: unknown): value is string {
|
|
56
|
+
return typeof value === 'string' && /^[a-f\d]{64}$/u.test(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nonempty(value: unknown): value is string {
|
|
60
|
+
return (
|
|
61
|
+
typeof value === 'string' &&
|
|
62
|
+
value.trim().length > 0 &&
|
|
63
|
+
!value.includes('\0')
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assetPath(value: unknown): value is string {
|
|
68
|
+
return (
|
|
69
|
+
nonempty(value) &&
|
|
70
|
+
value.endsWith('.js') &&
|
|
71
|
+
!value.startsWith('/') &&
|
|
72
|
+
!value.includes('\\') &&
|
|
73
|
+
!value.split('/').some(part => part === '..' || part === '.' || !part)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A build's manifest is an immutable artifact: its shape is checked once. */
|
|
78
|
+
const validatedManifests = new WeakMap<object, OctaneModuleManifest>();
|
|
79
|
+
|
|
80
|
+
/** Validates completed native bytes separately from the source/profile build. */
|
|
81
|
+
export function validateOctaneModuleManifest(
|
|
82
|
+
value: unknown,
|
|
83
|
+
expectedIdentity: RendererIdentity,
|
|
84
|
+
expectedNativeHydrationBuildId?: string,
|
|
85
|
+
): OctaneModuleManifest {
|
|
86
|
+
assertOctaneIdentity(expectedIdentity);
|
|
87
|
+
const cached =
|
|
88
|
+
value && typeof value === 'object'
|
|
89
|
+
? validatedManifests.get(value)
|
|
90
|
+
: undefined;
|
|
91
|
+
const manifest = cached ?? readOctaneModuleManifest(value);
|
|
92
|
+
readRendererIdentity(manifest.rendererIdentity, expectedIdentity);
|
|
93
|
+
if (expectedNativeHydrationBuildId !== undefined) {
|
|
94
|
+
assertNativeHydrationBuildId(expectedNativeHydrationBuildId);
|
|
95
|
+
if (manifest.nativeHydrationBuildId !== expectedNativeHydrationBuildId) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Octane native hydration build differs from the compiled client.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!cached) {
|
|
102
|
+
validatedManifests.set(value as object, manifest);
|
|
103
|
+
validatedManifests.set(manifest, manifest);
|
|
104
|
+
}
|
|
105
|
+
return manifest;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readOctaneModuleManifest(value: unknown): OctaneModuleManifest {
|
|
109
|
+
const manifest = record(value, [
|
|
110
|
+
'schemaVersion',
|
|
111
|
+
'renderer',
|
|
112
|
+
'runtimeVersion',
|
|
113
|
+
'compilerVersion',
|
|
114
|
+
'rendererIdentity',
|
|
115
|
+
'nativeHydrationBuildId',
|
|
116
|
+
'sourceModules',
|
|
117
|
+
'assets',
|
|
118
|
+
]);
|
|
119
|
+
if (
|
|
120
|
+
manifest.schemaVersion !== 1 ||
|
|
121
|
+
manifest.renderer !== 'octane' ||
|
|
122
|
+
manifest.runtimeVersion !== OCTANE_RUNTIME_VERSION ||
|
|
123
|
+
manifest.compilerVersion !== OCTANE_COMPILER_VERSION
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
'Octane compiler manifest ABI conflicts with the application.',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
// The caller compares this well-formed record with its expected identity.
|
|
130
|
+
const rendererIdentity = readRendererIdentity(
|
|
131
|
+
manifest.rendererIdentity,
|
|
132
|
+
manifest.rendererIdentity as RendererIdentity,
|
|
133
|
+
);
|
|
134
|
+
const nativeHydrationBuildId = manifest.nativeHydrationBuildId;
|
|
135
|
+
assertNativeHydrationBuildId(nativeHydrationBuildId);
|
|
136
|
+
if (!Array.isArray(manifest.assets) || !manifest.assets.length) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'Octane compiler manifest has no emitted JavaScript closure.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const assetNames = new Set<string>();
|
|
142
|
+
const assets = manifest.assets.map(value => {
|
|
143
|
+
const asset = record(value, ['file', 'sha256']);
|
|
144
|
+
if (
|
|
145
|
+
!assetPath(asset.file) ||
|
|
146
|
+
!digest(asset.sha256) ||
|
|
147
|
+
assetNames.has(asset.file)
|
|
148
|
+
) {
|
|
149
|
+
throw new Error('Invalid Octane emitted asset digest.');
|
|
150
|
+
}
|
|
151
|
+
assetNames.add(asset.file);
|
|
152
|
+
return Object.freeze({ file: asset.file, sha256: asset.sha256 });
|
|
153
|
+
});
|
|
154
|
+
if (
|
|
155
|
+
!Array.isArray(manifest.sourceModules) ||
|
|
156
|
+
!manifest.sourceModules.length
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'Octane compiler manifest has no authenticated native sources.',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const resources = new Set<string>();
|
|
163
|
+
const sourceModules = manifest.sourceModules.map(value => {
|
|
164
|
+
const source = record(value, [
|
|
165
|
+
'resource',
|
|
166
|
+
'canonicalId',
|
|
167
|
+
'moduleId',
|
|
168
|
+
'transformKind',
|
|
169
|
+
'emittedSourceSha256',
|
|
170
|
+
'assets',
|
|
171
|
+
]);
|
|
172
|
+
if (
|
|
173
|
+
!nonempty(source.resource) ||
|
|
174
|
+
source.resource.startsWith('/') ||
|
|
175
|
+
source.resource.includes('\\') ||
|
|
176
|
+
!nonempty(source.canonicalId) ||
|
|
177
|
+
!(
|
|
178
|
+
(typeof source.moduleId === 'string' && nonempty(source.moduleId)) ||
|
|
179
|
+
(typeof source.moduleId === 'number' &&
|
|
180
|
+
Number.isFinite(source.moduleId))
|
|
181
|
+
) ||
|
|
182
|
+
!(
|
|
183
|
+
source.transformKind === 'compile' ||
|
|
184
|
+
source.transformKind === 'slots' ||
|
|
185
|
+
source.transformKind === 'client-only-stub'
|
|
186
|
+
) ||
|
|
187
|
+
!digest(source.emittedSourceSha256) ||
|
|
188
|
+
!Array.isArray(source.assets) ||
|
|
189
|
+
!source.assets.length ||
|
|
190
|
+
source.assets.some(
|
|
191
|
+
file => typeof file !== 'string' || !assetNames.has(file),
|
|
192
|
+
) ||
|
|
193
|
+
new Set(source.assets).size !== source.assets.length ||
|
|
194
|
+
resources.has(source.resource)
|
|
195
|
+
) {
|
|
196
|
+
throw new Error('Invalid Octane native source-to-asset provenance.');
|
|
197
|
+
}
|
|
198
|
+
resources.add(source.resource);
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
resource: source.resource,
|
|
201
|
+
canonicalId: source.canonicalId,
|
|
202
|
+
moduleId: source.moduleId,
|
|
203
|
+
transformKind: source.transformKind,
|
|
204
|
+
emittedSourceSha256: source.emittedSourceSha256,
|
|
205
|
+
assets: Object.freeze([...source.assets] as string[]),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
renderer: 'octane',
|
|
211
|
+
runtimeVersion: OCTANE_RUNTIME_VERSION,
|
|
212
|
+
compilerVersion: OCTANE_COMPILER_VERSION,
|
|
213
|
+
rendererIdentity,
|
|
214
|
+
nativeHydrationBuildId,
|
|
215
|
+
sourceModules: Object.freeze(sourceModules),
|
|
216
|
+
assets: Object.freeze(assets),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The compiler artifact ABI. It lives in this runtime module, not in the
|
|
222
|
+
* build-only `./plugin`, because the production server validates manifests
|
|
223
|
+
* with it and deployments trace it from there.
|
|
224
|
+
*/
|
|
225
|
+
export const compilerArtifacts: NativeCompilerArtifacts = {
|
|
226
|
+
clientManifestFile: octaneModuleManifestFileName,
|
|
227
|
+
async validateClientManifest(value, identity, context) {
|
|
228
|
+
const hydrationBuildId =
|
|
229
|
+
context.compilationHash ?? context.hydrationBuildId;
|
|
230
|
+
if (
|
|
231
|
+
context.development &&
|
|
232
|
+
(typeof hydrationBuildId !== 'string' || !hydrationBuildId.trim())
|
|
233
|
+
)
|
|
234
|
+
throw new Error(
|
|
235
|
+
'Octane development snapshot has no native hydration build.',
|
|
236
|
+
);
|
|
237
|
+
const nativeManifest = validateOctaneModuleManifest(
|
|
238
|
+
value,
|
|
239
|
+
identity,
|
|
240
|
+
hydrationBuildId,
|
|
241
|
+
);
|
|
242
|
+
return {
|
|
243
|
+
nativeManifest,
|
|
244
|
+
hydrationBuildId: nativeManifest.nativeHydrationBuildId,
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
isMutableDevelopmentAsset(filename, entryNames) {
|
|
248
|
+
return (
|
|
249
|
+
filename === 'octane-client-build.json' ||
|
|
250
|
+
entryNames.some(entry => filename === octaneModuleManifestFileName(entry))
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
};
|