@mfe-orchestrator-hub/client 0.1.0 → 0.1.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/README.md +106 -2
- package/dist/index.cjs +198 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -11
- package/dist/index.d.ts +33 -11
- package/dist/index.js +198 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,10 +25,34 @@ Call `configure()` at the very top of the host entry point, before anything impo
|
|
|
25
25
|
```ts
|
|
26
26
|
import { configure } from "@mfe-orchestrator-hub/client"
|
|
27
27
|
|
|
28
|
+
configure({
|
|
29
|
+
backendUrl: import.meta.env.VITE_MFE_BACKEND_URL,
|
|
30
|
+
projectId: import.meta.env.VITE_MFE_PROJECT_ID
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`backendUrl` and `projectId` are the only required options.
|
|
35
|
+
|
|
36
|
+
### Which environment am I?
|
|
37
|
+
|
|
38
|
+
`environment` is optional. Leave it out and the client calls the *auto* routes, where the backend
|
|
39
|
+
resolves the environment from the domain the request comes from — the domains you declared for each
|
|
40
|
+
environment in the console. One build then serves staging and production without an environment
|
|
41
|
+
variable to set per deployment.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// staging.example.com → the STAGING environment, production.example.com → PROD, decided by the backend
|
|
45
|
+
configure({ backendUrl: "…", projectId: "…" })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pass it explicitly when the host already knows which environment it belongs to, or when one domain
|
|
49
|
+
has to serve several of them:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
28
52
|
configure({
|
|
29
53
|
backendUrl: import.meta.env.VITE_MFE_BACKEND_URL,
|
|
30
54
|
projectId: import.meta.env.VITE_MFE_PROJECT_ID,
|
|
31
|
-
environment: import.meta.env.VITE_MFE_ENVIRONMENT
|
|
55
|
+
environment: import.meta.env.VITE_MFE_ENVIRONMENT // ex. "DEV"
|
|
32
56
|
})
|
|
33
57
|
```
|
|
34
58
|
|
|
@@ -68,13 +92,42 @@ new ModuleFederationPlugin({
|
|
|
68
92
|
})
|
|
69
93
|
```
|
|
70
94
|
|
|
95
|
+
### Where `backendUrl` and `projectId` come from
|
|
96
|
+
|
|
97
|
+
The remotes above resolve themselves through this client, so the bundle has to carry the two
|
|
98
|
+
required options. The console writes them into the bundler config itself rather than into a `.env`:
|
|
99
|
+
the config is the file it commits to your repository, so a fresh clone builds a bundle that already
|
|
100
|
+
knows which console to ask and which project to ask about, with no variable left to set.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// vite.config.js
|
|
104
|
+
define: {
|
|
105
|
+
"import.meta.env.VITE_MFE_BACKEND_URL": JSON.stringify("https://console.mfe-orchestrator.dev/api"),
|
|
106
|
+
"import.meta.env.VITE_MFE_PROJECT_ID": JSON.stringify("6f1b2c3d4e5f6a7b8c9d0e1f")
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// webpack.config.js
|
|
112
|
+
new webpack.DefinePlugin({
|
|
113
|
+
"process.env.MFE_BACKEND_URL": JSON.stringify("https://console.mfe-orchestrator.dev/api"),
|
|
114
|
+
"process.env.MFE_PROJECT_ID": JSON.stringify("6f1b2c3d4e5f6a7b8c9d0e1f"),
|
|
115
|
+
// Verbatim text: the bare `undefined` is what an unset environment looks like, and the backend
|
|
116
|
+
// then resolves it from the domain. `process` does not exist in a browser, so leaving the key
|
|
117
|
+
// out would throw instead of reading as undefined.
|
|
118
|
+
"process.env.MFE_ENVIRONMENT": "undefined"
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`configure()` keeps reading the same names either way — a `.env` still works if you prefer one.
|
|
123
|
+
|
|
71
124
|
## API
|
|
72
125
|
|
|
73
126
|
```ts
|
|
74
127
|
export interface OrchestratorConfig {
|
|
75
128
|
backendUrl: string // ex. "https://console.mfe-orchestrator.dev/api"
|
|
76
129
|
projectId: string
|
|
77
|
-
environment
|
|
130
|
+
environment?: string // environment slug, ex. "DEV". Omitted: resolved by the backend from the domain
|
|
78
131
|
userId?: string | (() => string | undefined | Promise<string | undefined>)
|
|
79
132
|
}
|
|
80
133
|
|
|
@@ -136,6 +189,14 @@ GET {backendUrl}/serve/all/{projectId}/{environment}
|
|
|
136
189
|
?mfeSessionId=<uuid>&mfeDeviceId=<uuid>&mfeUserId=<optional>
|
|
137
190
|
```
|
|
138
191
|
|
|
192
|
+
and, when no `environment` is configured, the same request on the auto route, where the backend
|
|
193
|
+
resolves the environment from the domain the request comes from:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
GET {backendUrl}/serve/all/auto/{projectId}
|
|
197
|
+
?mfeSessionId=<uuid>&mfeDeviceId=<uuid>&mfeUserId=<optional>
|
|
198
|
+
```
|
|
199
|
+
|
|
139
200
|
### Why identities travel in the query string
|
|
140
201
|
|
|
141
202
|
Microfrontends are loaded with a cross-site `import()`. Module scripts are fetched with a fixed
|
|
@@ -175,6 +236,49 @@ This is not a retry: nothing here loops, schedules or backs off. It only means t
|
|
|
175
236
|
or one 502 during a console redeploy does not condemn the page to reject every `remoteUrl()` until a
|
|
176
237
|
full reload. Recovering is the host app's call, by asking again.
|
|
177
238
|
|
|
239
|
+
## When the configuration is wrong
|
|
240
|
+
|
|
241
|
+
`backendUrl` and `projectId` almost always come from build time variables, so the usual failure is
|
|
242
|
+
not a wrong value but a value that never reached the bundle. The client says so out loud instead of
|
|
243
|
+
firing a request at `undefined`:
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
[@mfe-orchestrator-hub/client] configure() cannot start the client: projectId is not usable.
|
|
247
|
+
|
|
248
|
+
• projectId is missing.
|
|
249
|
+
→ It is the id of the project in the console: open the project and copy it from its page, ex. "6f1b2c3d4e5f6a7b8c9d0e1f".
|
|
250
|
+
→ Nothing arrived here at all, which is what a value that never reached the bundle looks like: check VITE_MFE_PROJECT_ID (Vite) or MFE_PROJECT_ID (webpack) in the build that produced this bundle. The generated configs carry it in the "define" block of vite.config, or in the DefinePlugin of webpack.config, so look there first; declared only in a .env file the build never reads, it looks exactly like this.
|
|
251
|
+
|
|
252
|
+
Configure the client like this:
|
|
253
|
+
|
|
254
|
+
import { configure } from "@mfe-orchestrator-hub/client"
|
|
255
|
+
|
|
256
|
+
configure({
|
|
257
|
+
backendUrl: "https://console.mfe-orchestrator.dev/api",
|
|
258
|
+
projectId: "6f1b2c3d4e5f6a7b8c9d0e1f"
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
Call it once, at the very top of the host entry point, before anything imports a remote.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`configure()` throws, before anything is stored, when an option is:
|
|
265
|
+
|
|
266
|
+
| What arrived | What the message says |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `undefined`, `null`, `""`, `" "` | missing or empty, plus the environment variable to check |
|
|
269
|
+
| `projectID`, `backend_url`, `apiUrl`… | the key the client ignores, and how the option is spelled |
|
|
270
|
+
| `"undefined"`, `"…"`, `"<your-project-id>"` | a placeholder that was never filled in |
|
|
271
|
+
| a number, an object, an array | the type it received, instead of a string |
|
|
272
|
+
| `"console.test/api"` | not a URL: the two forms that do work, with and without protocol |
|
|
273
|
+
| `"localhost:3000/api"` | a bare `host:port` is read as a scheme, write `http://localhost:3000/api` |
|
|
274
|
+
| `" p1 "` as `projectId` | padded with whitespace, which would be percent encoded into the URL |
|
|
275
|
+
|
|
276
|
+
Reading anything before `configure()` ran rejects with the same guidance, naming the call that was
|
|
277
|
+
made too early. A `404` on the manifest route says which two things the backend could not find, the
|
|
278
|
+
project id or the environment, since it answers the same status for both. Every message shows the
|
|
279
|
+
snippet of the package the host actually uses: `<OrchestratorProvider>` for React,
|
|
280
|
+
`createOrchestrator()` for Vue, `provideOrchestrator()` for Angular.
|
|
281
|
+
|
|
178
282
|
## Not in v1
|
|
179
283
|
|
|
180
284
|
No automatic retries, no offline caching, no service worker, and nothing that reads or writes
|
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,156 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var PREFIX = "[@mfe-orchestrator-hub/client]";
|
|
5
|
+
var EXAMPLE_BACKEND_URL = "https://console.mfe-orchestrator.dev/api";
|
|
6
|
+
var EXAMPLE_PROJECT_ID = "6f1b2c3d4e5f6a7b8c9d0e1f";
|
|
7
|
+
var SNIPPETS = {
|
|
8
|
+
core: `import { configure } from "@mfe-orchestrator-hub/client"
|
|
9
|
+
|
|
10
|
+
configure({
|
|
11
|
+
backendUrl: "${EXAMPLE_BACKEND_URL}",
|
|
12
|
+
projectId: "${EXAMPLE_PROJECT_ID}"
|
|
13
|
+
})`,
|
|
14
|
+
react: `import { OrchestratorProvider } from "@mfe-orchestrator-hub/client-react"
|
|
15
|
+
|
|
16
|
+
<OrchestratorProvider config={{ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" }}>
|
|
17
|
+
<App />
|
|
18
|
+
</OrchestratorProvider>`,
|
|
19
|
+
vue: `import { createOrchestrator } from "@mfe-orchestrator-hub/client-vue"
|
|
20
|
+
|
|
21
|
+
app.use(createOrchestrator({ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" }))`,
|
|
22
|
+
angular: `import { provideOrchestrator } from "@mfe-orchestrator-hub/client-angular"
|
|
23
|
+
|
|
24
|
+
bootstrapApplication(AppComponent, {
|
|
25
|
+
providers: [provideOrchestrator({ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" })]
|
|
26
|
+
})`
|
|
27
|
+
};
|
|
28
|
+
var WHERE = {
|
|
29
|
+
core: "Call it once, at the very top of the host entry point, before anything imports a remote.",
|
|
30
|
+
react: "Render it above every component that resolves a remote. Better still, call configure() at the very top of the entry point: a bundler may import a remote before React mounts.",
|
|
31
|
+
vue: "Install the plugin before mounting the app. Better still, call configure() at the very top of the entry point: a bundler may import a remote before the app is created.",
|
|
32
|
+
angular: "Pass it to bootstrapApplication(). Better still, call configure() at the very top of main.ts: a lazily loaded remote must find the client already configured."
|
|
33
|
+
};
|
|
34
|
+
var WHAT_IT_IS = {
|
|
35
|
+
backendUrl: `It is the base URL of the console API, protocol included, ex. "${EXAMPLE_BACKEND_URL}". A path on the same origin as the host, ex. "/api", works too.`,
|
|
36
|
+
projectId: `It is the id of the project in the console: open the project and copy it from its page, ex. "${EXAMPLE_PROJECT_ID}".`
|
|
37
|
+
};
|
|
38
|
+
var BUILD_VARIABLE = {
|
|
39
|
+
backendUrl: { vite: "VITE_MFE_BACKEND_URL", webpack: "MFE_BACKEND_URL" },
|
|
40
|
+
projectId: { vite: "VITE_MFE_PROJECT_ID", webpack: "MFE_PROJECT_ID" }
|
|
41
|
+
};
|
|
42
|
+
var missingEnvHint = (option) => `Nothing arrived here at all, which is what a value that never reached the bundle looks like: check ${BUILD_VARIABLE[option].vite} (Vite) or ${BUILD_VARIABLE[option].webpack} (webpack) in the build that produced this bundle. The generated configs carry it in the "define" block of vite.config, or in the DefinePlugin of webpack.config, so look there first; declared only in a .env file the build never reads, it looks exactly like this.`;
|
|
43
|
+
var PLACEHOLDER = /^(…|\.{3}|undefined|null|nan|<[^>]*>|\{\{[^}]*\}\}|\$\{[^}]*\}|your[-_ ]?[a-z]*|x{3,}|change[-_ ]?me|todo|tbd)$/i;
|
|
44
|
+
var quoted = (value) => JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}\u2026` : value);
|
|
45
|
+
var described = (value) => {
|
|
46
|
+
if (value === null) {
|
|
47
|
+
return "null";
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return "an array";
|
|
51
|
+
}
|
|
52
|
+
const type = typeof value;
|
|
53
|
+
return type === "object" || type === "function" ? `a ${type}` : `a ${type} (${String(value)})`;
|
|
54
|
+
};
|
|
55
|
+
var normalized = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
56
|
+
var ALIASES = {
|
|
57
|
+
backendUrl: ["backendurl", "backendbaseurl", "apiurl", "baseurl", "consoleurl", "url", "backend"],
|
|
58
|
+
projectId: ["projectid", "project", "idproject", "projectuuid", "projectkey"]
|
|
59
|
+
};
|
|
60
|
+
var confusableKey = (option, config) => {
|
|
61
|
+
if (typeof config !== "object" || config === null) {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
const candidates = ALIASES[option];
|
|
65
|
+
return Object.keys(config).find((key) => key !== option && candidates.includes(normalized(key)));
|
|
66
|
+
};
|
|
67
|
+
var problem = (option, what, ...hints) => ({
|
|
68
|
+
option,
|
|
69
|
+
what,
|
|
70
|
+
hints: [WHAT_IT_IS[option], ...hints].filter((hint) => Boolean(hint))
|
|
71
|
+
});
|
|
72
|
+
var diagnoseBackendUrl = (value) => {
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
if (trimmed.startsWith("/")) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(trimmed);
|
|
80
|
+
} catch {
|
|
81
|
+
return problem(
|
|
82
|
+
"backendUrl",
|
|
83
|
+
`is ${quoted(value)}, which is not a URL`,
|
|
84
|
+
trimmed.includes(".") || trimmed.includes(":") ? `It looks like a host without a protocol: write "https://${trimmed}" instead, or "/${trimmed.replace(/^\/+/, "")}" if the console API answers on the same origin as this page.` : void 0
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
88
|
+
return problem(
|
|
89
|
+
"backendUrl",
|
|
90
|
+
`is ${quoted(value)}, whose "${parsed.protocol.replace(":", "")}" scheme is neither http nor https`,
|
|
91
|
+
'A bare "host:port" is read as a scheme: write "http://localhost:3000/api", not "localhost:3000/api".'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
var diagnose = (option, config) => {
|
|
97
|
+
const value = config?.[option];
|
|
98
|
+
const typo = confusableKey(option, config);
|
|
99
|
+
const typoHint = typo ? `The configuration carries "${typo}", which the client ignores. The option is spelled "${option}".` : void 0;
|
|
100
|
+
if (value === void 0 || value === null) {
|
|
101
|
+
return problem(option, value === null ? "is null" : "is missing", typoHint, typo ? void 0 : missingEnvHint(option));
|
|
102
|
+
}
|
|
103
|
+
if (typeof value !== "string") {
|
|
104
|
+
return problem(option, `is ${described(value)}, not a string`, typoHint);
|
|
105
|
+
}
|
|
106
|
+
if (value.trim() === "") {
|
|
107
|
+
return problem(option, value === "" ? "is an empty string" : "holds nothing but whitespace", typoHint, missingEnvHint(option));
|
|
108
|
+
}
|
|
109
|
+
if (PLACEHOLDER.test(value.trim())) {
|
|
110
|
+
const looksLikeAnUnsetVariable = /^(undefined|null|nan)$/i.test(value.trim());
|
|
111
|
+
return problem(option, `is still the placeholder ${quoted(value)}`, typoHint, looksLikeAnUnsetVariable ? missingEnvHint(option) : void 0);
|
|
112
|
+
}
|
|
113
|
+
if (option === "projectId" && value.trim() !== value) {
|
|
114
|
+
return problem(
|
|
115
|
+
"projectId",
|
|
116
|
+
`is ${quoted(value)}, padded with whitespace`,
|
|
117
|
+
"Trim the value: it is percent encoded into the manifest URL, so the spaces would reach the backend as part of the id."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return option === "backendUrl" ? diagnoseBackendUrl(value) : null;
|
|
121
|
+
};
|
|
122
|
+
var diagnoseConfig = (config) => ["backendUrl", "projectId"].map((option) => diagnose(option, config)).filter((found) => found !== null);
|
|
123
|
+
var bulleted = (problems) => problems.map(({ option, what, hints }) => [` \u2022 ${option} ${what}.`, ...hints.map((hint) => ` \u2192 ${hint}`)].join("\n")).join("\n");
|
|
124
|
+
var indented = (block) => block.split("\n").map((line) => line ? ` ${line}` : line).join("\n");
|
|
125
|
+
var howToConfigure = (integration) => `Configure the client like this:
|
|
126
|
+
|
|
127
|
+
${indented(SNIPPETS[integration])}
|
|
128
|
+
|
|
129
|
+
${WHERE[integration]}`;
|
|
130
|
+
var listed = (options) => options.length > 1 ? `${options.slice(0, -1).join(", ")} and ${options.at(-1)}` : String(options[0]);
|
|
131
|
+
var invalidConfigMessage = (problems, integration) => {
|
|
132
|
+
const options = problems.map(({ option }) => option);
|
|
133
|
+
const headline = `${PREFIX} configure() cannot start the client: ${listed(options)} ${options.length > 1 ? "are" : "is"} not usable.`;
|
|
134
|
+
return `${headline}
|
|
135
|
+
|
|
136
|
+
${bulleted(problems)}
|
|
137
|
+
|
|
138
|
+
${howToConfigure(integration)}`;
|
|
139
|
+
};
|
|
140
|
+
var noConfigMessage = (config, integration) => `${PREFIX} configure() was called with ${config === void 0 ? "no argument" : described(config)} instead of a configuration object. It needs at least backendUrl and projectId.
|
|
141
|
+
|
|
142
|
+
${howToConfigure(integration)}`;
|
|
143
|
+
var notConfiguredMessage = (caller, integration) => `${PREFIX} the client is not configured: ${caller} was called before configure() ever ran, so neither backendUrl nor projectId is known and no manifest can be requested.
|
|
144
|
+
|
|
145
|
+
${howToConfigure(integration)}`;
|
|
146
|
+
|
|
3
147
|
// src/state.ts
|
|
4
148
|
var STATE_KEY = "__mfe_orchestrator_client__";
|
|
5
149
|
var globalSlot = globalThis;
|
|
6
150
|
var getState = () => {
|
|
7
151
|
let state = globalSlot[STATE_KEY];
|
|
8
152
|
if (!state) {
|
|
9
|
-
state = { config: null, manifestPromise: null, identities: null };
|
|
153
|
+
state = { config: null, manifestPromise: null, identities: null, integration: "core" };
|
|
10
154
|
globalSlot[STATE_KEY] = state;
|
|
11
155
|
}
|
|
12
156
|
return state;
|
|
@@ -84,24 +228,37 @@ var readIdentities = () => ({
|
|
|
84
228
|
});
|
|
85
229
|
|
|
86
230
|
// 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
231
|
var withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
94
232
|
var resolveUserId = async (userId) => {
|
|
95
233
|
const value = typeof userId === "function" ? await userId() : userId;
|
|
96
234
|
return value === void 0 || value === null || value === "" ? void 0 : String(value);
|
|
97
235
|
};
|
|
98
236
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
237
|
+
var requireConfig = (config, integration) => {
|
|
238
|
+
if (typeof config !== "object" || config === null) {
|
|
239
|
+
throw new Error(noConfigMessage(config, integration));
|
|
240
|
+
}
|
|
241
|
+
const problems = diagnoseConfig(config);
|
|
242
|
+
if (problems.length > 0) {
|
|
243
|
+
throw new Error(invalidConfigMessage(problems, integration));
|
|
244
|
+
}
|
|
245
|
+
if (config.environment !== void 0 && config.environment !== null && typeof config.environment !== "string") {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`${PREFIX} configure() received environment as a ${typeof config.environment}, not a string. Pass the environment slug as it appears in the console, ex. "DEV", or leave it out to let the backend resolve it from the domain of this page.`
|
|
248
|
+
);
|
|
103
249
|
}
|
|
104
250
|
};
|
|
251
|
+
var warnAboutConfig = (config) => {
|
|
252
|
+
if (typeof config.environment === "string" && config.environment.trim() === "") {
|
|
253
|
+
console.warn(
|
|
254
|
+
`${PREFIX} configure() received an empty environment, so the client falls back to the auto routes and the backend resolves the environment from the domain of this page. Remove the option if that is what you want, or check the variable that feeds it.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var manifestPath = (config) => {
|
|
259
|
+
const projectId = encodeURIComponent(config.projectId);
|
|
260
|
+
return config.environment ? `/serve/all/${projectId}/${encodeURIComponent(config.environment)}` : `/serve/all/auto/${projectId}`;
|
|
261
|
+
};
|
|
105
262
|
var manifestUrl = async (config) => {
|
|
106
263
|
const { sessionId, deviceId } = identities();
|
|
107
264
|
const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId });
|
|
@@ -109,13 +266,27 @@ var manifestUrl = async (config) => {
|
|
|
109
266
|
if (userId) {
|
|
110
267
|
query.set("mfeUserId", userId);
|
|
111
268
|
}
|
|
112
|
-
return `${withoutTrailingSlash(config.backendUrl)}
|
|
269
|
+
return `${withoutTrailingSlash(config.backendUrl)}${manifestPath(config)}?${query.toString()}`;
|
|
270
|
+
};
|
|
271
|
+
var statusHint = (status, config) => {
|
|
272
|
+
if (status === 404) {
|
|
273
|
+
const environment = config.environment ? `the environment "${config.environment}" does not exist in it` : "no environment of that project declares the domain this page is served on, which is what the auto route resolves on";
|
|
274
|
+
return `
|
|
275
|
+
|
|
276
|
+
A 404 on this route means the backend did not find what the URL names: either the project id "${config.projectId}" is not a project of this console, or ${environment}. Check both in the console, and check that backendUrl points at the console API of the same instance.`;
|
|
277
|
+
}
|
|
278
|
+
if (status === 401 || status === 403) {
|
|
279
|
+
return `
|
|
280
|
+
|
|
281
|
+
The console refused the request. Check that the project id "${config.projectId}" belongs to the instance backendUrl points at, and that this domain is allowed to read its manifest.`;
|
|
282
|
+
}
|
|
283
|
+
return "";
|
|
113
284
|
};
|
|
114
285
|
var fetchManifest = async (config) => {
|
|
115
286
|
const url = await manifestUrl(config);
|
|
116
287
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
117
288
|
if (!response.ok) {
|
|
118
|
-
throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`);
|
|
289
|
+
throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}${statusHint(response.status, config)}`);
|
|
119
290
|
}
|
|
120
291
|
const payload = await response.json();
|
|
121
292
|
return {
|
|
@@ -124,8 +295,8 @@ var fetchManifest = async (config) => {
|
|
|
124
295
|
};
|
|
125
296
|
};
|
|
126
297
|
var configure = (config) => {
|
|
127
|
-
requireConfig(config);
|
|
128
298
|
const state = getState();
|
|
299
|
+
requireConfig(config, state.integration);
|
|
129
300
|
const current = state.config;
|
|
130
301
|
if (current) {
|
|
131
302
|
if (!sameConfig(current, config)) {
|
|
@@ -136,12 +307,16 @@ var configure = (config) => {
|
|
|
136
307
|
return;
|
|
137
308
|
}
|
|
138
309
|
state.config = { ...config };
|
|
310
|
+
warnAboutConfig(config);
|
|
311
|
+
};
|
|
312
|
+
var registerIntegration = (integration) => {
|
|
313
|
+
getState().integration = integration;
|
|
139
314
|
};
|
|
140
|
-
var
|
|
315
|
+
var loadManifest = (caller) => {
|
|
141
316
|
const state = getState();
|
|
142
317
|
const config = state.config;
|
|
143
318
|
if (!config) {
|
|
144
|
-
return Promise.reject(new Error(
|
|
319
|
+
return Promise.reject(new Error(notConfiguredMessage(caller, state.integration)));
|
|
145
320
|
}
|
|
146
321
|
if (!state.manifestPromise) {
|
|
147
322
|
const attempt = fetchManifest(config);
|
|
@@ -154,13 +329,15 @@ var manifest = () => {
|
|
|
154
329
|
}
|
|
155
330
|
return state.manifestPromise;
|
|
156
331
|
};
|
|
332
|
+
var manifest = () => loadManifest("manifest()");
|
|
157
333
|
var remoteUrl = async (slug) => {
|
|
158
|
-
const { microfrontends } = await
|
|
334
|
+
const { microfrontends } = await loadManifest(`remoteUrl("${slug}")`);
|
|
159
335
|
const found = microfrontends.find((microfrontend) => microfrontend.slug === slug);
|
|
160
336
|
if (!found) {
|
|
161
337
|
const available = microfrontends.map((microfrontend) => `"${microfrontend.slug}"`).join(", ");
|
|
162
|
-
const
|
|
163
|
-
|
|
338
|
+
const environment = getState().config?.environment;
|
|
339
|
+
const where = environment ? `environment "${environment}"` : "the environment resolved from this domain";
|
|
340
|
+
throw new Error(`${PREFIX} unknown microfrontend slug "${slug}". Available in ${where}: ${available || "none, this environment serves no microfrontend"}.`);
|
|
164
341
|
}
|
|
165
342
|
if (!found.url) {
|
|
166
343
|
throw new Error(`${PREFIX} the microfrontend "${slug}" is in the manifest but carries no url. Check its deployment in the console.`);
|
|
@@ -168,7 +345,7 @@ var remoteUrl = async (slug) => {
|
|
|
168
345
|
return found.url;
|
|
169
346
|
};
|
|
170
347
|
var globalVariables = async () => {
|
|
171
|
-
const { globalVariables: variables } = await
|
|
348
|
+
const { globalVariables: variables } = await loadManifest("globalVariables()");
|
|
172
349
|
const result = {};
|
|
173
350
|
for (const variable of variables) {
|
|
174
351
|
if (variable && typeof variable.key === "string") {
|
|
@@ -189,6 +366,7 @@ exports.configure = configure;
|
|
|
189
366
|
exports.globalVariables = globalVariables;
|
|
190
367
|
exports.identities = identities;
|
|
191
368
|
exports.manifest = manifest;
|
|
369
|
+
exports.registerIntegration = registerIntegration;
|
|
192
370
|
exports.remoteUrl = remoteUrl;
|
|
193
371
|
//# sourceMappingURL=index.cjs.map
|
|
194
372
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/state.ts","../src/storage.ts","../src/index.ts"],"names":["created"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Everything the client says when its configuration is wrong or absent.\n *\n * The two required options almost always arrive from build time environment variables, so the\n * failure the host sees is not \"a value is invalid\" but \"nothing reached the bundle\". Messages here\n * are written for that moment: they name the option, show what actually arrived, say where the real\n * value comes from, and print the exact call to write, in the flavour of the package the host uses.\n */\n\nexport const PREFIX = \"[@mfe-orchestrator-hub/client]\"\n\n/** Which package the host went through. Only decides which snippet an error prints. */\nexport type Integration = \"core\" | \"react\" | \"vue\" | \"angular\"\n\n/** The two options without which nothing can be requested. */\nexport type RequiredOption = \"backendUrl\" | \"projectId\"\n\nconst EXAMPLE_BACKEND_URL = \"https://console.mfe-orchestrator.dev/api\"\nconst EXAMPLE_PROJECT_ID = \"6f1b2c3d4e5f6a7b8c9d0e1f\"\n\nconst SNIPPETS: Record<Integration, string> = {\n core: `import { configure } from \"@mfe-orchestrator-hub/client\"\n\nconfigure({\n backendUrl: \"${EXAMPLE_BACKEND_URL}\",\n projectId: \"${EXAMPLE_PROJECT_ID}\"\n})`,\n react: `import { OrchestratorProvider } from \"@mfe-orchestrator-hub/client-react\"\n\n<OrchestratorProvider config={{ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" }}>\n <App />\n</OrchestratorProvider>`,\n vue: `import { createOrchestrator } from \"@mfe-orchestrator-hub/client-vue\"\n\napp.use(createOrchestrator({ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" }))`,\n angular: `import { provideOrchestrator } from \"@mfe-orchestrator-hub/client-angular\"\n\nbootstrapApplication(AppComponent, {\n providers: [provideOrchestrator({ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" })]\n})`\n}\n\nconst WHERE: Record<Integration, string> = {\n core: \"Call it once, at the very top of the host entry point, before anything imports a remote.\",\n react: \"Render it above every component that resolves a remote. Better still, call configure() at the very top of the entry point: a bundler may import a remote before React mounts.\",\n vue: \"Install the plugin before mounting the app. Better still, call configure() at the very top of the entry point: a bundler may import a remote before the app is created.\",\n angular: \"Pass it to bootstrapApplication(). Better still, call configure() at the very top of main.ts: a lazily loaded remote must find the client already configured.\"\n}\n\nconst WHAT_IT_IS: Record<RequiredOption, string> = {\n backendUrl: `It is the base URL of the console API, protocol included, ex. \"${EXAMPLE_BACKEND_URL}\". A path on the same origin as the host, ex. \"/api\", works too.`,\n projectId: `It is the id of the project in the console: open the project and copy it from its page, ex. \"${EXAMPLE_PROJECT_ID}\".`\n}\n\n/** The name the value travels under from the bundler config into the bundle, per toolchain. */\nconst BUILD_VARIABLE: Record<RequiredOption, { vite: string; webpack: string }> = {\n backendUrl: { vite: \"VITE_MFE_BACKEND_URL\", webpack: \"MFE_BACKEND_URL\" },\n projectId: { vite: \"VITE_MFE_PROJECT_ID\", webpack: \"MFE_PROJECT_ID\" }\n}\n\n/**\n * What an option that was never substituted into the bundle looks like.\n *\n * The bundler config is named before the .env file on purpose: it is where the console writes both\n * values when it generates the config, so it is where they are missing from when they are missing.\n */\nconst missingEnvHint = (option: RequiredOption): string =>\n `Nothing arrived here at all, which is what a value that never reached the bundle looks like: check ${BUILD_VARIABLE[option].vite} (Vite) or ${BUILD_VARIABLE[option].webpack} (webpack) in the build that produced this bundle. The generated configs carry it in the \"define\" block of vite.config, or in the DefinePlugin of webpack.config, so look there first; declared only in a .env file the build never reads, it looks exactly like this.`\n\n/** Values that mean \"the template was never filled in\". */\nconst PLACEHOLDER = /^(…|\\.{3}|undefined|null|nan|<[^>]*>|\\{\\{[^}]*\\}\\}|\\$\\{[^}]*\\}|your[-_ ]?[a-z]*|x{3,}|change[-_ ]?me|todo|tbd)$/i\n\n/** Long values are cut: an error message is not the place to print a whole JWT. */\nconst quoted = (value: string): string => JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}…` : value)\n\nconst described = (value: unknown): string => {\n if (value === null) {\n return \"null\"\n }\n if (Array.isArray(value)) {\n return \"an array\"\n }\n const type = typeof value\n return type === \"object\" || type === \"function\" ? `a ${type}` : `a ${type} (${String(value)})`\n}\n\n/** Same key modulo case and separators: projectID, project_id, PROJECT-ID all collapse onto projectid. */\nconst normalized = (key: string): string => key.toLowerCase().replace(/[^a-z0-9]/g, \"\")\n\nconst ALIASES: Record<RequiredOption, string[]> = {\n backendUrl: [\"backendurl\", \"backendbaseurl\", \"apiurl\", \"baseurl\", \"consoleurl\", \"url\", \"backend\"],\n projectId: [\"projectid\", \"project\", \"idproject\", \"projectuuid\", \"projectkey\"]\n}\n\n/**\n * The key the host actually wrote, when it looks like a misspelling of the one that is missing.\n * `projectID` is by far the most common: it is silently ignored, so without this the message would\n * claim the option is absent while the developer stares at a line that clearly sets it.\n */\nconst confusableKey = (option: RequiredOption, config: unknown): string | undefined => {\n if (typeof config !== \"object\" || config === null) {\n return undefined\n }\n const candidates = ALIASES[option]\n return Object.keys(config).find(key => key !== option && candidates.includes(normalized(key)))\n}\n\n/** One thing that is wrong with one option, already spelled out. */\nexport interface ConfigProblem {\n option: RequiredOption\n /** Predicate completing \"<option> …\", ex. `is missing`. */\n what: string\n /** Extra lines, printed under the option, that say how to fix this particular case. */\n hints: string[]\n}\n\nconst problem = (option: RequiredOption, what: string, ...hints: (string | undefined)[]): ConfigProblem => ({\n option,\n what,\n hints: [WHAT_IT_IS[option], ...hints].filter((hint): hint is string => Boolean(hint))\n})\n\n/**\n * A backendUrl the client can actually build a request on: an absolute http(s) URL, or a path on the\n * host's own origin, which `fetch` resolves against the page.\n */\nconst diagnoseBackendUrl = (value: string): ConfigProblem | null => {\n const trimmed = value.trim()\n if (trimmed.startsWith(\"/\")) {\n return null\n }\n let parsed: URL\n try {\n parsed = new URL(trimmed)\n } catch {\n return problem(\n \"backendUrl\",\n `is ${quoted(value)}, which is not a URL`,\n trimmed.includes(\".\") || trimmed.includes(\":\")\n ? `It looks like a host without a protocol: write \"https://${trimmed}\" instead, or \"/${trimmed.replace(/^\\/+/, \"\")}\" if the console API answers on the same origin as this page.`\n : undefined\n )\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return problem(\n \"backendUrl\",\n `is ${quoted(value)}, whose \"${parsed.protocol.replace(\":\", \"\")}\" scheme is neither http nor https`,\n 'A bare \"host:port\" is read as a scheme: write \"http://localhost:3000/api\", not \"localhost:3000/api\".'\n )\n }\n return null\n}\n\n/** Everything wrong with one required option, or null when it is usable. */\nconst diagnose = (option: RequiredOption, config: unknown): ConfigProblem | null => {\n const value = (config as Record<string, unknown> | null | undefined)?.[option]\n const typo = confusableKey(option, config)\n const typoHint = typo ? `The configuration carries \"${typo}\", which the client ignores. The option is spelled \"${option}\".` : undefined\n\n if (value === undefined || value === null) {\n return problem(option, value === null ? \"is null\" : \"is missing\", typoHint, typo ? undefined : missingEnvHint(option))\n }\n if (typeof value !== \"string\") {\n return problem(option, `is ${described(value)}, not a string`, typoHint)\n }\n if (value.trim() === \"\") {\n return problem(option, value === \"\" ? \"is an empty string\" : \"holds nothing but whitespace\", typoHint, missingEnvHint(option))\n }\n if (PLACEHOLDER.test(value.trim())) {\n const looksLikeAnUnsetVariable = /^(undefined|null|nan)$/i.test(value.trim())\n return problem(option, `is still the placeholder ${quoted(value)}`, typoHint, looksLikeAnUnsetVariable ? missingEnvHint(option) : undefined)\n }\n if (option === \"projectId\" && value.trim() !== value) {\n // Percent encoded into the path, so the padding would travel to the backend as %20.\n return problem(\n \"projectId\",\n `is ${quoted(value)}, padded with whitespace`,\n \"Trim the value: it is percent encoded into the manifest URL, so the spaces would reach the backend as part of the id.\"\n )\n }\n return option === \"backendUrl\" ? diagnoseBackendUrl(value) : null\n}\n\n/** Every problem of a configuration, in the order the options are declared. Empty means usable. */\nexport const diagnoseConfig = (config: unknown): ConfigProblem[] =>\n ([\"backendUrl\", \"projectId\"] as const).map(option => diagnose(option, config)).filter((found): found is ConfigProblem => found !== null)\n\nconst bulleted = (problems: ConfigProblem[]): string => problems.map(({ option, what, hints }) => [` • ${option} ${what}.`, ...hints.map(hint => ` → ${hint}`)].join(\"\\n\")).join(\"\\n\")\n\nconst indented = (block: string): string =>\n block\n .split(\"\\n\")\n .map(line => (line ? ` ${line}` : line))\n .join(\"\\n\")\n\nconst howToConfigure = (integration: Integration): string => `Configure the client like this:\\n\\n${indented(SNIPPETS[integration])}\\n\\n${WHERE[integration]}`\n\nconst listed = (options: RequiredOption[]): string => (options.length > 1 ? `${options.slice(0, -1).join(\", \")} and ${options.at(-1)}` : String(options[0]))\n\n/** The message thrown by `configure()` when what it received cannot be used. */\nexport const invalidConfigMessage = (problems: ConfigProblem[], integration: Integration): string => {\n const options = problems.map(({ option }) => option)\n const headline = `${PREFIX} configure() cannot start the client: ${listed(options)} ${options.length > 1 ? \"are\" : \"is\"} not usable.`\n return `${headline}\\n\\n${bulleted(problems)}\\n\\n${howToConfigure(integration)}`\n}\n\n/** The message thrown by `configure()` when it did not even receive an object. */\nexport const noConfigMessage = (config: unknown, integration: Integration): string =>\n `${PREFIX} configure() was called with ${config === undefined ? \"no argument\" : described(config)} instead of a configuration object. It needs at least backendUrl and projectId.\\n\\n${howToConfigure(integration)}`\n\n/** The message every read rejects with while the client has no configuration at all. */\nexport const notConfiguredMessage = (caller: string, integration: Integration): string =>\n `${PREFIX} the client is not configured: ${caller} was called before configure() ever ran, so neither backendUrl nor projectId is known and no manifest can be requested.\\n\\n${howToConfigure(integration)}`\n","import type { Integration } from \"./errors\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport interface OrchestratorState {\n config: OrchestratorConfig | null\n manifestPromise: Promise<Manifest> | null\n identities: Identities | null\n /** Set by the framework adapters as they load, so an error prints the snippet of their own API. */\n integration: Integration\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, integration: \"core\" }\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 { diagnoseConfig, type Integration, invalidConfigMessage, noConfigMessage, notConfiguredMessage, PREFIX } from \"./errors\"\nimport { getState } from \"./state\"\nimport { readIdentities } from \"./storage\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport type { ConfigProblem, Integration } from \"./errors\"\nexport type { GlobalVariable, Identities, Manifest, Microfrontend, OrchestratorConfig } from \"./types\"\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\n/**\n * Rejects a configuration the client could not use, with a message that names the option, shows what\n * actually arrived and prints the call to write. environment is deliberately not required: without\n * it the backend resolves it from the domain. It is still checked for type, because a non string\n * there would silently end up percent encoded into the path.\n */\nconst requireConfig = (config: OrchestratorConfig, integration: Integration): void => {\n if (typeof config !== \"object\" || config === null) {\n throw new Error(noConfigMessage(config, integration))\n }\n const problems = diagnoseConfig(config)\n if (problems.length > 0) {\n throw new Error(invalidConfigMessage(problems, integration))\n }\n if (config.environment !== undefined && config.environment !== null && typeof config.environment !== \"string\") {\n throw new Error(\n `${PREFIX} configure() received environment as a ${typeof config.environment}, not a string. Pass the environment slug as it appears in the console, ex. \"DEV\", or leave it out to let the backend resolve it from the domain of this page.`\n )\n }\n}\n\n/** Notices about a stored configuration that works but probably does not do what the host meant. */\nconst warnAboutConfig = (config: OrchestratorConfig): void => {\n if (typeof config.environment === \"string\" && config.environment.trim() === \"\") {\n console.warn(\n `${PREFIX} configure() received an empty environment, so the client falls back to the auto routes and the backend resolves the environment from the domain of this page. Remove the option if that is what you want, or check the variable that feeds it.`\n )\n }\n}\n\n/**\n * Two shapes, one per route family. With an explicit environment the slug is the last segment; with\n * no environment the \"auto\" segment takes its place before the project id and the backend resolves\n * the environment from the domain the request comes from.\n */\nconst manifestPath = (config: OrchestratorConfig): string => {\n const projectId = encodeURIComponent(config.projectId)\n return config.environment ? `/serve/all/${projectId}/${encodeURIComponent(config.environment)}` : `/serve/all/auto/${projectId}`\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)}${manifestPath(config)}?${query.toString()}`\n}\n\n/**\n * What a status usually means on this route. The backend answers the same 404 for an unknown project\n * and for an unknown environment, and the two are indistinguishable from here, so both are named.\n */\nconst statusHint = (status: number, config: OrchestratorConfig): string => {\n if (status === 404) {\n const environment = config.environment\n ? `the environment \"${config.environment}\" does not exist in it`\n : \"no environment of that project declares the domain this page is served on, which is what the auto route resolves on\"\n return `\\n\\nA 404 on this route means the backend did not find what the URL names: either the project id \"${config.projectId}\" is not a project of this console, or ${environment}. Check both in the console, and check that backendUrl points at the console API of the same instance.`\n }\n if (status === 401 || status === 403) {\n return `\\n\\nThe console refused the request. Check that the project id \"${config.projectId}\" belongs to the instance backendUrl points at, and that this domain is allowed to read its manifest.`\n }\n return \"\"\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}${statusHint(response.status, config)}`)\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 * `backendUrl` and `projectId` are required. `environment` is not: omitting it makes the client use\n * the \"auto\" routes, where the backend resolves the environment from the domain of the host page.\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 const state = getState()\n requireConfig(config, state.integration)\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 warnAboutConfig(config)\n}\n\n/**\n * @internal Lets a framework adapter say which package the host is really using, so a configuration\n * error prints the snippet of that API instead of the bare `configure()` one. Idempotent, and never\n * needed by an application: the adapters call it as they load.\n */\nexport const registerIntegration = (integration: Integration): void => {\n getState().integration = integration\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 */\nconst loadManifest = (caller: string): 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(notConfiguredMessage(caller, state.integration)))\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/** The whole manifest of the environment. See `loadManifest` for the memoization contract. */\nexport const manifest = (): Promise<Manifest> => loadManifest(\"manifest()\")\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 loadManifest(`remoteUrl(\"${slug}\")`)\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 // With no configured environment the host cannot name the one it was served: only the backend knows.\n const where = environment ? `environment \"${environment}\"` : \"the environment resolved from this domain\"\n throw new Error(`${PREFIX} unknown microfrontend slug \"${slug}\". Available in ${where}: ${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 loadManifest(\"globalVariables()\")\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/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** Which package the host went through. Only decides which snippet an error prints. */
|
|
2
|
+
type Integration = "core" | "react" | "vue" | "angular";
|
|
3
|
+
/** The two options without which nothing can be requested. */
|
|
4
|
+
type RequiredOption = "backendUrl" | "projectId";
|
|
5
|
+
/** One thing that is wrong with one option, already spelled out. */
|
|
6
|
+
interface ConfigProblem {
|
|
7
|
+
option: RequiredOption;
|
|
8
|
+
/** Predicate completing "<option> …", ex. `is missing`. */
|
|
9
|
+
what: string;
|
|
10
|
+
/** Extra lines, printed under the option, that say how to fix this particular case. */
|
|
11
|
+
hints: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
/**
|
|
2
15
|
* Configuration of the orchestrator client. Handed over once, at the very top of the host entry point.
|
|
3
16
|
*/
|
|
@@ -6,8 +19,13 @@ interface OrchestratorConfig {
|
|
|
6
19
|
backendUrl: string;
|
|
7
20
|
/** Id of the project the host belongs to. */
|
|
8
21
|
projectId: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Environment slug, ex. "DEV". Optional: when it is omitted the client calls the "auto" routes
|
|
24
|
+
* and the backend resolves the environment from the domain the request comes from, out of the
|
|
25
|
+
* domains declared for each environment in the console. Pass it explicitly whenever the host
|
|
26
|
+
* already knows which environment it belongs to, or when one domain serves several of them.
|
|
27
|
+
*/
|
|
28
|
+
environment?: string;
|
|
11
29
|
/**
|
|
12
30
|
* Identity of the logged in user, if the host has one. May be a plain value, a getter, or an
|
|
13
31
|
* async getter resolved right before the manifest request is issued.
|
|
@@ -33,7 +51,10 @@ interface Microfrontend {
|
|
|
33
51
|
/** Ready to use, already version pinned URL of the remote entry. Use it verbatim. */
|
|
34
52
|
url: string;
|
|
35
53
|
}
|
|
36
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The whole payload of `GET {backendUrl}/serve/all/{projectId}/{environmentSlug}`, or of
|
|
56
|
+
* `GET {backendUrl}/serve/all/auto/{projectId}` when no environment is configured.
|
|
57
|
+
*/
|
|
37
58
|
interface Manifest {
|
|
38
59
|
globalVariables: GlobalVariable[];
|
|
39
60
|
microfrontends: Microfrontend[];
|
|
@@ -47,20 +68,21 @@ interface Identities {
|
|
|
47
68
|
/**
|
|
48
69
|
* Hands the client its configuration. Call it once, synchronously, before any remote is imported.
|
|
49
70
|
*
|
|
71
|
+
* `backendUrl` and `projectId` are required. `environment` is not: omitting it makes the client use
|
|
72
|
+
* the "auto" routes, where the backend resolves the environment from the domain of the host page.
|
|
73
|
+
*
|
|
50
74
|
* Idempotent: calling it again with the same options is a no op, so a framework provider may safely
|
|
51
75
|
* call it on every render. A second call with different options is ignored, with a warning, because
|
|
52
76
|
* the manifest of this page load may already be in flight or resolved.
|
|
53
77
|
*/
|
|
54
78
|
declare const configure: (config: OrchestratorConfig) => void;
|
|
55
79
|
/**
|
|
56
|
-
*
|
|
57
|
-
* that
|
|
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.
|
|
80
|
+
* @internal Lets a framework adapter say which package the host is really using, so a configuration
|
|
81
|
+
* error prints the snippet of that API instead of the bare `configure()` one. Idempotent, and never
|
|
82
|
+
* needed by an application: the adapters call it as they load.
|
|
63
83
|
*/
|
|
84
|
+
declare const registerIntegration: (integration: Integration) => void;
|
|
85
|
+
/** The whole manifest of the environment. See `loadManifest` for the memoization contract. */
|
|
64
86
|
declare const manifest: () => Promise<Manifest>;
|
|
65
87
|
/**
|
|
66
88
|
* The ready to use, version pinned URL of a remote. Awaits the manifest internally.
|
|
@@ -77,4 +99,4 @@ declare const globalVariables: () => Promise<Record<string, string>>;
|
|
|
77
99
|
*/
|
|
78
100
|
declare const identities: () => Identities;
|
|
79
101
|
|
|
80
|
-
export { type GlobalVariable, type Identities, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, remoteUrl };
|
|
102
|
+
export { type ConfigProblem, type GlobalVariable, type Identities, type Integration, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, registerIntegration, remoteUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** Which package the host went through. Only decides which snippet an error prints. */
|
|
2
|
+
type Integration = "core" | "react" | "vue" | "angular";
|
|
3
|
+
/** The two options without which nothing can be requested. */
|
|
4
|
+
type RequiredOption = "backendUrl" | "projectId";
|
|
5
|
+
/** One thing that is wrong with one option, already spelled out. */
|
|
6
|
+
interface ConfigProblem {
|
|
7
|
+
option: RequiredOption;
|
|
8
|
+
/** Predicate completing "<option> …", ex. `is missing`. */
|
|
9
|
+
what: string;
|
|
10
|
+
/** Extra lines, printed under the option, that say how to fix this particular case. */
|
|
11
|
+
hints: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
/**
|
|
2
15
|
* Configuration of the orchestrator client. Handed over once, at the very top of the host entry point.
|
|
3
16
|
*/
|
|
@@ -6,8 +19,13 @@ interface OrchestratorConfig {
|
|
|
6
19
|
backendUrl: string;
|
|
7
20
|
/** Id of the project the host belongs to. */
|
|
8
21
|
projectId: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Environment slug, ex. "DEV". Optional: when it is omitted the client calls the "auto" routes
|
|
24
|
+
* and the backend resolves the environment from the domain the request comes from, out of the
|
|
25
|
+
* domains declared for each environment in the console. Pass it explicitly whenever the host
|
|
26
|
+
* already knows which environment it belongs to, or when one domain serves several of them.
|
|
27
|
+
*/
|
|
28
|
+
environment?: string;
|
|
11
29
|
/**
|
|
12
30
|
* Identity of the logged in user, if the host has one. May be a plain value, a getter, or an
|
|
13
31
|
* async getter resolved right before the manifest request is issued.
|
|
@@ -33,7 +51,10 @@ interface Microfrontend {
|
|
|
33
51
|
/** Ready to use, already version pinned URL of the remote entry. Use it verbatim. */
|
|
34
52
|
url: string;
|
|
35
53
|
}
|
|
36
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The whole payload of `GET {backendUrl}/serve/all/{projectId}/{environmentSlug}`, or of
|
|
56
|
+
* `GET {backendUrl}/serve/all/auto/{projectId}` when no environment is configured.
|
|
57
|
+
*/
|
|
37
58
|
interface Manifest {
|
|
38
59
|
globalVariables: GlobalVariable[];
|
|
39
60
|
microfrontends: Microfrontend[];
|
|
@@ -47,20 +68,21 @@ interface Identities {
|
|
|
47
68
|
/**
|
|
48
69
|
* Hands the client its configuration. Call it once, synchronously, before any remote is imported.
|
|
49
70
|
*
|
|
71
|
+
* `backendUrl` and `projectId` are required. `environment` is not: omitting it makes the client use
|
|
72
|
+
* the "auto" routes, where the backend resolves the environment from the domain of the host page.
|
|
73
|
+
*
|
|
50
74
|
* Idempotent: calling it again with the same options is a no op, so a framework provider may safely
|
|
51
75
|
* call it on every render. A second call with different options is ignored, with a warning, because
|
|
52
76
|
* the manifest of this page load may already be in flight or resolved.
|
|
53
77
|
*/
|
|
54
78
|
declare const configure: (config: OrchestratorConfig) => void;
|
|
55
79
|
/**
|
|
56
|
-
*
|
|
57
|
-
* that
|
|
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.
|
|
80
|
+
* @internal Lets a framework adapter say which package the host is really using, so a configuration
|
|
81
|
+
* error prints the snippet of that API instead of the bare `configure()` one. Idempotent, and never
|
|
82
|
+
* needed by an application: the adapters call it as they load.
|
|
63
83
|
*/
|
|
84
|
+
declare const registerIntegration: (integration: Integration) => void;
|
|
85
|
+
/** The whole manifest of the environment. See `loadManifest` for the memoization contract. */
|
|
64
86
|
declare const manifest: () => Promise<Manifest>;
|
|
65
87
|
/**
|
|
66
88
|
* The ready to use, version pinned URL of a remote. Awaits the manifest internally.
|
|
@@ -77,4 +99,4 @@ declare const globalVariables: () => Promise<Record<string, string>>;
|
|
|
77
99
|
*/
|
|
78
100
|
declare const identities: () => Identities;
|
|
79
101
|
|
|
80
|
-
export { type GlobalVariable, type Identities, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, remoteUrl };
|
|
102
|
+
export { type ConfigProblem, type GlobalVariable, type Identities, type Integration, type Manifest, type Microfrontend, type OrchestratorConfig, configure, globalVariables, identities, manifest, registerIntegration, remoteUrl };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,154 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PREFIX = "[@mfe-orchestrator-hub/client]";
|
|
3
|
+
var EXAMPLE_BACKEND_URL = "https://console.mfe-orchestrator.dev/api";
|
|
4
|
+
var EXAMPLE_PROJECT_ID = "6f1b2c3d4e5f6a7b8c9d0e1f";
|
|
5
|
+
var SNIPPETS = {
|
|
6
|
+
core: `import { configure } from "@mfe-orchestrator-hub/client"
|
|
7
|
+
|
|
8
|
+
configure({
|
|
9
|
+
backendUrl: "${EXAMPLE_BACKEND_URL}",
|
|
10
|
+
projectId: "${EXAMPLE_PROJECT_ID}"
|
|
11
|
+
})`,
|
|
12
|
+
react: `import { OrchestratorProvider } from "@mfe-orchestrator-hub/client-react"
|
|
13
|
+
|
|
14
|
+
<OrchestratorProvider config={{ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" }}>
|
|
15
|
+
<App />
|
|
16
|
+
</OrchestratorProvider>`,
|
|
17
|
+
vue: `import { createOrchestrator } from "@mfe-orchestrator-hub/client-vue"
|
|
18
|
+
|
|
19
|
+
app.use(createOrchestrator({ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" }))`,
|
|
20
|
+
angular: `import { provideOrchestrator } from "@mfe-orchestrator-hub/client-angular"
|
|
21
|
+
|
|
22
|
+
bootstrapApplication(AppComponent, {
|
|
23
|
+
providers: [provideOrchestrator({ backendUrl: "${EXAMPLE_BACKEND_URL}", projectId: "${EXAMPLE_PROJECT_ID}" })]
|
|
24
|
+
})`
|
|
25
|
+
};
|
|
26
|
+
var WHERE = {
|
|
27
|
+
core: "Call it once, at the very top of the host entry point, before anything imports a remote.",
|
|
28
|
+
react: "Render it above every component that resolves a remote. Better still, call configure() at the very top of the entry point: a bundler may import a remote before React mounts.",
|
|
29
|
+
vue: "Install the plugin before mounting the app. Better still, call configure() at the very top of the entry point: a bundler may import a remote before the app is created.",
|
|
30
|
+
angular: "Pass it to bootstrapApplication(). Better still, call configure() at the very top of main.ts: a lazily loaded remote must find the client already configured."
|
|
31
|
+
};
|
|
32
|
+
var WHAT_IT_IS = {
|
|
33
|
+
backendUrl: `It is the base URL of the console API, protocol included, ex. "${EXAMPLE_BACKEND_URL}". A path on the same origin as the host, ex. "/api", works too.`,
|
|
34
|
+
projectId: `It is the id of the project in the console: open the project and copy it from its page, ex. "${EXAMPLE_PROJECT_ID}".`
|
|
35
|
+
};
|
|
36
|
+
var BUILD_VARIABLE = {
|
|
37
|
+
backendUrl: { vite: "VITE_MFE_BACKEND_URL", webpack: "MFE_BACKEND_URL" },
|
|
38
|
+
projectId: { vite: "VITE_MFE_PROJECT_ID", webpack: "MFE_PROJECT_ID" }
|
|
39
|
+
};
|
|
40
|
+
var missingEnvHint = (option) => `Nothing arrived here at all, which is what a value that never reached the bundle looks like: check ${BUILD_VARIABLE[option].vite} (Vite) or ${BUILD_VARIABLE[option].webpack} (webpack) in the build that produced this bundle. The generated configs carry it in the "define" block of vite.config, or in the DefinePlugin of webpack.config, so look there first; declared only in a .env file the build never reads, it looks exactly like this.`;
|
|
41
|
+
var PLACEHOLDER = /^(…|\.{3}|undefined|null|nan|<[^>]*>|\{\{[^}]*\}\}|\$\{[^}]*\}|your[-_ ]?[a-z]*|x{3,}|change[-_ ]?me|todo|tbd)$/i;
|
|
42
|
+
var quoted = (value) => JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}\u2026` : value);
|
|
43
|
+
var described = (value) => {
|
|
44
|
+
if (value === null) {
|
|
45
|
+
return "null";
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return "an array";
|
|
49
|
+
}
|
|
50
|
+
const type = typeof value;
|
|
51
|
+
return type === "object" || type === "function" ? `a ${type}` : `a ${type} (${String(value)})`;
|
|
52
|
+
};
|
|
53
|
+
var normalized = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
54
|
+
var ALIASES = {
|
|
55
|
+
backendUrl: ["backendurl", "backendbaseurl", "apiurl", "baseurl", "consoleurl", "url", "backend"],
|
|
56
|
+
projectId: ["projectid", "project", "idproject", "projectuuid", "projectkey"]
|
|
57
|
+
};
|
|
58
|
+
var confusableKey = (option, config) => {
|
|
59
|
+
if (typeof config !== "object" || config === null) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
const candidates = ALIASES[option];
|
|
63
|
+
return Object.keys(config).find((key) => key !== option && candidates.includes(normalized(key)));
|
|
64
|
+
};
|
|
65
|
+
var problem = (option, what, ...hints) => ({
|
|
66
|
+
option,
|
|
67
|
+
what,
|
|
68
|
+
hints: [WHAT_IT_IS[option], ...hints].filter((hint) => Boolean(hint))
|
|
69
|
+
});
|
|
70
|
+
var diagnoseBackendUrl = (value) => {
|
|
71
|
+
const trimmed = value.trim();
|
|
72
|
+
if (trimmed.startsWith("/")) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = new URL(trimmed);
|
|
78
|
+
} catch {
|
|
79
|
+
return problem(
|
|
80
|
+
"backendUrl",
|
|
81
|
+
`is ${quoted(value)}, which is not a URL`,
|
|
82
|
+
trimmed.includes(".") || trimmed.includes(":") ? `It looks like a host without a protocol: write "https://${trimmed}" instead, or "/${trimmed.replace(/^\/+/, "")}" if the console API answers on the same origin as this page.` : void 0
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
86
|
+
return problem(
|
|
87
|
+
"backendUrl",
|
|
88
|
+
`is ${quoted(value)}, whose "${parsed.protocol.replace(":", "")}" scheme is neither http nor https`,
|
|
89
|
+
'A bare "host:port" is read as a scheme: write "http://localhost:3000/api", not "localhost:3000/api".'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
var diagnose = (option, config) => {
|
|
95
|
+
const value = config?.[option];
|
|
96
|
+
const typo = confusableKey(option, config);
|
|
97
|
+
const typoHint = typo ? `The configuration carries "${typo}", which the client ignores. The option is spelled "${option}".` : void 0;
|
|
98
|
+
if (value === void 0 || value === null) {
|
|
99
|
+
return problem(option, value === null ? "is null" : "is missing", typoHint, typo ? void 0 : missingEnvHint(option));
|
|
100
|
+
}
|
|
101
|
+
if (typeof value !== "string") {
|
|
102
|
+
return problem(option, `is ${described(value)}, not a string`, typoHint);
|
|
103
|
+
}
|
|
104
|
+
if (value.trim() === "") {
|
|
105
|
+
return problem(option, value === "" ? "is an empty string" : "holds nothing but whitespace", typoHint, missingEnvHint(option));
|
|
106
|
+
}
|
|
107
|
+
if (PLACEHOLDER.test(value.trim())) {
|
|
108
|
+
const looksLikeAnUnsetVariable = /^(undefined|null|nan)$/i.test(value.trim());
|
|
109
|
+
return problem(option, `is still the placeholder ${quoted(value)}`, typoHint, looksLikeAnUnsetVariable ? missingEnvHint(option) : void 0);
|
|
110
|
+
}
|
|
111
|
+
if (option === "projectId" && value.trim() !== value) {
|
|
112
|
+
return problem(
|
|
113
|
+
"projectId",
|
|
114
|
+
`is ${quoted(value)}, padded with whitespace`,
|
|
115
|
+
"Trim the value: it is percent encoded into the manifest URL, so the spaces would reach the backend as part of the id."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return option === "backendUrl" ? diagnoseBackendUrl(value) : null;
|
|
119
|
+
};
|
|
120
|
+
var diagnoseConfig = (config) => ["backendUrl", "projectId"].map((option) => diagnose(option, config)).filter((found) => found !== null);
|
|
121
|
+
var bulleted = (problems) => problems.map(({ option, what, hints }) => [` \u2022 ${option} ${what}.`, ...hints.map((hint) => ` \u2192 ${hint}`)].join("\n")).join("\n");
|
|
122
|
+
var indented = (block) => block.split("\n").map((line) => line ? ` ${line}` : line).join("\n");
|
|
123
|
+
var howToConfigure = (integration) => `Configure the client like this:
|
|
124
|
+
|
|
125
|
+
${indented(SNIPPETS[integration])}
|
|
126
|
+
|
|
127
|
+
${WHERE[integration]}`;
|
|
128
|
+
var listed = (options) => options.length > 1 ? `${options.slice(0, -1).join(", ")} and ${options.at(-1)}` : String(options[0]);
|
|
129
|
+
var invalidConfigMessage = (problems, integration) => {
|
|
130
|
+
const options = problems.map(({ option }) => option);
|
|
131
|
+
const headline = `${PREFIX} configure() cannot start the client: ${listed(options)} ${options.length > 1 ? "are" : "is"} not usable.`;
|
|
132
|
+
return `${headline}
|
|
133
|
+
|
|
134
|
+
${bulleted(problems)}
|
|
135
|
+
|
|
136
|
+
${howToConfigure(integration)}`;
|
|
137
|
+
};
|
|
138
|
+
var noConfigMessage = (config, integration) => `${PREFIX} configure() was called with ${config === void 0 ? "no argument" : described(config)} instead of a configuration object. It needs at least backendUrl and projectId.
|
|
139
|
+
|
|
140
|
+
${howToConfigure(integration)}`;
|
|
141
|
+
var notConfiguredMessage = (caller, integration) => `${PREFIX} the client is not configured: ${caller} was called before configure() ever ran, so neither backendUrl nor projectId is known and no manifest can be requested.
|
|
142
|
+
|
|
143
|
+
${howToConfigure(integration)}`;
|
|
144
|
+
|
|
1
145
|
// src/state.ts
|
|
2
146
|
var STATE_KEY = "__mfe_orchestrator_client__";
|
|
3
147
|
var globalSlot = globalThis;
|
|
4
148
|
var getState = () => {
|
|
5
149
|
let state = globalSlot[STATE_KEY];
|
|
6
150
|
if (!state) {
|
|
7
|
-
state = { config: null, manifestPromise: null, identities: null };
|
|
151
|
+
state = { config: null, manifestPromise: null, identities: null, integration: "core" };
|
|
8
152
|
globalSlot[STATE_KEY] = state;
|
|
9
153
|
}
|
|
10
154
|
return state;
|
|
@@ -82,24 +226,37 @@ var readIdentities = () => ({
|
|
|
82
226
|
});
|
|
83
227
|
|
|
84
228
|
// 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
229
|
var withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
92
230
|
var resolveUserId = async (userId) => {
|
|
93
231
|
const value = typeof userId === "function" ? await userId() : userId;
|
|
94
232
|
return value === void 0 || value === null || value === "" ? void 0 : String(value);
|
|
95
233
|
};
|
|
96
234
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
235
|
+
var requireConfig = (config, integration) => {
|
|
236
|
+
if (typeof config !== "object" || config === null) {
|
|
237
|
+
throw new Error(noConfigMessage(config, integration));
|
|
238
|
+
}
|
|
239
|
+
const problems = diagnoseConfig(config);
|
|
240
|
+
if (problems.length > 0) {
|
|
241
|
+
throw new Error(invalidConfigMessage(problems, integration));
|
|
242
|
+
}
|
|
243
|
+
if (config.environment !== void 0 && config.environment !== null && typeof config.environment !== "string") {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`${PREFIX} configure() received environment as a ${typeof config.environment}, not a string. Pass the environment slug as it appears in the console, ex. "DEV", or leave it out to let the backend resolve it from the domain of this page.`
|
|
246
|
+
);
|
|
101
247
|
}
|
|
102
248
|
};
|
|
249
|
+
var warnAboutConfig = (config) => {
|
|
250
|
+
if (typeof config.environment === "string" && config.environment.trim() === "") {
|
|
251
|
+
console.warn(
|
|
252
|
+
`${PREFIX} configure() received an empty environment, so the client falls back to the auto routes and the backend resolves the environment from the domain of this page. Remove the option if that is what you want, or check the variable that feeds it.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var manifestPath = (config) => {
|
|
257
|
+
const projectId = encodeURIComponent(config.projectId);
|
|
258
|
+
return config.environment ? `/serve/all/${projectId}/${encodeURIComponent(config.environment)}` : `/serve/all/auto/${projectId}`;
|
|
259
|
+
};
|
|
103
260
|
var manifestUrl = async (config) => {
|
|
104
261
|
const { sessionId, deviceId } = identities();
|
|
105
262
|
const query = new URLSearchParams({ mfeSessionId: sessionId, mfeDeviceId: deviceId });
|
|
@@ -107,13 +264,27 @@ var manifestUrl = async (config) => {
|
|
|
107
264
|
if (userId) {
|
|
108
265
|
query.set("mfeUserId", userId);
|
|
109
266
|
}
|
|
110
|
-
return `${withoutTrailingSlash(config.backendUrl)}
|
|
267
|
+
return `${withoutTrailingSlash(config.backendUrl)}${manifestPath(config)}?${query.toString()}`;
|
|
268
|
+
};
|
|
269
|
+
var statusHint = (status, config) => {
|
|
270
|
+
if (status === 404) {
|
|
271
|
+
const environment = config.environment ? `the environment "${config.environment}" does not exist in it` : "no environment of that project declares the domain this page is served on, which is what the auto route resolves on";
|
|
272
|
+
return `
|
|
273
|
+
|
|
274
|
+
A 404 on this route means the backend did not find what the URL names: either the project id "${config.projectId}" is not a project of this console, or ${environment}. Check both in the console, and check that backendUrl points at the console API of the same instance.`;
|
|
275
|
+
}
|
|
276
|
+
if (status === 401 || status === 403) {
|
|
277
|
+
return `
|
|
278
|
+
|
|
279
|
+
The console refused the request. Check that the project id "${config.projectId}" belongs to the instance backendUrl points at, and that this domain is allowed to read its manifest.`;
|
|
280
|
+
}
|
|
281
|
+
return "";
|
|
111
282
|
};
|
|
112
283
|
var fetchManifest = async (config) => {
|
|
113
284
|
const url = await manifestUrl(config);
|
|
114
285
|
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
115
286
|
if (!response.ok) {
|
|
116
|
-
throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}`);
|
|
287
|
+
throw new Error(`${PREFIX} the manifest request failed with ${response.status} ${response.statusText}. GET ${url}${statusHint(response.status, config)}`);
|
|
117
288
|
}
|
|
118
289
|
const payload = await response.json();
|
|
119
290
|
return {
|
|
@@ -122,8 +293,8 @@ var fetchManifest = async (config) => {
|
|
|
122
293
|
};
|
|
123
294
|
};
|
|
124
295
|
var configure = (config) => {
|
|
125
|
-
requireConfig(config);
|
|
126
296
|
const state = getState();
|
|
297
|
+
requireConfig(config, state.integration);
|
|
127
298
|
const current = state.config;
|
|
128
299
|
if (current) {
|
|
129
300
|
if (!sameConfig(current, config)) {
|
|
@@ -134,12 +305,16 @@ var configure = (config) => {
|
|
|
134
305
|
return;
|
|
135
306
|
}
|
|
136
307
|
state.config = { ...config };
|
|
308
|
+
warnAboutConfig(config);
|
|
309
|
+
};
|
|
310
|
+
var registerIntegration = (integration) => {
|
|
311
|
+
getState().integration = integration;
|
|
137
312
|
};
|
|
138
|
-
var
|
|
313
|
+
var loadManifest = (caller) => {
|
|
139
314
|
const state = getState();
|
|
140
315
|
const config = state.config;
|
|
141
316
|
if (!config) {
|
|
142
|
-
return Promise.reject(new Error(
|
|
317
|
+
return Promise.reject(new Error(notConfiguredMessage(caller, state.integration)));
|
|
143
318
|
}
|
|
144
319
|
if (!state.manifestPromise) {
|
|
145
320
|
const attempt = fetchManifest(config);
|
|
@@ -152,13 +327,15 @@ var manifest = () => {
|
|
|
152
327
|
}
|
|
153
328
|
return state.manifestPromise;
|
|
154
329
|
};
|
|
330
|
+
var manifest = () => loadManifest("manifest()");
|
|
155
331
|
var remoteUrl = async (slug) => {
|
|
156
|
-
const { microfrontends } = await
|
|
332
|
+
const { microfrontends } = await loadManifest(`remoteUrl("${slug}")`);
|
|
157
333
|
const found = microfrontends.find((microfrontend) => microfrontend.slug === slug);
|
|
158
334
|
if (!found) {
|
|
159
335
|
const available = microfrontends.map((microfrontend) => `"${microfrontend.slug}"`).join(", ");
|
|
160
|
-
const
|
|
161
|
-
|
|
336
|
+
const environment = getState().config?.environment;
|
|
337
|
+
const where = environment ? `environment "${environment}"` : "the environment resolved from this domain";
|
|
338
|
+
throw new Error(`${PREFIX} unknown microfrontend slug "${slug}". Available in ${where}: ${available || "none, this environment serves no microfrontend"}.`);
|
|
162
339
|
}
|
|
163
340
|
if (!found.url) {
|
|
164
341
|
throw new Error(`${PREFIX} the microfrontend "${slug}" is in the manifest but carries no url. Check its deployment in the console.`);
|
|
@@ -166,7 +343,7 @@ var remoteUrl = async (slug) => {
|
|
|
166
343
|
return found.url;
|
|
167
344
|
};
|
|
168
345
|
var globalVariables = async () => {
|
|
169
|
-
const { globalVariables: variables } = await
|
|
346
|
+
const { globalVariables: variables } = await loadManifest("globalVariables()");
|
|
170
347
|
const result = {};
|
|
171
348
|
for (const variable of variables) {
|
|
172
349
|
if (variable && typeof variable.key === "string") {
|
|
@@ -183,6 +360,6 @@ var identities = () => {
|
|
|
183
360
|
return state.identities;
|
|
184
361
|
};
|
|
185
362
|
|
|
186
|
-
export { configure, globalVariables, identities, manifest, remoteUrl };
|
|
363
|
+
export { configure, globalVariables, identities, manifest, registerIntegration, remoteUrl };
|
|
187
364
|
//# sourceMappingURL=index.js.map
|
|
188
365
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/state.ts","../src/storage.ts","../src/index.ts"],"names":["created"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Everything the client says when its configuration is wrong or absent.\n *\n * The two required options almost always arrive from build time environment variables, so the\n * failure the host sees is not \"a value is invalid\" but \"nothing reached the bundle\". Messages here\n * are written for that moment: they name the option, show what actually arrived, say where the real\n * value comes from, and print the exact call to write, in the flavour of the package the host uses.\n */\n\nexport const PREFIX = \"[@mfe-orchestrator-hub/client]\"\n\n/** Which package the host went through. Only decides which snippet an error prints. */\nexport type Integration = \"core\" | \"react\" | \"vue\" | \"angular\"\n\n/** The two options without which nothing can be requested. */\nexport type RequiredOption = \"backendUrl\" | \"projectId\"\n\nconst EXAMPLE_BACKEND_URL = \"https://console.mfe-orchestrator.dev/api\"\nconst EXAMPLE_PROJECT_ID = \"6f1b2c3d4e5f6a7b8c9d0e1f\"\n\nconst SNIPPETS: Record<Integration, string> = {\n core: `import { configure } from \"@mfe-orchestrator-hub/client\"\n\nconfigure({\n backendUrl: \"${EXAMPLE_BACKEND_URL}\",\n projectId: \"${EXAMPLE_PROJECT_ID}\"\n})`,\n react: `import { OrchestratorProvider } from \"@mfe-orchestrator-hub/client-react\"\n\n<OrchestratorProvider config={{ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" }}>\n <App />\n</OrchestratorProvider>`,\n vue: `import { createOrchestrator } from \"@mfe-orchestrator-hub/client-vue\"\n\napp.use(createOrchestrator({ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" }))`,\n angular: `import { provideOrchestrator } from \"@mfe-orchestrator-hub/client-angular\"\n\nbootstrapApplication(AppComponent, {\n providers: [provideOrchestrator({ backendUrl: \"${EXAMPLE_BACKEND_URL}\", projectId: \"${EXAMPLE_PROJECT_ID}\" })]\n})`\n}\n\nconst WHERE: Record<Integration, string> = {\n core: \"Call it once, at the very top of the host entry point, before anything imports a remote.\",\n react: \"Render it above every component that resolves a remote. Better still, call configure() at the very top of the entry point: a bundler may import a remote before React mounts.\",\n vue: \"Install the plugin before mounting the app. Better still, call configure() at the very top of the entry point: a bundler may import a remote before the app is created.\",\n angular: \"Pass it to bootstrapApplication(). Better still, call configure() at the very top of main.ts: a lazily loaded remote must find the client already configured.\"\n}\n\nconst WHAT_IT_IS: Record<RequiredOption, string> = {\n backendUrl: `It is the base URL of the console API, protocol included, ex. \"${EXAMPLE_BACKEND_URL}\". A path on the same origin as the host, ex. \"/api\", works too.`,\n projectId: `It is the id of the project in the console: open the project and copy it from its page, ex. \"${EXAMPLE_PROJECT_ID}\".`\n}\n\n/** The name the value travels under from the bundler config into the bundle, per toolchain. */\nconst BUILD_VARIABLE: Record<RequiredOption, { vite: string; webpack: string }> = {\n backendUrl: { vite: \"VITE_MFE_BACKEND_URL\", webpack: \"MFE_BACKEND_URL\" },\n projectId: { vite: \"VITE_MFE_PROJECT_ID\", webpack: \"MFE_PROJECT_ID\" }\n}\n\n/**\n * What an option that was never substituted into the bundle looks like.\n *\n * The bundler config is named before the .env file on purpose: it is where the console writes both\n * values when it generates the config, so it is where they are missing from when they are missing.\n */\nconst missingEnvHint = (option: RequiredOption): string =>\n `Nothing arrived here at all, which is what a value that never reached the bundle looks like: check ${BUILD_VARIABLE[option].vite} (Vite) or ${BUILD_VARIABLE[option].webpack} (webpack) in the build that produced this bundle. The generated configs carry it in the \"define\" block of vite.config, or in the DefinePlugin of webpack.config, so look there first; declared only in a .env file the build never reads, it looks exactly like this.`\n\n/** Values that mean \"the template was never filled in\". */\nconst PLACEHOLDER = /^(…|\\.{3}|undefined|null|nan|<[^>]*>|\\{\\{[^}]*\\}\\}|\\$\\{[^}]*\\}|your[-_ ]?[a-z]*|x{3,}|change[-_ ]?me|todo|tbd)$/i\n\n/** Long values are cut: an error message is not the place to print a whole JWT. */\nconst quoted = (value: string): string => JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}…` : value)\n\nconst described = (value: unknown): string => {\n if (value === null) {\n return \"null\"\n }\n if (Array.isArray(value)) {\n return \"an array\"\n }\n const type = typeof value\n return type === \"object\" || type === \"function\" ? `a ${type}` : `a ${type} (${String(value)})`\n}\n\n/** Same key modulo case and separators: projectID, project_id, PROJECT-ID all collapse onto projectid. */\nconst normalized = (key: string): string => key.toLowerCase().replace(/[^a-z0-9]/g, \"\")\n\nconst ALIASES: Record<RequiredOption, string[]> = {\n backendUrl: [\"backendurl\", \"backendbaseurl\", \"apiurl\", \"baseurl\", \"consoleurl\", \"url\", \"backend\"],\n projectId: [\"projectid\", \"project\", \"idproject\", \"projectuuid\", \"projectkey\"]\n}\n\n/**\n * The key the host actually wrote, when it looks like a misspelling of the one that is missing.\n * `projectID` is by far the most common: it is silently ignored, so without this the message would\n * claim the option is absent while the developer stares at a line that clearly sets it.\n */\nconst confusableKey = (option: RequiredOption, config: unknown): string | undefined => {\n if (typeof config !== \"object\" || config === null) {\n return undefined\n }\n const candidates = ALIASES[option]\n return Object.keys(config).find(key => key !== option && candidates.includes(normalized(key)))\n}\n\n/** One thing that is wrong with one option, already spelled out. */\nexport interface ConfigProblem {\n option: RequiredOption\n /** Predicate completing \"<option> …\", ex. `is missing`. */\n what: string\n /** Extra lines, printed under the option, that say how to fix this particular case. */\n hints: string[]\n}\n\nconst problem = (option: RequiredOption, what: string, ...hints: (string | undefined)[]): ConfigProblem => ({\n option,\n what,\n hints: [WHAT_IT_IS[option], ...hints].filter((hint): hint is string => Boolean(hint))\n})\n\n/**\n * A backendUrl the client can actually build a request on: an absolute http(s) URL, or a path on the\n * host's own origin, which `fetch` resolves against the page.\n */\nconst diagnoseBackendUrl = (value: string): ConfigProblem | null => {\n const trimmed = value.trim()\n if (trimmed.startsWith(\"/\")) {\n return null\n }\n let parsed: URL\n try {\n parsed = new URL(trimmed)\n } catch {\n return problem(\n \"backendUrl\",\n `is ${quoted(value)}, which is not a URL`,\n trimmed.includes(\".\") || trimmed.includes(\":\")\n ? `It looks like a host without a protocol: write \"https://${trimmed}\" instead, or \"/${trimmed.replace(/^\\/+/, \"\")}\" if the console API answers on the same origin as this page.`\n : undefined\n )\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return problem(\n \"backendUrl\",\n `is ${quoted(value)}, whose \"${parsed.protocol.replace(\":\", \"\")}\" scheme is neither http nor https`,\n 'A bare \"host:port\" is read as a scheme: write \"http://localhost:3000/api\", not \"localhost:3000/api\".'\n )\n }\n return null\n}\n\n/** Everything wrong with one required option, or null when it is usable. */\nconst diagnose = (option: RequiredOption, config: unknown): ConfigProblem | null => {\n const value = (config as Record<string, unknown> | null | undefined)?.[option]\n const typo = confusableKey(option, config)\n const typoHint = typo ? `The configuration carries \"${typo}\", which the client ignores. The option is spelled \"${option}\".` : undefined\n\n if (value === undefined || value === null) {\n return problem(option, value === null ? \"is null\" : \"is missing\", typoHint, typo ? undefined : missingEnvHint(option))\n }\n if (typeof value !== \"string\") {\n return problem(option, `is ${described(value)}, not a string`, typoHint)\n }\n if (value.trim() === \"\") {\n return problem(option, value === \"\" ? \"is an empty string\" : \"holds nothing but whitespace\", typoHint, missingEnvHint(option))\n }\n if (PLACEHOLDER.test(value.trim())) {\n const looksLikeAnUnsetVariable = /^(undefined|null|nan)$/i.test(value.trim())\n return problem(option, `is still the placeholder ${quoted(value)}`, typoHint, looksLikeAnUnsetVariable ? missingEnvHint(option) : undefined)\n }\n if (option === \"projectId\" && value.trim() !== value) {\n // Percent encoded into the path, so the padding would travel to the backend as %20.\n return problem(\n \"projectId\",\n `is ${quoted(value)}, padded with whitespace`,\n \"Trim the value: it is percent encoded into the manifest URL, so the spaces would reach the backend as part of the id.\"\n )\n }\n return option === \"backendUrl\" ? diagnoseBackendUrl(value) : null\n}\n\n/** Every problem of a configuration, in the order the options are declared. Empty means usable. */\nexport const diagnoseConfig = (config: unknown): ConfigProblem[] =>\n ([\"backendUrl\", \"projectId\"] as const).map(option => diagnose(option, config)).filter((found): found is ConfigProblem => found !== null)\n\nconst bulleted = (problems: ConfigProblem[]): string => problems.map(({ option, what, hints }) => [` • ${option} ${what}.`, ...hints.map(hint => ` → ${hint}`)].join(\"\\n\")).join(\"\\n\")\n\nconst indented = (block: string): string =>\n block\n .split(\"\\n\")\n .map(line => (line ? ` ${line}` : line))\n .join(\"\\n\")\n\nconst howToConfigure = (integration: Integration): string => `Configure the client like this:\\n\\n${indented(SNIPPETS[integration])}\\n\\n${WHERE[integration]}`\n\nconst listed = (options: RequiredOption[]): string => (options.length > 1 ? `${options.slice(0, -1).join(\", \")} and ${options.at(-1)}` : String(options[0]))\n\n/** The message thrown by `configure()` when what it received cannot be used. */\nexport const invalidConfigMessage = (problems: ConfigProblem[], integration: Integration): string => {\n const options = problems.map(({ option }) => option)\n const headline = `${PREFIX} configure() cannot start the client: ${listed(options)} ${options.length > 1 ? \"are\" : \"is\"} not usable.`\n return `${headline}\\n\\n${bulleted(problems)}\\n\\n${howToConfigure(integration)}`\n}\n\n/** The message thrown by `configure()` when it did not even receive an object. */\nexport const noConfigMessage = (config: unknown, integration: Integration): string =>\n `${PREFIX} configure() was called with ${config === undefined ? \"no argument\" : described(config)} instead of a configuration object. It needs at least backendUrl and projectId.\\n\\n${howToConfigure(integration)}`\n\n/** The message every read rejects with while the client has no configuration at all. */\nexport const notConfiguredMessage = (caller: string, integration: Integration): string =>\n `${PREFIX} the client is not configured: ${caller} was called before configure() ever ran, so neither backendUrl nor projectId is known and no manifest can be requested.\\n\\n${howToConfigure(integration)}`\n","import type { Integration } from \"./errors\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport interface OrchestratorState {\n config: OrchestratorConfig | null\n manifestPromise: Promise<Manifest> | null\n identities: Identities | null\n /** Set by the framework adapters as they load, so an error prints the snippet of their own API. */\n integration: Integration\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, integration: \"core\" }\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 { diagnoseConfig, type Integration, invalidConfigMessage, noConfigMessage, notConfiguredMessage, PREFIX } from \"./errors\"\nimport { getState } from \"./state\"\nimport { readIdentities } from \"./storage\"\nimport type { Identities, Manifest, OrchestratorConfig } from \"./types\"\n\nexport type { ConfigProblem, Integration } from \"./errors\"\nexport type { GlobalVariable, Identities, Manifest, Microfrontend, OrchestratorConfig } from \"./types\"\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\n/**\n * Rejects a configuration the client could not use, with a message that names the option, shows what\n * actually arrived and prints the call to write. environment is deliberately not required: without\n * it the backend resolves it from the domain. It is still checked for type, because a non string\n * there would silently end up percent encoded into the path.\n */\nconst requireConfig = (config: OrchestratorConfig, integration: Integration): void => {\n if (typeof config !== \"object\" || config === null) {\n throw new Error(noConfigMessage(config, integration))\n }\n const problems = diagnoseConfig(config)\n if (problems.length > 0) {\n throw new Error(invalidConfigMessage(problems, integration))\n }\n if (config.environment !== undefined && config.environment !== null && typeof config.environment !== \"string\") {\n throw new Error(\n `${PREFIX} configure() received environment as a ${typeof config.environment}, not a string. Pass the environment slug as it appears in the console, ex. \"DEV\", or leave it out to let the backend resolve it from the domain of this page.`\n )\n }\n}\n\n/** Notices about a stored configuration that works but probably does not do what the host meant. */\nconst warnAboutConfig = (config: OrchestratorConfig): void => {\n if (typeof config.environment === \"string\" && config.environment.trim() === \"\") {\n console.warn(\n `${PREFIX} configure() received an empty environment, so the client falls back to the auto routes and the backend resolves the environment from the domain of this page. Remove the option if that is what you want, or check the variable that feeds it.`\n )\n }\n}\n\n/**\n * Two shapes, one per route family. With an explicit environment the slug is the last segment; with\n * no environment the \"auto\" segment takes its place before the project id and the backend resolves\n * the environment from the domain the request comes from.\n */\nconst manifestPath = (config: OrchestratorConfig): string => {\n const projectId = encodeURIComponent(config.projectId)\n return config.environment ? `/serve/all/${projectId}/${encodeURIComponent(config.environment)}` : `/serve/all/auto/${projectId}`\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)}${manifestPath(config)}?${query.toString()}`\n}\n\n/**\n * What a status usually means on this route. The backend answers the same 404 for an unknown project\n * and for an unknown environment, and the two are indistinguishable from here, so both are named.\n */\nconst statusHint = (status: number, config: OrchestratorConfig): string => {\n if (status === 404) {\n const environment = config.environment\n ? `the environment \"${config.environment}\" does not exist in it`\n : \"no environment of that project declares the domain this page is served on, which is what the auto route resolves on\"\n return `\\n\\nA 404 on this route means the backend did not find what the URL names: either the project id \"${config.projectId}\" is not a project of this console, or ${environment}. Check both in the console, and check that backendUrl points at the console API of the same instance.`\n }\n if (status === 401 || status === 403) {\n return `\\n\\nThe console refused the request. Check that the project id \"${config.projectId}\" belongs to the instance backendUrl points at, and that this domain is allowed to read its manifest.`\n }\n return \"\"\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}${statusHint(response.status, config)}`)\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 * `backendUrl` and `projectId` are required. `environment` is not: omitting it makes the client use\n * the \"auto\" routes, where the backend resolves the environment from the domain of the host page.\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 const state = getState()\n requireConfig(config, state.integration)\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 warnAboutConfig(config)\n}\n\n/**\n * @internal Lets a framework adapter say which package the host is really using, so a configuration\n * error prints the snippet of that API instead of the bare `configure()` one. Idempotent, and never\n * needed by an application: the adapters call it as they load.\n */\nexport const registerIntegration = (integration: Integration): void => {\n getState().integration = integration\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 */\nconst loadManifest = (caller: string): 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(notConfiguredMessage(caller, state.integration)))\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/** The whole manifest of the environment. See `loadManifest` for the memoization contract. */\nexport const manifest = (): Promise<Manifest> => loadManifest(\"manifest()\")\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 loadManifest(`remoteUrl(\"${slug}\")`)\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 // With no configured environment the host cannot name the one it was served: only the backend knows.\n const where = environment ? `environment \"${environment}\"` : \"the environment resolved from this domain\"\n throw new Error(`${PREFIX} unknown microfrontend slug \"${slug}\". Available in ${where}: ${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 loadManifest(\"globalVariables()\")\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfe-orchestrator-hub/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Framework agnostic client for MFE Orchestrator: resolves version pinned microfrontend URLs from the console manifest",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lorenzo De Francesco",
|