@mfe-orchestrator-hub/client 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lorenzo De Francesco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @mfe-orchestrator-hub/client
2
+
3
+ Framework agnostic client for [MFE Orchestrator](https://github.com/mfe-orchestrator). It asks the
4
+ console which microfrontends the current environment serves and hands back their ready to use,
5
+ version pinned URLs.
6
+
7
+ Zero runtime dependencies, ESM + CJS, ships its own types.
8
+
9
+ ## The one rule
10
+
11
+ **The host page never decides which version it gets.** It sends the identities it holds and uses the
12
+ URL it receives verbatim. It does not parse the version out of the URL, does not draw any random
13
+ number, does not know that a canary exists. All of that lives in the backend.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add @mfe-orchestrator-hub/client
19
+ ```
20
+
21
+ ## Configure
22
+
23
+ Call `configure()` at the very top of the host entry point, before anything imports a remote.
24
+
25
+ ```ts
26
+ import { configure } from "@mfe-orchestrator-hub/client"
27
+
28
+ configure({
29
+ backendUrl: import.meta.env.VITE_MFE_BACKEND_URL,
30
+ projectId: import.meta.env.VITE_MFE_PROJECT_ID,
31
+ environment: import.meta.env.VITE_MFE_ENVIRONMENT
32
+ })
33
+ ```
34
+
35
+ `configure()` is idempotent: calling it again with the same options is a no op, so a framework
36
+ provider may call it on every render. A second call with different options is ignored, with a
37
+ warning, because the manifest of this page load may already be in flight.
38
+
39
+ ## Bundler configuration
40
+
41
+ The string below is injected into the host bundle, so the bare specifier is resolved by the host's
42
+ own bundler. This is exactly the form the console generates.
43
+
44
+ ### Vite — `@originjs/vite-plugin-federation`
45
+
46
+ ```js
47
+ federation({
48
+ name: "shell",
49
+ remotes: {
50
+ checkoutnew: {
51
+ external: `import('@mfe-orchestrator-hub/client').then(m => m.remoteUrl('checkout-new'))`,
52
+ externalType: "promise"
53
+ }
54
+ },
55
+ shared: ["react", "react-dom"]
56
+ })
57
+ ```
58
+
59
+ ### Webpack — `ModuleFederationPlugin`
60
+
61
+ ```js
62
+ new ModuleFederationPlugin({
63
+ name: "shell",
64
+ remotes: {
65
+ checkoutnew: `promise import('@mfe-orchestrator-hub/client').then(m => m.remoteUrl('checkout-new'))`
66
+ },
67
+ shared: { react: { singleton: true }, "react-dom": { singleton: true } }
68
+ })
69
+ ```
70
+
71
+ ## API
72
+
73
+ ```ts
74
+ export interface OrchestratorConfig {
75
+ backendUrl: string // ex. "https://console.mfe-orchestrator.dev/api"
76
+ projectId: string
77
+ environment: string // environment slug, ex. "DEV"
78
+ userId?: string | (() => string | undefined | Promise<string | undefined>)
79
+ }
80
+
81
+ /** Call once, synchronously, before any remote is imported. Idempotent. */
82
+ export function configure(config: OrchestratorConfig): void
83
+
84
+ /** Resolves the ready to use, version pinned URL of a remote. Awaits the manifest internally. */
85
+ export function remoteUrl(slug: string): Promise<string>
86
+
87
+ /** Whole manifest, fetched once and memoized. */
88
+ export function manifest(): Promise<Manifest>
89
+
90
+ /** Global variables of the environment, as a plain object. */
91
+ export function globalVariables(): Promise<Record<string, string>>
92
+
93
+ /** The two ids, exposed for telemetry/debugging. */
94
+ export function identities(): { sessionId: string; deviceId: string }
95
+ ```
96
+
97
+ The types `Manifest`, `Microfrontend`, `GlobalVariable`, `Identities` and `OrchestratorConfig` are
98
+ exported as well.
99
+
100
+ ### `remoteUrl(slug)`
101
+
102
+ ```ts
103
+ import { remoteUrl } from "@mfe-orchestrator-hub/client"
104
+
105
+ const url = await remoteUrl("checkout-new")
106
+ // https://console…/serve/mfe/files/auto/<projectId>/checkout-new/_v/1.5.0-rc1/assets/remoteEntry.js
107
+ ```
108
+
109
+ Use that URL as it is. Never rebuild it by hand and never strip the `_v/<version>/` segment: a
110
+ classic script (webpack `publicPath: "auto"`) derives the base of its chunks from
111
+ `document.currentScript.src`, which is the URL *before* any redirect, so without the segment already
112
+ in place it would request its chunks off a different version and mix two builds in one page.
113
+
114
+ An unknown slug rejects with a message listing the slugs the environment does serve.
115
+
116
+ ### `globalVariables()`
117
+
118
+ ```ts
119
+ const variables = await globalVariables()
120
+ // { API_URL: "https://…", FEATURE_FLAG: "on" }
121
+ ```
122
+
123
+ ### `identities()`
124
+
125
+ ```ts
126
+ const { sessionId, deviceId } = identities()
127
+ ```
128
+
129
+ ## How it talks to the backend
130
+
131
+ One single successful request per page load, memoized as a promise so that concurrent callers share
132
+ it:
133
+
134
+ ```
135
+ GET {backendUrl}/serve/all/{projectId}/{environment}
136
+ ?mfeSessionId=<uuid>&mfeDeviceId=<uuid>&mfeUserId=<optional>
137
+ ```
138
+
139
+ ### Why identities travel in the query string
140
+
141
+ Microfrontends are loaded with a cross-site `import()`. Module scripts are fetched with a fixed
142
+ `same-origin` credentials mode, so no cookie of the console domain is ever sent with them and no
143
+ `Set-Cookie` of ours is stored. The host page is the only place holding this state and the URL is the
144
+ only way to hand it over.
145
+
146
+ | query param | where it is kept | storage key | lifetime |
147
+ | --- | --- | --- | --- |
148
+ | `mfeSessionId` | `sessionStorage` | `mfe-orchestrator.sessionId` | dies with the browser session |
149
+ | `mfeDeviceId` | `localStorage` | `mfe-orchestrator.deviceId` | survives browser restarts |
150
+ | `mfeUserId` | supplied by the host app | — | not stored by the SDK |
151
+
152
+ All three are sent whenever available: the client does not know, and must not guess, which one the
153
+ backend uses.
154
+
155
+ `userId` may be a value, a getter, or an async getter. It is resolved as late as possible, right
156
+ before the request is issued, so a host that only knows its user after an auth round trip can pass a
157
+ function.
158
+
159
+ Both ids are generated with `crypto.randomUUID()` on first use and persisted. Storage can throw —
160
+ Safari in private mode, disabled storage, hardened browsers that throw on the property access
161
+ itself — and in that case the client falls back to ids held in memory for the lifetime of the page.
162
+ This never throws out of the SDK.
163
+
164
+ ### Successes are cached, failures are not
165
+
166
+ A resolved manifest is memoized for the lifetime of the page. A failed attempt is not: when the
167
+ shared attempt settles as rejected, the memo is dropped, so the next call to `manifest()`,
168
+ `remoteUrl()` or `globalVariables()` starts a fresh request.
169
+
170
+ Every caller already waiting on the failed attempt still gets that rejection — the memo is cleared
171
+ once, when the attempt settles, not in each caller's own `catch` — so a failure rejects all of them
172
+ and costs exactly one request.
173
+
174
+ This is not a retry: nothing here loops, schedules or backs off. It only means that one flaky lookup
175
+ or one 502 during a console redeploy does not condemn the page to reject every `remoteUrl()` until a
176
+ full reload. Recovering is the host app's call, by asking again.
177
+
178
+ ## Not in v1
179
+
180
+ No automatic retries, no offline caching, no service worker, and nothing that reads or writes
181
+ cookies. A network error is surfaced exactly as `fetch` produced it.
182
+
183
+ ## Framework adapters
184
+
185
+ - React — [`@mfe-orchestrator-hub/client-react`](https://github.com/mfe-orchestrator/client-react)
186
+ - Vue — [`@mfe-orchestrator-hub/client-vue`](https://github.com/mfe-orchestrator/client-vue)
187
+ - Angular — [`@mfe-orchestrator-hub/client-angular`](https://github.com/mfe-orchestrator/client-angular)
188
+
189
+ They are ergonomics only: every decision lives here, in the core.
190
+
191
+ ## Development
192
+
193
+ ```sh
194
+ pnpm install
195
+ pnpm test # vitest
196
+ pnpm build # tsup, ESM + CJS + types
197
+ pnpm typecheck
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT © Lorenzo De Francesco
package/dist/index.cjs ADDED
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ // src/state.ts
4
+ var STATE_KEY = "__mfe_orchestrator_client__";
5
+ var globalSlot = globalThis;
6
+ var getState = () => {
7
+ let state = globalSlot[STATE_KEY];
8
+ if (!state) {
9
+ state = { config: null, manifestPromise: null, identities: null };
10
+ globalSlot[STATE_KEY] = state;
11
+ }
12
+ return state;
13
+ };
14
+
15
+ // src/storage.ts
16
+ var SESSION_ID_KEY = "mfe-orchestrator.sessionId";
17
+ var DEVICE_ID_KEY = "mfe-orchestrator.deviceId";
18
+ var PROBE_KEY = "mfe-orchestrator.probe";
19
+ var inMemoryIds = /* @__PURE__ */ new Map();
20
+ var uuidFromRandomBytes = (bytes) => {
21
+ bytes[6] = bytes[6] & 15 | 64;
22
+ bytes[8] = bytes[8] & 63 | 128;
23
+ const hex = [];
24
+ for (const byte of bytes) {
25
+ hex.push(byte.toString(16).padStart(2, "0"));
26
+ }
27
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
28
+ };
29
+ var newId = () => {
30
+ const cryptoApi = globalThis.crypto;
31
+ try {
32
+ if (typeof cryptoApi?.randomUUID === "function") {
33
+ return cryptoApi.randomUUID();
34
+ }
35
+ if (typeof cryptoApi?.getRandomValues === "function") {
36
+ return uuidFromRandomBytes(cryptoApi.getRandomValues(new Uint8Array(16)));
37
+ }
38
+ } catch {
39
+ }
40
+ const bytes = new Uint8Array(16);
41
+ for (let index = 0; index < bytes.length; index++) {
42
+ bytes[index] = Math.floor(Math.random() * 256);
43
+ }
44
+ return uuidFromRandomBytes(bytes);
45
+ };
46
+ var openStorage = (kind) => {
47
+ try {
48
+ const storage = kind === "local" ? globalThis.localStorage : globalThis.sessionStorage;
49
+ if (!storage) {
50
+ return null;
51
+ }
52
+ storage.setItem(PROBE_KEY, "1");
53
+ storage.removeItem(PROBE_KEY);
54
+ return storage;
55
+ } catch {
56
+ return null;
57
+ }
58
+ };
59
+ var persistentId = (kind, key) => {
60
+ const storage = openStorage(kind);
61
+ if (storage) {
62
+ try {
63
+ const stored = storage.getItem(key);
64
+ if (stored) {
65
+ return stored;
66
+ }
67
+ const created2 = newId();
68
+ storage.setItem(key, created2);
69
+ return created2;
70
+ } catch {
71
+ }
72
+ }
73
+ const remembered = inMemoryIds.get(key);
74
+ if (remembered) {
75
+ return remembered;
76
+ }
77
+ const created = newId();
78
+ inMemoryIds.set(key, created);
79
+ return created;
80
+ };
81
+ var readIdentities = () => ({
82
+ sessionId: persistentId("session", SESSION_ID_KEY),
83
+ deviceId: persistentId("local", DEVICE_ID_KEY)
84
+ });
85
+
86
+ // src/index.ts
87
+ var PREFIX = "[@mfe-orchestrator-hub/client]";
88
+ var NOT_CONFIGURED = `${PREFIX} call configure() first. Configure the client at the very top of the host entry point, before anything imports a remote:
89
+
90
+ import { configure } from "@mfe-orchestrator-hub/client"
91
+
92
+ configure({ backendUrl: "\u2026", projectId: "\u2026", environment: "\u2026" })`;
93
+ var withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
94
+ var resolveUserId = async (userId) => {
95
+ const value = typeof userId === "function" ? await userId() : userId;
96
+ return value === void 0 || value === null || value === "" ? void 0 : String(value);
97
+ };
98
+ var sameConfig = (left, right) => left.backendUrl === right.backendUrl && left.projectId === right.projectId && left.environment === right.environment && left.userId === right.userId;
99
+ var requireConfig = (config) => {
100
+ const missing = ["backendUrl", "projectId", "environment"].filter((key) => !config?.[key]);
101
+ if (missing.length > 0) {
102
+ throw new Error(`${PREFIX} configure() is missing required option${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}.`);
103
+ }
104
+ };
105
+ var manifestUrl = async (config) => {
106
+ const { sessionId, deviceId } = identities();
107
+ const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId });
108
+ const userId = await resolveUserId(config.userId);
109
+ if (userId) {
110
+ query.set("mfeUserId", userId);
111
+ }
112
+ return `${withoutTrailingSlash(config.backendUrl)}/serve/all/${encodeURIComponent(config.projectId)}/${encodeURIComponent(config.environment)}?${query.toString()}`;
113
+ };
114
+ var fetchManifest = async (config) => {
115
+ const url = await manifestUrl(config);
116
+ const response = await fetch(url, { headers: { accept: "application/json" } });
117
+ if (!response.ok) {
118
+ throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`);
119
+ }
120
+ const payload = await response.json();
121
+ return {
122
+ globalVariables: payload?.globalVariables ?? [],
123
+ microfrontends: payload?.microfrontends ?? []
124
+ };
125
+ };
126
+ var configure = (config) => {
127
+ requireConfig(config);
128
+ const state = getState();
129
+ const current = state.config;
130
+ if (current) {
131
+ if (!sameConfig(current, config)) {
132
+ console.warn(
133
+ `${PREFIX} configure() was called again with a different configuration. The first one is kept: ${JSON.stringify({ backendUrl: current.backendUrl, projectId: current.projectId, environment: current.environment })}.`
134
+ );
135
+ }
136
+ return;
137
+ }
138
+ state.config = { ...config };
139
+ };
140
+ var manifest = () => {
141
+ const state = getState();
142
+ const config = state.config;
143
+ if (!config) {
144
+ return Promise.reject(new Error(NOT_CONFIGURED));
145
+ }
146
+ if (!state.manifestPromise) {
147
+ const attempt = fetchManifest(config);
148
+ state.manifestPromise = attempt;
149
+ attempt.catch(() => {
150
+ if (state.manifestPromise === attempt) {
151
+ state.manifestPromise = null;
152
+ }
153
+ });
154
+ }
155
+ return state.manifestPromise;
156
+ };
157
+ var remoteUrl = async (slug) => {
158
+ const { microfrontends } = await manifest();
159
+ const found = microfrontends.find((microfrontend) => microfrontend.slug === slug);
160
+ if (!found) {
161
+ const available = microfrontends.map((microfrontend) => `"${microfrontend.slug}"`).join(", ");
162
+ const { environment } = getState().config ?? { environment: "?" };
163
+ throw new Error(`${PREFIX} unknown microfrontend slug "${slug}". Available in environment "${environment}": ${available || "none, this environment serves no microfrontend"}.`);
164
+ }
165
+ if (!found.url) {
166
+ throw new Error(`${PREFIX} the microfrontend "${slug}" is in the manifest but carries no url. Check its deployment in the console.`);
167
+ }
168
+ return found.url;
169
+ };
170
+ var globalVariables = async () => {
171
+ const { globalVariables: variables } = await manifest();
172
+ const result = {};
173
+ for (const variable of variables) {
174
+ if (variable && typeof variable.key === "string") {
175
+ result[variable.key] = variable.value;
176
+ }
177
+ }
178
+ return result;
179
+ };
180
+ var identities = () => {
181
+ const state = getState();
182
+ if (!state.identities) {
183
+ state.identities = readIdentities();
184
+ }
185
+ return state.identities;
186
+ };
187
+
188
+ exports.configure = configure;
189
+ exports.globalVariables = globalVariables;
190
+ exports.identities = identities;
191
+ exports.manifest = manifest;
192
+ exports.remoteUrl = remoteUrl;
193
+ //# sourceMappingURL=index.cjs.map
194
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/state.ts","../src/storage.ts","../src/index.ts"],"names":["created"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport interface OrchestratorState {\n config: OrchestratorConfig | null\n manifestPromise: Promise<Manifest> | null\n identities: Identities | null\n}\n\n/**\n * The state lives on `globalThis` under this key, not in a module scope variable.\n *\n * A host can end up with two copies of this package loaded at once: the app imports the ESM build\n * while something else pulls the CJS one, or the bundler config and the app resolve to different\n * chunks. Two module scopes would mean two configurations and two manifest requests, which is\n * exactly what the contract forbids. One well known global slot makes the singleton real.\n */\nexport const STATE_KEY = \"__mfe_orchestrator_client__\"\n\nconst globalSlot = globalThis as typeof globalThis & Record<typeof STATE_KEY, OrchestratorState | undefined>\n\nexport const getState = (): OrchestratorState => {\n let state = globalSlot[STATE_KEY]\n if (!state) {\n state = { config: null, manifestPromise: null, identities: null }\n globalSlot[STATE_KEY] = state\n }\n return state\n}\n","import type { Identities } from \"./types\"\n\nexport const SESSION_ID_KEY = \"mfe-orchestrator.sessionId\"\nexport const DEVICE_ID_KEY = \"mfe-orchestrator.deviceId\"\n\nconst PROBE_KEY = \"mfe-orchestrator.probe\"\n\ntype StorageKind = \"local\" | \"session\"\n\n/**\n * Ids minted while storage was unusable. They live for the lifetime of the page so that, even\n * without storage, every request of this page load carries one stable pair of ids.\n */\nconst inMemoryIds = new Map<string, string>()\n\nconst uuidFromRandomBytes = (bytes: Uint8Array): string => {\n bytes[6] = (bytes[6] & 0x0f) | 0x40\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex: string[] = []\n for (const byte of bytes) {\n hex.push(byte.toString(16).padStart(2, \"0\"))\n }\n return `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10, 16).join(\"\")}`\n}\n\n/**\n * A v4 UUID. `crypto.randomUUID` is the intended path; the two fallbacks only exist so that a page\n * served over plain http, or an exotic runtime, still gets an id instead of an exception.\n */\nexport const newId = (): string => {\n const cryptoApi = globalThis.crypto\n try {\n if (typeof cryptoApi?.randomUUID === \"function\") {\n return cryptoApi.randomUUID()\n }\n if (typeof cryptoApi?.getRandomValues === \"function\") {\n return uuidFromRandomBytes(cryptoApi.getRandomValues(new Uint8Array(16)))\n }\n } catch {\n // fall through to the non cryptographic fallback\n }\n const bytes = new Uint8Array(16)\n for (let index = 0; index < bytes.length; index++) {\n bytes[index] = Math.floor(Math.random() * 256)\n }\n return uuidFromRandomBytes(bytes)\n}\n\n/**\n * The requested storage, or `null` when it is missing or unusable.\n *\n * Both the property access and the write can throw: Safari in private mode exposes the object and\n * then refuses `setItem`, some hardened browsers throw a SecurityError on the access itself. The\n * write probe is the only reliable way to tell a usable storage from a decorative one.\n */\nconst openStorage = (kind: StorageKind): Storage | null => {\n try {\n const storage = kind === \"local\" ? globalThis.localStorage : globalThis.sessionStorage\n if (!storage) {\n return null\n }\n storage.setItem(PROBE_KEY, \"1\")\n storage.removeItem(PROBE_KEY)\n return storage\n } catch {\n return null\n }\n}\n\n/**\n * The id under `key`, read from storage and created on first use. Falls back to an id kept in\n * memory when storage cannot be used, so this never throws out of the SDK.\n */\nconst persistentId = (kind: StorageKind, key: string): string => {\n const storage = openStorage(kind)\n if (storage) {\n try {\n const stored = storage.getItem(key)\n if (stored) {\n return stored\n }\n const created = newId()\n storage.setItem(key, created)\n return created\n } catch {\n // storage passed the probe and failed anyway: keep going with an in memory id\n }\n }\n const remembered = inMemoryIds.get(key)\n if (remembered) {\n return remembered\n }\n const created = newId()\n inMemoryIds.set(key, created)\n return created\n}\n\n/** Reads, and creates when needed, the session and device ids. */\nexport const readIdentities = (): Identities => ({\n sessionId: persistentId(\"session\", SESSION_ID_KEY),\n deviceId: persistentId(\"local\", DEVICE_ID_KEY)\n})\n","import { getState } from \"./state\"\nimport { readIdentities } from \"./storage\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport type { GlobalVariable, Identities, Manifest, Microfrontend, OrchestratorConfig } from \"./types\"\n\nconst PREFIX = \"[@mfe-orchestrator-hub/client]\"\n\nconst NOT_CONFIGURED = `${PREFIX} call configure() first. Configure the client at the very top of the host entry point, before anything imports a remote:\n\n import { configure } from \"@mfe-orchestrator-hub/client\"\n\n configure({ backendUrl: \"…\", projectId: \"…\", environment: \"…\" })`\n\nconst withoutTrailingSlash = (value: string): string => value.replace(/\\/+$/, \"\")\n\nconst resolveUserId = async (userId: OrchestratorConfig[\"userId\"]): Promise<string | undefined> => {\n const value = typeof userId === \"function\" ? await userId() : userId\n return value === undefined || value === null || value === \"\" ? undefined : String(value)\n}\n\nconst sameConfig = (left: OrchestratorConfig, right: OrchestratorConfig): boolean =>\n left.backendUrl === right.backendUrl && left.projectId === right.projectId && left.environment === right.environment && left.userId === right.userId\n\nconst requireConfig = (config: OrchestratorConfig): void => {\n const missing = ([\"backendUrl\", \"projectId\", \"environment\"] as const).filter(key => !config?.[key])\n if (missing.length > 0) {\n throw new Error(`${PREFIX} configure() is missing required option${missing.length > 1 ? \"s\" : \"\"}: ${missing.join(\", \")}.`)\n }\n}\n\nconst manifestUrl = async (config: OrchestratorConfig): Promise<string> => {\n const { sessionId, deviceId } = identities()\n const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId })\n // Resolved as late as possible: the host may only know the user after its own auth round trip.\n const userId = await resolveUserId(config.userId)\n if (userId) {\n query.set(\"mfeUserId\", userId)\n }\n return `${withoutTrailingSlash(config.backendUrl)}/serve/all/${encodeURIComponent(config.projectId)}/${encodeURIComponent(config.environment)}?${query.toString()}`\n}\n\nconst fetchManifest = async (config: OrchestratorConfig): Promise<Manifest> => {\n const url = await manifestUrl(config)\n // No retry in v1: whatever fetch throws travels straight to the caller.\n const response = await fetch(url, { headers: { accept: \"application/json\" } })\n if (!response.ok) {\n throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`)\n }\n const payload = (await response.json()) as Partial<Manifest> | null\n return {\n globalVariables: payload?.globalVariables ?? [],\n microfrontends: payload?.microfrontends ?? []\n }\n}\n\n/**\n * Hands the client its configuration. Call it once, synchronously, before any remote is imported.\n *\n * Idempotent: calling it again with the same options is a no op, so a framework provider may safely\n * call it on every render. A second call with different options is ignored, with a warning, because\n * the manifest of this page load may already be in flight or resolved.\n */\nexport const configure = (config: OrchestratorConfig): void => {\n requireConfig(config)\n const state = getState()\n const current = state.config\n if (current) {\n if (!sameConfig(current, config)) {\n console.warn(\n `${PREFIX} configure() was called again with a different configuration. The first one is kept: ${JSON.stringify({ backendUrl: current.backendUrl, projectId: current.projectId, environment: current.environment })}.`\n )\n }\n return\n }\n state.config = { ...config }\n}\n\n/**\n * The whole manifest of the environment, fetched once per page load and memoized as a promise, so\n * that concurrent callers share the single request in flight.\n *\n * A successful result is memoized for good. A failed attempt is not: the memo is dropped when the\n * shared attempt settles as rejected, so the next caller starts a fresh request. That is not a retry\n * — nothing here loops or backs off — it only means one flaky lookup at boot does not condemn the\n * page to reject every `remoteUrl()` until a full reload.\n */\nexport const manifest = (): Promise<Manifest> => {\n const state = getState()\n const config = state.config\n if (!config) {\n // Not memoized: the host may still configure the client and try again.\n return Promise.reject(new Error(NOT_CONFIGURED))\n }\n if (!state.manifestPromise) {\n // Assigned before the first await inside fetchManifest, which is what makes N concurrent\n // callers collapse onto one request.\n const attempt = fetchManifest(config)\n state.manifestPromise = attempt\n // Cleared once, when the shared attempt settles, not in each caller's own catch: every\n // caller already waiting on this attempt still gets its rejection. The identity check keeps\n // a late failure from evicting a newer attempt.\n attempt.catch(() => {\n if (state.manifestPromise === attempt) {\n state.manifestPromise = null\n }\n })\n }\n return state.manifestPromise\n}\n\n/**\n * The ready to use, version pinned URL of a remote. Awaits the manifest internally.\n *\n * The returned URL is used verbatim: the host never parses the version out of it and never strips\n * the `_v/<version>/` segment, or a classic script would load its chunks off another version.\n */\nexport const remoteUrl = async (slug: string): Promise<string> => {\n const { microfrontends } = await manifest()\n const found = microfrontends.find(microfrontend => microfrontend.slug === slug)\n if (!found) {\n const available = microfrontends.map(microfrontend => `\"${microfrontend.slug}\"`).join(\", \")\n const { environment } = getState().config ?? { environment: \"?\" }\n throw new Error(`${PREFIX} unknown microfrontend slug \"${slug}\". Available in environment \"${environment}\": ${available || \"none, this environment serves no microfrontend\"}.`)\n }\n if (!found.url) {\n throw new Error(`${PREFIX} the microfrontend \"${slug}\" is in the manifest but carries no url. Check its deployment in the console.`)\n }\n return found.url\n}\n\n/** The global variables of the environment, as a plain object. */\nexport const globalVariables = async (): Promise<Record<string, string>> => {\n const { globalVariables: variables } = await manifest()\n const result: Record<string, string> = {}\n for (const variable of variables) {\n if (variable && typeof variable.key === \"string\") {\n result[variable.key] = variable.value\n }\n }\n return result\n}\n\n/**\n * The session and device ids of this page, exposed for telemetry and debugging. Created on first\n * use and stable for the lifetime of the page, whether or not storage is available.\n */\nexport const identities = (): Identities => {\n const state = getState()\n if (!state.identities) {\n state.identities = readIdentities()\n }\n return state.identities\n}\n"]}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Configuration of the orchestrator client. Handed over once, at the very top of the host entry point.
3
+ */
4
+ interface OrchestratorConfig {
5
+ /** Base URL of the console API, ex. "https://console.mfe-orchestrator.dev/api". */
6
+ backendUrl: string;
7
+ /** Id of the project the host belongs to. */
8
+ projectId: string;
9
+ /** Environment slug, ex. "DEV". */
10
+ environment: string;
11
+ /**
12
+ * Identity of the logged in user, if the host has one. May be a plain value, a getter, or an
13
+ * async getter resolved right before the manifest request is issued.
14
+ */
15
+ userId?: string | (() => string | undefined | Promise<string | undefined>);
16
+ }
17
+ /** A single environment variable of the deployment. */
18
+ interface GlobalVariable {
19
+ key: string;
20
+ value: string;
21
+ }
22
+ /** A microfrontend as served by the console for the current environment. */
23
+ interface Microfrontend {
24
+ /** Stable, human readable id of the microfrontend. */
25
+ slug: string;
26
+ /** Display name. */
27
+ name: string;
28
+ /** Federation safe name, the one used as the remote key in the bundler config. */
29
+ nameToIntegrate: string;
30
+ /** Version actually served. Informative only: never rebuild the URL out of it. */
31
+ version: string;
32
+ continuousDeployment: boolean;
33
+ /** Ready to use, already version pinned URL of the remote entry. Use it verbatim. */
34
+ url: string;
35
+ }
36
+ /** The whole payload of `GET {backendUrl}/serve/all/{projectId}/{environmentSlug}`. */
37
+ interface Manifest {
38
+ globalVariables: GlobalVariable[];
39
+ microfrontends: Microfrontend[];
40
+ }
41
+ /** The two ids the host holds on behalf of the backend. */
42
+ interface Identities {
43
+ sessionId: string;
44
+ deviceId: string;
45
+ }
46
+
47
+ /**
48
+ * Hands the client its configuration. Call it once, synchronously, before any remote is imported.
49
+ *
50
+ * Idempotent: calling it again with the same options is a no op, so a framework provider may safely
51
+ * call it on every render. A second call with different options is ignored, with a warning, because
52
+ * the manifest of this page load may already be in flight or resolved.
53
+ */
54
+ declare const configure: (config: OrchestratorConfig) => void;
55
+ /**
56
+ * The whole manifest of the environment, fetched once per page load and memoized as a promise, so
57
+ * that concurrent callers share the single request in flight.
58
+ *
59
+ * A successful result is memoized for good. A failed attempt is not: the memo is dropped when the
60
+ * shared attempt settles as rejected, so the next caller starts a fresh request. That is not a retry
61
+ * — nothing here loops or backs off — it only means one flaky lookup at boot does not condemn the
62
+ * page to reject every `remoteUrl()` until a full reload.
63
+ */
64
+ declare const manifest: () => Promise<Manifest>;
65
+ /**
66
+ * The ready to use, version pinned URL of a remote. Awaits the manifest internally.
67
+ *
68
+ * The returned URL is used verbatim: the host never parses the version out of it and never strips
69
+ * the `_v/<version>/` segment, or a classic script would load its chunks off another version.
70
+ */
71
+ declare const remoteUrl: (slug: string) => Promise<string>;
72
+ /** The global variables of the environment, as a plain object. */
73
+ declare const globalVariables: () => Promise<Record<string, string>>;
74
+ /**
75
+ * The session and device ids of this page, exposed for telemetry and debugging. Created on first
76
+ * use and stable for the lifetime of the page, whether or not storage is available.
77
+ */
78
+ declare const identities: () => Identities;
79
+
80
+ export { type GlobalVariable, type Identities, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, remoteUrl };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Configuration of the orchestrator client. Handed over once, at the very top of the host entry point.
3
+ */
4
+ interface OrchestratorConfig {
5
+ /** Base URL of the console API, ex. "https://console.mfe-orchestrator.dev/api". */
6
+ backendUrl: string;
7
+ /** Id of the project the host belongs to. */
8
+ projectId: string;
9
+ /** Environment slug, ex. "DEV". */
10
+ environment: string;
11
+ /**
12
+ * Identity of the logged in user, if the host has one. May be a plain value, a getter, or an
13
+ * async getter resolved right before the manifest request is issued.
14
+ */
15
+ userId?: string | (() => string | undefined | Promise<string | undefined>);
16
+ }
17
+ /** A single environment variable of the deployment. */
18
+ interface GlobalVariable {
19
+ key: string;
20
+ value: string;
21
+ }
22
+ /** A microfrontend as served by the console for the current environment. */
23
+ interface Microfrontend {
24
+ /** Stable, human readable id of the microfrontend. */
25
+ slug: string;
26
+ /** Display name. */
27
+ name: string;
28
+ /** Federation safe name, the one used as the remote key in the bundler config. */
29
+ nameToIntegrate: string;
30
+ /** Version actually served. Informative only: never rebuild the URL out of it. */
31
+ version: string;
32
+ continuousDeployment: boolean;
33
+ /** Ready to use, already version pinned URL of the remote entry. Use it verbatim. */
34
+ url: string;
35
+ }
36
+ /** The whole payload of `GET {backendUrl}/serve/all/{projectId}/{environmentSlug}`. */
37
+ interface Manifest {
38
+ globalVariables: GlobalVariable[];
39
+ microfrontends: Microfrontend[];
40
+ }
41
+ /** The two ids the host holds on behalf of the backend. */
42
+ interface Identities {
43
+ sessionId: string;
44
+ deviceId: string;
45
+ }
46
+
47
+ /**
48
+ * Hands the client its configuration. Call it once, synchronously, before any remote is imported.
49
+ *
50
+ * Idempotent: calling it again with the same options is a no op, so a framework provider may safely
51
+ * call it on every render. A second call with different options is ignored, with a warning, because
52
+ * the manifest of this page load may already be in flight or resolved.
53
+ */
54
+ declare const configure: (config: OrchestratorConfig) => void;
55
+ /**
56
+ * The whole manifest of the environment, fetched once per page load and memoized as a promise, so
57
+ * that concurrent callers share the single request in flight.
58
+ *
59
+ * A successful result is memoized for good. A failed attempt is not: the memo is dropped when the
60
+ * shared attempt settles as rejected, so the next caller starts a fresh request. That is not a retry
61
+ * — nothing here loops or backs off — it only means one flaky lookup at boot does not condemn the
62
+ * page to reject every `remoteUrl()` until a full reload.
63
+ */
64
+ declare const manifest: () => Promise<Manifest>;
65
+ /**
66
+ * The ready to use, version pinned URL of a remote. Awaits the manifest internally.
67
+ *
68
+ * The returned URL is used verbatim: the host never parses the version out of it and never strips
69
+ * the `_v/<version>/` segment, or a classic script would load its chunks off another version.
70
+ */
71
+ declare const remoteUrl: (slug: string) => Promise<string>;
72
+ /** The global variables of the environment, as a plain object. */
73
+ declare const globalVariables: () => Promise<Record<string, string>>;
74
+ /**
75
+ * The session and device ids of this page, exposed for telemetry and debugging. Created on first
76
+ * use and stable for the lifetime of the page, whether or not storage is available.
77
+ */
78
+ declare const identities: () => Identities;
79
+
80
+ export { type GlobalVariable, type Identities, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, remoteUrl };
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ // src/state.ts
2
+ var STATE_KEY = "__mfe_orchestrator_client__";
3
+ var globalSlot = globalThis;
4
+ var getState = () => {
5
+ let state = globalSlot[STATE_KEY];
6
+ if (!state) {
7
+ state = { config: null, manifestPromise: null, identities: null };
8
+ globalSlot[STATE_KEY] = state;
9
+ }
10
+ return state;
11
+ };
12
+
13
+ // src/storage.ts
14
+ var SESSION_ID_KEY = "mfe-orchestrator.sessionId";
15
+ var DEVICE_ID_KEY = "mfe-orchestrator.deviceId";
16
+ var PROBE_KEY = "mfe-orchestrator.probe";
17
+ var inMemoryIds = /* @__PURE__ */ new Map();
18
+ var uuidFromRandomBytes = (bytes) => {
19
+ bytes[6] = bytes[6] & 15 | 64;
20
+ bytes[8] = bytes[8] & 63 | 128;
21
+ const hex = [];
22
+ for (const byte of bytes) {
23
+ hex.push(byte.toString(16).padStart(2, "0"));
24
+ }
25
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
26
+ };
27
+ var newId = () => {
28
+ const cryptoApi = globalThis.crypto;
29
+ try {
30
+ if (typeof cryptoApi?.randomUUID === "function") {
31
+ return cryptoApi.randomUUID();
32
+ }
33
+ if (typeof cryptoApi?.getRandomValues === "function") {
34
+ return uuidFromRandomBytes(cryptoApi.getRandomValues(new Uint8Array(16)));
35
+ }
36
+ } catch {
37
+ }
38
+ const bytes = new Uint8Array(16);
39
+ for (let index = 0; index < bytes.length; index++) {
40
+ bytes[index] = Math.floor(Math.random() * 256);
41
+ }
42
+ return uuidFromRandomBytes(bytes);
43
+ };
44
+ var openStorage = (kind) => {
45
+ try {
46
+ const storage = kind === "local" ? globalThis.localStorage : globalThis.sessionStorage;
47
+ if (!storage) {
48
+ return null;
49
+ }
50
+ storage.setItem(PROBE_KEY, "1");
51
+ storage.removeItem(PROBE_KEY);
52
+ return storage;
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
57
+ var persistentId = (kind, key) => {
58
+ const storage = openStorage(kind);
59
+ if (storage) {
60
+ try {
61
+ const stored = storage.getItem(key);
62
+ if (stored) {
63
+ return stored;
64
+ }
65
+ const created2 = newId();
66
+ storage.setItem(key, created2);
67
+ return created2;
68
+ } catch {
69
+ }
70
+ }
71
+ const remembered = inMemoryIds.get(key);
72
+ if (remembered) {
73
+ return remembered;
74
+ }
75
+ const created = newId();
76
+ inMemoryIds.set(key, created);
77
+ return created;
78
+ };
79
+ var readIdentities = () => ({
80
+ sessionId: persistentId("session", SESSION_ID_KEY),
81
+ deviceId: persistentId("local", DEVICE_ID_KEY)
82
+ });
83
+
84
+ // src/index.ts
85
+ var PREFIX = "[@mfe-orchestrator-hub/client]";
86
+ var NOT_CONFIGURED = `${PREFIX} call configure() first. Configure the client at the very top of the host entry point, before anything imports a remote:
87
+
88
+ import { configure } from "@mfe-orchestrator-hub/client"
89
+
90
+ configure({ backendUrl: "\u2026", projectId: "\u2026", environment: "\u2026" })`;
91
+ var withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
92
+ var resolveUserId = async (userId) => {
93
+ const value = typeof userId === "function" ? await userId() : userId;
94
+ return value === void 0 || value === null || value === "" ? void 0 : String(value);
95
+ };
96
+ var sameConfig = (left, right) => left.backendUrl === right.backendUrl && left.projectId === right.projectId && left.environment === right.environment && left.userId === right.userId;
97
+ var requireConfig = (config) => {
98
+ const missing = ["backendUrl", "projectId", "environment"].filter((key) => !config?.[key]);
99
+ if (missing.length > 0) {
100
+ throw new Error(`${PREFIX} configure() is missing required option${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}.`);
101
+ }
102
+ };
103
+ var manifestUrl = async (config) => {
104
+ const { sessionId, deviceId } = identities();
105
+ const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId });
106
+ const userId = await resolveUserId(config.userId);
107
+ if (userId) {
108
+ query.set("mfeUserId", userId);
109
+ }
110
+ return `${withoutTrailingSlash(config.backendUrl)}/serve/all/${encodeURIComponent(config.projectId)}/${encodeURIComponent(config.environment)}?${query.toString()}`;
111
+ };
112
+ var fetchManifest = async (config) => {
113
+ const url = await manifestUrl(config);
114
+ const response = await fetch(url, { headers: { accept: "application/json" } });
115
+ if (!response.ok) {
116
+ throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`);
117
+ }
118
+ const payload = await response.json();
119
+ return {
120
+ globalVariables: payload?.globalVariables ?? [],
121
+ microfrontends: payload?.microfrontends ?? []
122
+ };
123
+ };
124
+ var configure = (config) => {
125
+ requireConfig(config);
126
+ const state = getState();
127
+ const current = state.config;
128
+ if (current) {
129
+ if (!sameConfig(current, config)) {
130
+ console.warn(
131
+ `${PREFIX} configure() was called again with a different configuration. The first one is kept: ${JSON.stringify({ backendUrl: current.backendUrl, projectId: current.projectId, environment: current.environment })}.`
132
+ );
133
+ }
134
+ return;
135
+ }
136
+ state.config = { ...config };
137
+ };
138
+ var manifest = () => {
139
+ const state = getState();
140
+ const config = state.config;
141
+ if (!config) {
142
+ return Promise.reject(new Error(NOT_CONFIGURED));
143
+ }
144
+ if (!state.manifestPromise) {
145
+ const attempt = fetchManifest(config);
146
+ state.manifestPromise = attempt;
147
+ attempt.catch(() => {
148
+ if (state.manifestPromise === attempt) {
149
+ state.manifestPromise = null;
150
+ }
151
+ });
152
+ }
153
+ return state.manifestPromise;
154
+ };
155
+ var remoteUrl = async (slug) => {
156
+ const { microfrontends } = await manifest();
157
+ const found = microfrontends.find((microfrontend) => microfrontend.slug === slug);
158
+ if (!found) {
159
+ const available = microfrontends.map((microfrontend) => `"${microfrontend.slug}"`).join(", ");
160
+ const { environment } = getState().config ?? { environment: "?" };
161
+ throw new Error(`${PREFIX} unknown microfrontend slug "${slug}". Available in environment "${environment}": ${available || "none, this environment serves no microfrontend"}.`);
162
+ }
163
+ if (!found.url) {
164
+ throw new Error(`${PREFIX} the microfrontend "${slug}" is in the manifest but carries no url. Check its deployment in the console.`);
165
+ }
166
+ return found.url;
167
+ };
168
+ var globalVariables = async () => {
169
+ const { globalVariables: variables } = await manifest();
170
+ const result = {};
171
+ for (const variable of variables) {
172
+ if (variable && typeof variable.key === "string") {
173
+ result[variable.key] = variable.value;
174
+ }
175
+ }
176
+ return result;
177
+ };
178
+ var identities = () => {
179
+ const state = getState();
180
+ if (!state.identities) {
181
+ state.identities = readIdentities();
182
+ }
183
+ return state.identities;
184
+ };
185
+
186
+ export { configure, globalVariables, identities, manifest, remoteUrl };
187
+ //# sourceMappingURL=index.js.map
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/state.ts","../src/storage.ts","../src/index.ts"],"names":["created"],"mappings":"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","file":"index.js","sourcesContent":["import type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport interface OrchestratorState {\n config: OrchestratorConfig | null\n manifestPromise: Promise<Manifest> | null\n identities: Identities | null\n}\n\n/**\n * The state lives on `globalThis` under this key, not in a module scope variable.\n *\n * A host can end up with two copies of this package loaded at once: the app imports the ESM build\n * while something else pulls the CJS one, or the bundler config and the app resolve to different\n * chunks. Two module scopes would mean two configurations and two manifest requests, which is\n * exactly what the contract forbids. One well known global slot makes the singleton real.\n */\nexport const STATE_KEY = \"__mfe_orchestrator_client__\"\n\nconst globalSlot = globalThis as typeof globalThis & Record<typeof STATE_KEY, OrchestratorState | undefined>\n\nexport const getState = (): OrchestratorState => {\n let state = globalSlot[STATE_KEY]\n if (!state) {\n state = { config: null, manifestPromise: null, identities: null }\n globalSlot[STATE_KEY] = state\n }\n return state\n}\n","import type { Identities } from \"./types\"\n\nexport const SESSION_ID_KEY = \"mfe-orchestrator.sessionId\"\nexport const DEVICE_ID_KEY = \"mfe-orchestrator.deviceId\"\n\nconst PROBE_KEY = \"mfe-orchestrator.probe\"\n\ntype StorageKind = \"local\" | \"session\"\n\n/**\n * Ids minted while storage was unusable. They live for the lifetime of the page so that, even\n * without storage, every request of this page load carries one stable pair of ids.\n */\nconst inMemoryIds = new Map<string, string>()\n\nconst uuidFromRandomBytes = (bytes: Uint8Array): string => {\n bytes[6] = (bytes[6] & 0x0f) | 0x40\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex: string[] = []\n for (const byte of bytes) {\n hex.push(byte.toString(16).padStart(2, \"0\"))\n }\n return `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10, 16).join(\"\")}`\n}\n\n/**\n * A v4 UUID. `crypto.randomUUID` is the intended path; the two fallbacks only exist so that a page\n * served over plain http, or an exotic runtime, still gets an id instead of an exception.\n */\nexport const newId = (): string => {\n const cryptoApi = globalThis.crypto\n try {\n if (typeof cryptoApi?.randomUUID === \"function\") {\n return cryptoApi.randomUUID()\n }\n if (typeof cryptoApi?.getRandomValues === \"function\") {\n return uuidFromRandomBytes(cryptoApi.getRandomValues(new Uint8Array(16)))\n }\n } catch {\n // fall through to the non cryptographic fallback\n }\n const bytes = new Uint8Array(16)\n for (let index = 0; index < bytes.length; index++) {\n bytes[index] = Math.floor(Math.random() * 256)\n }\n return uuidFromRandomBytes(bytes)\n}\n\n/**\n * The requested storage, or `null` when it is missing or unusable.\n *\n * Both the property access and the write can throw: Safari in private mode exposes the object and\n * then refuses `setItem`, some hardened browsers throw a SecurityError on the access itself. The\n * write probe is the only reliable way to tell a usable storage from a decorative one.\n */\nconst openStorage = (kind: StorageKind): Storage | null => {\n try {\n const storage = kind === \"local\" ? globalThis.localStorage : globalThis.sessionStorage\n if (!storage) {\n return null\n }\n storage.setItem(PROBE_KEY, \"1\")\n storage.removeItem(PROBE_KEY)\n return storage\n } catch {\n return null\n }\n}\n\n/**\n * The id under `key`, read from storage and created on first use. Falls back to an id kept in\n * memory when storage cannot be used, so this never throws out of the SDK.\n */\nconst persistentId = (kind: StorageKind, key: string): string => {\n const storage = openStorage(kind)\n if (storage) {\n try {\n const stored = storage.getItem(key)\n if (stored) {\n return stored\n }\n const created = newId()\n storage.setItem(key, created)\n return created\n } catch {\n // storage passed the probe and failed anyway: keep going with an in memory id\n }\n }\n const remembered = inMemoryIds.get(key)\n if (remembered) {\n return remembered\n }\n const created = newId()\n inMemoryIds.set(key, created)\n return created\n}\n\n/** Reads, and creates when needed, the session and device ids. */\nexport const readIdentities = (): Identities => ({\n sessionId: persistentId(\"session\", SESSION_ID_KEY),\n deviceId: persistentId(\"local\", DEVICE_ID_KEY)\n})\n","import { getState } from \"./state\"\nimport { readIdentities } from \"./storage\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport type { GlobalVariable, Identities, Manifest, Microfrontend, OrchestratorConfig } from \"./types\"\n\nconst PREFIX = \"[@mfe-orchestrator-hub/client]\"\n\nconst NOT_CONFIGURED = `${PREFIX} call configure() first. Configure the client at the very top of the host entry point, before anything imports a remote:\n\n import { configure } from \"@mfe-orchestrator-hub/client\"\n\n configure({ backendUrl: \"…\", projectId: \"…\", environment: \"…\" })`\n\nconst withoutTrailingSlash = (value: string): string => value.replace(/\\/+$/, \"\")\n\nconst resolveUserId = async (userId: OrchestratorConfig[\"userId\"]): Promise<string | undefined> => {\n const value = typeof userId === \"function\" ? await userId() : userId\n return value === undefined || value === null || value === \"\" ? undefined : String(value)\n}\n\nconst sameConfig = (left: OrchestratorConfig, right: OrchestratorConfig): boolean =>\n left.backendUrl === right.backendUrl && left.projectId === right.projectId && left.environment === right.environment && left.userId === right.userId\n\nconst requireConfig = (config: OrchestratorConfig): void => {\n const missing = ([\"backendUrl\", \"projectId\", \"environment\"] as const).filter(key => !config?.[key])\n if (missing.length > 0) {\n throw new Error(`${PREFIX} configure() is missing required option${missing.length > 1 ? \"s\" : \"\"}: ${missing.join(\", \")}.`)\n }\n}\n\nconst manifestUrl = async (config: OrchestratorConfig): Promise<string> => {\n const { sessionId, deviceId } = identities()\n const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId })\n // Resolved as late as possible: the host may only know the user after its own auth round trip.\n const userId = await resolveUserId(config.userId)\n if (userId) {\n query.set(\"mfeUserId\", userId)\n }\n return `${withoutTrailingSlash(config.backendUrl)}/serve/all/${encodeURIComponent(config.projectId)}/${encodeURIComponent(config.environment)}?${query.toString()}`\n}\n\nconst fetchManifest = async (config: OrchestratorConfig): Promise<Manifest> => {\n const url = await manifestUrl(config)\n // No retry in v1: whatever fetch throws travels straight to the caller.\n const response = await fetch(url, { headers: { accept: \"application/json\" } })\n if (!response.ok) {\n throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`)\n }\n const payload = (await response.json()) as Partial<Manifest> | null\n return {\n globalVariables: payload?.globalVariables ?? [],\n microfrontends: payload?.microfrontends ?? []\n }\n}\n\n/**\n * Hands the client its configuration. Call it once, synchronously, before any remote is imported.\n *\n * Idempotent: calling it again with the same options is a no op, so a framework provider may safely\n * call it on every render. A second call with different options is ignored, with a warning, because\n * the manifest of this page load may already be in flight or resolved.\n */\nexport const configure = (config: OrchestratorConfig): void => {\n requireConfig(config)\n const state = getState()\n const current = state.config\n if (current) {\n if (!sameConfig(current, config)) {\n console.warn(\n `${PREFIX} configure() was called again with a different configuration. The first one is kept: ${JSON.stringify({ backendUrl: current.backendUrl, projectId: current.projectId, environment: current.environment })}.`\n )\n }\n return\n }\n state.config = { ...config }\n}\n\n/**\n * The whole manifest of the environment, fetched once per page load and memoized as a promise, so\n * that concurrent callers share the single request in flight.\n *\n * A successful result is memoized for good. A failed attempt is not: the memo is dropped when the\n * shared attempt settles as rejected, so the next caller starts a fresh request. That is not a retry\n * — nothing here loops or backs off — it only means one flaky lookup at boot does not condemn the\n * page to reject every `remoteUrl()` until a full reload.\n */\nexport const manifest = (): Promise<Manifest> => {\n const state = getState()\n const config = state.config\n if (!config) {\n // Not memoized: the host may still configure the client and try again.\n return Promise.reject(new Error(NOT_CONFIGURED))\n }\n if (!state.manifestPromise) {\n // Assigned before the first await inside fetchManifest, which is what makes N concurrent\n // callers collapse onto one request.\n const attempt = fetchManifest(config)\n state.manifestPromise = attempt\n // Cleared once, when the shared attempt settles, not in each caller's own catch: every\n // caller already waiting on this attempt still gets its rejection. The identity check keeps\n // a late failure from evicting a newer attempt.\n attempt.catch(() => {\n if (state.manifestPromise === attempt) {\n state.manifestPromise = null\n }\n })\n }\n return state.manifestPromise\n}\n\n/**\n * The ready to use, version pinned URL of a remote. Awaits the manifest internally.\n *\n * The returned URL is used verbatim: the host never parses the version out of it and never strips\n * the `_v/<version>/` segment, or a classic script would load its chunks off another version.\n */\nexport const remoteUrl = async (slug: string): Promise<string> => {\n const { microfrontends } = await manifest()\n const found = microfrontends.find(microfrontend => microfrontend.slug === slug)\n if (!found) {\n const available = microfrontends.map(microfrontend => `\"${microfrontend.slug}\"`).join(\", \")\n const { environment } = getState().config ?? { environment: \"?\" }\n throw new Error(`${PREFIX} unknown microfrontend slug \"${slug}\". Available in environment \"${environment}\": ${available || \"none, this environment serves no microfrontend\"}.`)\n }\n if (!found.url) {\n throw new Error(`${PREFIX} the microfrontend \"${slug}\" is in the manifest but carries no url. Check its deployment in the console.`)\n }\n return found.url\n}\n\n/** The global variables of the environment, as a plain object. */\nexport const globalVariables = async (): Promise<Record<string, string>> => {\n const { globalVariables: variables } = await manifest()\n const result: Record<string, string> = {}\n for (const variable of variables) {\n if (variable && typeof variable.key === \"string\") {\n result[variable.key] = variable.value\n }\n }\n return result\n}\n\n/**\n * The session and device ids of this page, exposed for telemetry and debugging. Created on first\n * use and stable for the lifetime of the page, whether or not storage is available.\n */\nexport const identities = (): Identities => {\n const state = getState()\n if (!state.identities) {\n state.identities = readIdentities()\n }\n return state.identities\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mfe-orchestrator-hub/client",
3
+ "version": "0.1.0",
4
+ "description": "Framework agnostic client for MFE Orchestrator: resolves version pinned microfrontend URLs from the console manifest",
5
+ "license": "MIT",
6
+ "author": "Lorenzo De Francesco",
7
+ "type": "module",
8
+ "packageManager": "pnpm@10.19.0",
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "keywords": [
13
+ "mfe",
14
+ "microfrontend",
15
+ "module-federation",
16
+ "orchestrator",
17
+ "vite",
18
+ "webpack"
19
+ ],
20
+ "homepage": "https://github.com/mfe-orchestrator/client-core#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/mfe-orchestrator/client-core/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/mfe-orchestrator/client-core.git"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "import": {
40
+ "types": "./dist/index.d.ts",
41
+ "default": "./dist/index.js"
42
+ },
43
+ "require": {
44
+ "types": "./dist/index.d.cts",
45
+ "default": "./dist/index.cjs"
46
+ }
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "sideEffects": false,
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit",
57
+ "lint": "biome lint --write ./src ./test",
58
+ "format": "biome format --write ./src ./test",
59
+ "prepublishOnly": "pnpm build"
60
+ },
61
+ "devDependencies": {
62
+ "@biomejs/biome": "^2.5.7",
63
+ "tsup": "^8.5.0",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^3.2.4"
66
+ }
67
+ }