@mfeflow/runtime 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/beacon.d.ts +45 -0
- package/dist/beacon.js +169 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/entry-retry.js +5 -2
- package/dist/plugins/report.d.ts +1 -0
- package/dist/plugins/report.js +8 -1
- package/dist/registry.d.ts +38 -30
- package/dist/registry.js +106 -30
- package/dist/report.d.ts +9 -0
- package/dist/report.js +22 -3
- package/package.json +7 -2
package/dist/beacon.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RegistrySources } from './registry.js';
|
|
2
|
+
import { type RuntimeConfigValues } from './runtime-config.js';
|
|
3
|
+
/** The registry server's endpoint for these index URLs, and the env they name. */
|
|
4
|
+
export interface BeaconTarget {
|
|
5
|
+
url: string;
|
|
6
|
+
env: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const CLIENT_ID_KEY = "mfeflow:client";
|
|
9
|
+
/** The first index URL that is a registry server's `/v1/index/<env>`. A static host has no beacon. */
|
|
10
|
+
export declare function beaconTarget(urls: readonly string[]): BeaconTarget | undefined;
|
|
11
|
+
/** A stable id for this browser, so the server keeps giving it the same rollout side. Not tied to a person. */
|
|
12
|
+
export declare function clientId(): string;
|
|
13
|
+
/** The server index URLs get `?u=<client id>`; any other URL is returned as it came. */
|
|
14
|
+
export declare function withClientId(url: string, id?: string): string;
|
|
15
|
+
export type Send = (url: string, body: string) => boolean;
|
|
16
|
+
export interface BeaconReporter {
|
|
17
|
+
load(remote: string, version: string): void;
|
|
18
|
+
error(remote: string, version: string): void;
|
|
19
|
+
/** Sends what has been counted since the last flush as one request. Nothing counted, nothing sent. */
|
|
20
|
+
flush(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Counts, per remote@version, whether this page loaded it and whether it failed, and sends the
|
|
25
|
+
* page's counts as one request: `{ env, u, entries }`. A page is one load and at most one error
|
|
26
|
+
* for a remote, so the error rate reads as "page views that hit an error". What was sent is
|
|
27
|
+
* remembered, so a flush on `visibilitychange` followed by more loads never reports the same load
|
|
28
|
+
* again. An error that comes after a clean report is sent once more.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createBeaconReporter(target: BeaconTarget, send: Send, client?: string): BeaconReporter;
|
|
31
|
+
/** Stops the running reporter so the next start is a fresh one. For tests. */
|
|
32
|
+
export declare function resetRegistryBeaconForTests(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Starts the reporter when the runtime config has `registryBeacon: true` and one of the index URLs
|
|
35
|
+
* is a registry server's. Otherwise does nothing. Calling it again returns the reporter already
|
|
36
|
+
* running, so the Module Federation plugin is registered once.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startRegistryBeacon(config: RuntimeConfigValues, urls: readonly string[], send?: Send): BeaconReporter | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* `registrySources()` for a shell that wants the beacon. With `registryBeacon: true` in the runtime
|
|
41
|
+
* config it adds this browser's id to the registry server's index URLs and starts the reporter;
|
|
42
|
+
* without it, the sources come back untouched. Import it on demand
|
|
43
|
+
* (`await import('@mfeflow/runtime/beacon')`) so a shell with the option off never downloads it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function registrySourcesWithBeacon(waitMs?: number): Promise<RegistrySources>;
|
package/dist/beacon.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { registerPlugins } from "@module-federation/enhanced/runtime";
|
|
2
|
+
import { REMOTE_ERROR_EVENT, remoteVersion } from "./report.js";
|
|
3
|
+
import { registrySources } from "./registry.js";
|
|
4
|
+
import { RUNTIME_CONFIG_KEY } from "./runtime-config.js";
|
|
5
|
+
const SERVER_INDEX = /\/v1\/index\/([a-z][a-z0-9-]*)$/;
|
|
6
|
+
const PARSE_BASE = 'https://page.invalid';
|
|
7
|
+
const MAX_ENTRIES = 20;
|
|
8
|
+
const MAX_BYTES = 4096;
|
|
9
|
+
const MAX_PENDING = 200;
|
|
10
|
+
const CLIENT_ID_KEY = 'mfeflow:client';
|
|
11
|
+
function beaconTarget(urls) {
|
|
12
|
+
for (const raw of urls){
|
|
13
|
+
if (!URL.canParse(raw, PARSE_BASE)) continue;
|
|
14
|
+
const url = new URL(raw, PARSE_BASE);
|
|
15
|
+
const env = SERVER_INDEX.exec(url.pathname)?.[1];
|
|
16
|
+
if (void 0 === env) continue;
|
|
17
|
+
const beacon = new URL(raw, "u" < typeof location ? PARSE_BASE : location.href);
|
|
18
|
+
beacon.pathname = beacon.pathname.replace(SERVER_INDEX, '/v1/beacon');
|
|
19
|
+
beacon.search = '';
|
|
20
|
+
beacon.hash = '';
|
|
21
|
+
return {
|
|
22
|
+
url: beacon.href,
|
|
23
|
+
env
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const randomId = ()=>Array.from(crypto.getRandomValues(new Uint8Array(16)), (b)=>b.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
let memoryId;
|
|
29
|
+
function clientId() {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(CLIENT_ID_KEY);
|
|
32
|
+
if (null !== stored && /^[\w.:-]{1,128}$/.test(stored)) return stored;
|
|
33
|
+
const fresh = randomId();
|
|
34
|
+
localStorage.setItem(CLIENT_ID_KEY, fresh);
|
|
35
|
+
return fresh;
|
|
36
|
+
} catch {
|
|
37
|
+
memoryId ??= randomId();
|
|
38
|
+
return memoryId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function withClientId(url, id = clientId()) {
|
|
42
|
+
if (!URL.canParse(url, PARSE_BASE)) return url;
|
|
43
|
+
const parsed = new URL(url, PARSE_BASE);
|
|
44
|
+
if (!SERVER_INDEX.test(parsed.pathname)) return url;
|
|
45
|
+
const absolute = URL.canParse(url);
|
|
46
|
+
const target = absolute ? new URL(url) : parsed;
|
|
47
|
+
target.searchParams.set('u', id);
|
|
48
|
+
return absolute ? target.href : `${target.pathname}${target.search}${target.hash}`;
|
|
49
|
+
}
|
|
50
|
+
const byteLength = (text)=>new TextEncoder().encode(text).length;
|
|
51
|
+
function createBeaconReporter(target, send, client = clientId()) {
|
|
52
|
+
const pending = new Map();
|
|
53
|
+
const sent = new Map();
|
|
54
|
+
const note = (remote, version, failed)=>{
|
|
55
|
+
const key = `${remote}@${version}`;
|
|
56
|
+
const before = sent.get(key);
|
|
57
|
+
if (void 0 !== before && (before || !failed)) return;
|
|
58
|
+
const known = pending.get(key);
|
|
59
|
+
if (void 0 !== known) known.failed ||= failed;
|
|
60
|
+
else if (pending.size < MAX_PENDING) pending.set(key, {
|
|
61
|
+
remote,
|
|
62
|
+
version,
|
|
63
|
+
failed
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const flush = ()=>{
|
|
67
|
+
if (0 === pending.size) return;
|
|
68
|
+
const batch = [
|
|
69
|
+
...pending.entries()
|
|
70
|
+
];
|
|
71
|
+
pending.clear();
|
|
72
|
+
for (const [key, { failed }] of batch)sent.set(key, failed);
|
|
73
|
+
const head = JSON.stringify({
|
|
74
|
+
env: target.env,
|
|
75
|
+
u: client,
|
|
76
|
+
entries: []
|
|
77
|
+
});
|
|
78
|
+
let chunk = [];
|
|
79
|
+
let size = head.length;
|
|
80
|
+
const emit = ()=>{
|
|
81
|
+
if (chunk.length > 0) send(target.url, JSON.stringify({
|
|
82
|
+
env: target.env,
|
|
83
|
+
u: client,
|
|
84
|
+
entries: chunk
|
|
85
|
+
}));
|
|
86
|
+
chunk = [];
|
|
87
|
+
size = head.length;
|
|
88
|
+
};
|
|
89
|
+
for (const [, { remote, version, failed }] of batch){
|
|
90
|
+
const entry = {
|
|
91
|
+
remote,
|
|
92
|
+
version,
|
|
93
|
+
loads: 1,
|
|
94
|
+
errors: failed ? 1 : 0
|
|
95
|
+
};
|
|
96
|
+
const extra = byteLength(JSON.stringify(entry)) + 1;
|
|
97
|
+
if (chunk.length === MAX_ENTRIES || size + extra > MAX_BYTES) emit();
|
|
98
|
+
chunk.push(entry);
|
|
99
|
+
size += extra;
|
|
100
|
+
}
|
|
101
|
+
emit();
|
|
102
|
+
};
|
|
103
|
+
const onError = (event)=>{
|
|
104
|
+
const detail = event instanceof CustomEvent ? event.detail : void 0;
|
|
105
|
+
const mfe = 'object' == typeof detail && null !== detail ? Reflect.get(detail, 'mfe') : void 0;
|
|
106
|
+
const version = 'object' == typeof detail && null !== detail ? Reflect.get(detail, 'version') : void 0;
|
|
107
|
+
if ('string' == typeof mfe && 'string' == typeof version) note(mfe, version, true);
|
|
108
|
+
};
|
|
109
|
+
const onHide = ()=>{
|
|
110
|
+
if ("u" < typeof document || 'hidden' === document.visibilityState) flush();
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener(REMOTE_ERROR_EVENT, onError);
|
|
113
|
+
window.addEventListener('pagehide', flush);
|
|
114
|
+
document.addEventListener('visibilitychange', onHide);
|
|
115
|
+
return {
|
|
116
|
+
load: (remote, version)=>note(remote, version, false),
|
|
117
|
+
error: (remote, version)=>note(remote, version, true),
|
|
118
|
+
flush,
|
|
119
|
+
stop () {
|
|
120
|
+
window.removeEventListener(REMOTE_ERROR_EVENT, onError);
|
|
121
|
+
window.removeEventListener('pagehide', flush);
|
|
122
|
+
document.removeEventListener('visibilitychange', onHide);
|
|
123
|
+
pending.clear();
|
|
124
|
+
sent.clear();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const sendWithBeacon = (url, body)=>"u" > typeof navigator && 'function' == typeof navigator.sendBeacon ? navigator.sendBeacon(url, body) : false;
|
|
129
|
+
let active;
|
|
130
|
+
function resetRegistryBeaconForTests() {
|
|
131
|
+
active?.stop();
|
|
132
|
+
active = void 0;
|
|
133
|
+
}
|
|
134
|
+
function startRegistryBeacon(config, urls, send = sendWithBeacon) {
|
|
135
|
+
if (true !== config.registryBeacon) return;
|
|
136
|
+
if (void 0 !== active) return active;
|
|
137
|
+
const target = beaconTarget(urls);
|
|
138
|
+
if (void 0 === target) return;
|
|
139
|
+
const reporter = createBeaconReporter(target, send);
|
|
140
|
+
active = reporter;
|
|
141
|
+
registerPlugins([
|
|
142
|
+
{
|
|
143
|
+
name: 'mfeflow-beacon',
|
|
144
|
+
onLoad (args) {
|
|
145
|
+
const remote = args.id.split('/')[0] ?? args.id;
|
|
146
|
+
const version = remoteVersion(remote);
|
|
147
|
+
if (void 0 !== version) reporter.load(remote, version);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
return reporter;
|
|
152
|
+
}
|
|
153
|
+
const isConfig = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
154
|
+
async function registrySourcesWithBeacon(waitMs) {
|
|
155
|
+
const sources = await registrySources(waitMs);
|
|
156
|
+
const config = Reflect.get(globalThis, RUNTIME_CONFIG_KEY);
|
|
157
|
+
if (!isConfig(config) || true !== config.registryBeacon) return sources;
|
|
158
|
+
try {
|
|
159
|
+
const urls = sources.urls.map((url)=>withClientId(url));
|
|
160
|
+
startRegistryBeacon(config, urls);
|
|
161
|
+
return {
|
|
162
|
+
...sources,
|
|
163
|
+
urls
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return sources;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export { CLIENT_ID_KEY, beaconTarget, clientId, createBeaconReporter, registrySourcesWithBeacon, resetRegistryBeaconForTests, startRegistryBeacon, withClientId };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { composeProviders, withProps, type ProviderComponent } from './providers.js';
|
|
2
2
|
export { REMOTE_ERROR_EVENT, reportRemoteError, type RemoteErrorReport } from './report.js';
|
|
3
|
-
export { DEFAULT_REGISTRY_URL, isRegistryIndex, loadRegistryIndex, REGISTRY_GLOBAL, registeredRemotes, registerRemotesFromIndex, type LoadRegistryOptions, type RegisterOptions, type RegistryIndex, type RegistryRemote, } from './registry.js';
|
|
3
|
+
export { DEFAULT_REGISTRY_URL, isRegistryIndex, loadRegistryIndex, REGISTRY_GLOBAL, REGISTRY_FALLBACK, registeredRemotes, registrySources, registrySourcesFromConfig, type RegistrySources, registerRemotesFromIndex, type LoadRegistryOptions, type RegisterOptions, type RegistryIndex, type RegistryRemote, } from './registry.js';
|
|
4
4
|
export { DefaultRemoteFallback, RemoteBoundary, remoteComponent, type FallbackProps, type RemoteBoundaryProps, type RemoteOptions, } from './remote.js';
|
|
5
5
|
export { ASSERT_PARAM, assertRequested, checkSingletons, type SingletonCheck } from './singletons.js';
|
|
6
6
|
export { applyDevOverrides, OVERRIDE_PARAM, OVERRIDE_STORAGE_KEY, type DevOverrideOptions, } from './overrides.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { composeProviders, withProps } from "./providers.js";
|
|
2
2
|
export { REMOTE_ERROR_EVENT, reportRemoteError } from "./report.js";
|
|
3
|
-
export { DEFAULT_REGISTRY_URL, REGISTRY_GLOBAL, isRegistryIndex, loadRegistryIndex, registerRemotesFromIndex, registeredRemotes } from "./registry.js";
|
|
3
|
+
export { DEFAULT_REGISTRY_URL, REGISTRY_FALLBACK, REGISTRY_GLOBAL, isRegistryIndex, loadRegistryIndex, registerRemotesFromIndex, registeredRemotes, registrySources, registrySourcesFromConfig } from "./registry.js";
|
|
4
4
|
export { DefaultRemoteFallback, RemoteBoundary, remoteComponent } from "./remote.js";
|
|
5
5
|
export { ASSERT_PARAM, assertRequested, checkSingletons } from "./singletons.js";
|
|
6
6
|
export { OVERRIDE_PARAM, OVERRIDE_STORAGE_KEY, applyDevOverrides } from "./overrides.js";
|
|
@@ -15,16 +15,19 @@ function createEntryRetryPlugin(options = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
await sleep(delayMs * attempt);
|
|
17
17
|
if (globalLoading[uniqueKey] === failed) delete globalLoading[uniqueKey];
|
|
18
|
+
const before = globalLoading[uniqueKey];
|
|
18
19
|
try {
|
|
19
|
-
const
|
|
20
|
+
const attemptLoad = getRemoteEntry({
|
|
20
21
|
origin,
|
|
21
22
|
remoteInfo,
|
|
22
23
|
remoteEntryExports,
|
|
23
24
|
getEntryUrl: (url)=>`${url}${url.includes('?') ? '&' : '?'}retry=${String(attempt)}`
|
|
24
25
|
});
|
|
26
|
+
const started = globalLoading[uniqueKey];
|
|
27
|
+
failed = started === before ? void 0 : started;
|
|
28
|
+
const exports = await attemptLoad;
|
|
25
29
|
if (exports) return exports;
|
|
26
30
|
} catch {}
|
|
27
|
-
failed = globalLoading[uniqueKey];
|
|
28
31
|
}
|
|
29
32
|
return;
|
|
30
33
|
} finally{
|
package/dist/plugins/report.d.ts
CHANGED
package/dist/plugins/report.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
const EVENT = 'mfeflow:remote-error';
|
|
2
|
+
const VERSIONS = Symbol.for('mfeflow.remote-versions');
|
|
2
3
|
function mfeflowReportPlugin() {
|
|
3
4
|
return {
|
|
4
5
|
name: 'mfeflow-report',
|
|
5
6
|
errorLoadRemote (args) {
|
|
6
7
|
const mfe = args.id.split('/')[0] ?? args.id;
|
|
8
|
+
const versions = Reflect.get(globalThis, VERSIONS);
|
|
9
|
+
const known = versions instanceof Map ? versions.get(mfe) : void 0;
|
|
10
|
+
const version = 'string' == typeof known ? known : void 0;
|
|
7
11
|
const report = {
|
|
8
12
|
mfe,
|
|
9
13
|
where: `errorLoadRemote:${args.lifecycle}`,
|
|
10
14
|
message: args.error instanceof Error ? args.error.message : String(args.error),
|
|
11
|
-
time: Date.now()
|
|
15
|
+
time: Date.now(),
|
|
16
|
+
...void 0 === version ? {} : {
|
|
17
|
+
version
|
|
18
|
+
}
|
|
12
19
|
};
|
|
13
20
|
(globalThis.mfeflowReports ??= []).push(report);
|
|
14
21
|
if ("u" > typeof window && 'function' == typeof CustomEvent) window.dispatchEvent(new CustomEvent(EVENT, {
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
entry: string;
|
|
6
|
-
/**
|
|
7
|
-
* The remote's entry script. Registered in place of the manifest in
|
|
8
|
-
* production, so the shell fetches no manifest and ships no snapshot code.
|
|
9
|
-
*/
|
|
10
|
-
remoteEntry?: string;
|
|
11
|
-
/** How `remoteEntry` loads (`module` for ESM). Passed to Module Federation with it. */
|
|
12
|
-
type?: string;
|
|
13
|
-
/** The global `remoteEntry` defines when it differs from `name`. */
|
|
14
|
-
entryGlobalName?: string;
|
|
15
|
-
version?: string;
|
|
16
|
-
routes?: string[];
|
|
17
|
-
owner?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface RegistryIndex {
|
|
20
|
-
shell?: string;
|
|
21
|
-
env?: string;
|
|
22
|
-
generatedAt?: string;
|
|
23
|
-
remotes: RegistryRemote[];
|
|
24
|
-
}
|
|
1
|
+
import { isRegistryIndexRemote, type RegistryIndex, type RegistryIndexRemote } from '@mfeflow/contracts/entry';
|
|
2
|
+
import { type RuntimeConfigValues } from './runtime-config.js';
|
|
3
|
+
export type RegistryRemote = RegistryIndexRemote;
|
|
4
|
+
export type { RegistryIndex };
|
|
25
5
|
/** Reported for a registry entry that has only a manifest URL in a shell built without the manifest loader. */
|
|
26
6
|
export declare const NO_REMOTE_ENTRY = "MFE_NO_REMOTE_ENTRY";
|
|
27
7
|
/** True for a valid registry remote. */
|
|
28
|
-
export declare
|
|
8
|
+
export declare const isRegistryRemote: typeof isRegistryIndexRemote;
|
|
29
9
|
/**
|
|
30
10
|
* True for a registry index envelope (what the shell fetches at boot). Remotes
|
|
31
11
|
* are checked one by one in `loadRegistryIndex`, so one bad remote does not
|
|
@@ -35,11 +15,16 @@ export declare function isRegistryIndex(value: unknown): value is RegistryIndex;
|
|
|
35
15
|
/** Reported for a registry remote that is skipped because a field is invalid. */
|
|
36
16
|
export declare const INVALID_REMOTE = "MFE_INVALID_REMOTE";
|
|
37
17
|
export interface LoadRegistryOptions {
|
|
18
|
+
/** The whole time budget for all URLs together, in ms. Default 5000. */
|
|
38
19
|
timeoutMs?: number;
|
|
20
|
+
/** How long each URL except the last gets, in ms. The last one gets what is left. Default 1500. */
|
|
21
|
+
primaryTimeoutMs?: number;
|
|
39
22
|
fetch?: typeof fetch;
|
|
40
23
|
/** Use an index the server or edge inlined as `window.__MFE_REGISTRY__`. Default true. */
|
|
41
24
|
inline?: boolean;
|
|
42
|
-
/**
|
|
25
|
+
/** Origins other than the page's that an index URL may be on. */
|
|
26
|
+
allowedOrigins?: readonly string[];
|
|
27
|
+
/** Allow any origin for the index URLs. Default false; prefer `allowedOrigins`. */
|
|
43
28
|
allowCrossOrigin?: boolean;
|
|
44
29
|
}
|
|
45
30
|
/**
|
|
@@ -49,13 +34,36 @@ export interface LoadRegistryOptions {
|
|
|
49
34
|
*/
|
|
50
35
|
export declare const REGISTRY_GLOBAL = "__MFE_REGISTRY__";
|
|
51
36
|
export declare const DEFAULT_REGISTRY_URL = "/registry/index.json";
|
|
37
|
+
/** Reported once when the primary index failed and a later URL served the shell. */
|
|
38
|
+
export declare const REGISTRY_FALLBACK = "MFE_REGISTRY_FALLBACK";
|
|
52
39
|
/**
|
|
53
40
|
* Reads the registry index: `window.__MFE_REGISTRY__` when the edge inlined it,
|
|
54
|
-
* otherwise
|
|
55
|
-
*
|
|
56
|
-
*
|
|
41
|
+
* otherwise the first of `source` (one URL or an ordered list, primary first)
|
|
42
|
+
* that answers. A 5xx, network error, timeout, non-JSON body, invalid envelope
|
|
43
|
+
* or disallowed origin moves on to the next URL; each URL except the last gets
|
|
44
|
+
* `primaryTimeoutMs`, the last gets the rest of `timeoutMs`. Each URL must be on
|
|
45
|
+
* the page's origin or in `allowedOrigins`. Falling back is reported once
|
|
46
|
+
* (`MFE_REGISTRY_FALLBACK`). Throws when every URL fails. A remote with an
|
|
47
|
+
* invalid field is skipped and reported (`MFE_INVALID_REMOTE`); the valid ones stay.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadRegistryIndex(source?: string | readonly string[], options?: LoadRegistryOptions): Promise<RegistryIndex>;
|
|
50
|
+
/**
|
|
51
|
+
* The index URLs and extra allowed origins from the shell's runtime config:
|
|
52
|
+
* `registryUrls` (primary first) and `registryOrigins`. Without them it is the
|
|
53
|
+
* default same-origin URL and no extra origins.
|
|
54
|
+
*/
|
|
55
|
+
export declare function registrySourcesFromConfig(config: RuntimeConfigValues): RegistrySources;
|
|
56
|
+
export interface RegistrySources {
|
|
57
|
+
urls: string[];
|
|
58
|
+
allowedOrigins: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Where the shell loads the registry from, without holding up boot. An inlined runtime config is
|
|
62
|
+
* used as is. When the index is inlined nothing is fetched (inline the runtime config too if its
|
|
63
|
+
* remotes live on another origin). Otherwise the config file is used if it answers within
|
|
64
|
+
* `waitMs`; past that, or when it fails, the same-origin default.
|
|
57
65
|
*/
|
|
58
|
-
export declare function
|
|
66
|
+
export declare function registrySources(waitMs?: number): Promise<RegistrySources>;
|
|
59
67
|
/** Remotes registered by the last `registerRemotesFromIndex` call (for the dev overlay). */
|
|
60
68
|
export declare function registeredRemotes(): readonly RegistryRemote[];
|
|
61
69
|
export interface RegisterOptions {
|
package/dist/registry.js
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRegistryIndexRemote, pickRegistryFields, registryRemoteIssues } from "@mfeflow/contracts/entry";
|
|
2
2
|
import { registerRemotes } from "@module-federation/enhanced/runtime";
|
|
3
|
-
import { reportRemoteError } from "./report.js";
|
|
3
|
+
import { rememberRemoteVersions, reportRemoteError } from "./report.js";
|
|
4
|
+
import { RUNTIME_CONFIG_KEY, fetchRuntimeConfig } from "./runtime-config.js";
|
|
4
5
|
const NO_REMOTE_ENTRY = 'MFE_NO_REMOTE_ENTRY';
|
|
5
6
|
function builtWithSnapshot() {
|
|
6
7
|
return 'boolean' == typeof __MFEFLOW_SNAPSHOT__ ? __MFEFLOW_SNAPSHOT__ : true;
|
|
7
8
|
}
|
|
8
|
-
function isStringArray(value) {
|
|
9
|
-
return Array.isArray(value) && value.every((v)=>'string' == typeof v);
|
|
10
|
-
}
|
|
11
9
|
function remoteProblem(value) {
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
if ('remoteEntry' in value && 'string' != typeof value.remoteEntry) return 'remoteEntry';
|
|
16
|
-
if ('type' in value && ('string' != typeof value.type || !ENTRY_TYPES.includes(value.type))) return 'type';
|
|
17
|
-
if ('entryGlobalName' in value && ('string' != typeof value.entryGlobalName || !GLOBAL_NAME.test(value.entryGlobalName))) return 'entryGlobalName';
|
|
18
|
-
if ('version' in value && 'string' != typeof value.version) return 'version';
|
|
19
|
-
if ('owner' in value && 'string' != typeof value.owner) return 'owner';
|
|
20
|
-
if ('routes' in value && !isStringArray(value.routes)) return 'routes';
|
|
21
|
-
}
|
|
22
|
-
function isRegistryRemote(value) {
|
|
23
|
-
return void 0 === remoteProblem(value);
|
|
10
|
+
const first = registryRemoteIssues(value).find((issue)=>!issue.path.startsWith('exposes'));
|
|
11
|
+
if (void 0 === first) return;
|
|
12
|
+
return '' === first.path ? 'not an object' : first.path.split(/[.[]/)[0] ?? first.path;
|
|
24
13
|
}
|
|
14
|
+
const isRegistryRemote = isRegistryIndexRemote;
|
|
25
15
|
function isRegistryIndex(value) {
|
|
26
16
|
return 'object' == typeof value && null !== value && 'remotes' in value && Array.isArray(value.remotes);
|
|
27
17
|
}
|
|
@@ -41,22 +31,107 @@ function withoutInvalidRemotes(index) {
|
|
|
41
31
|
}
|
|
42
32
|
const REGISTRY_GLOBAL = '__MFE_REGISTRY__';
|
|
43
33
|
const DEFAULT_REGISTRY_URL = '/registry/index.json';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
const REGISTRY_FALLBACK = 'MFE_REGISTRY_FALLBACK';
|
|
35
|
+
function originOf(url) {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(url, "u" < typeof location ? void 0 : location.href).origin;
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
42
|
+
function originAllowed(url, options) {
|
|
43
|
+
if (true === options.allowCrossOrigin) return true;
|
|
44
|
+
const origin = originOf(url);
|
|
45
|
+
if (void 0 === origin) return false;
|
|
46
|
+
if ("u" < typeof location || origin === location.origin) return true;
|
|
47
|
+
return (options.allowedOrigins ?? []).some((allowed)=>originOf(allowed) === origin);
|
|
48
|
+
}
|
|
49
|
+
async function fetchIndex(url, timeoutMs, options) {
|
|
50
|
+
if (!originAllowed(url, options)) throw new Error(`registry index ${url}: must be same-origin or in allowedOrigins`);
|
|
52
51
|
const doFetch = options.fetch ?? fetch;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await doFetch(url, {
|
|
56
|
+
signal
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (signal.aborted) throw new Error(`registry index ${url}: timed out after ${String(timeoutMs)} ms`, {
|
|
60
|
+
cause: error
|
|
61
|
+
});
|
|
62
|
+
throw new Error(`registry index ${url}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
63
|
+
cause: error
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (res.redirected && !originAllowed(res.url, options)) throw new Error(`registry index ${url}: redirected to ${res.url}, which is not allowed`);
|
|
56
67
|
if (!res.ok) throw new Error(`registry index ${url}: HTTP ${String(res.status)}`);
|
|
57
|
-
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = await res.json();
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error(`registry index ${url}: not valid JSON`);
|
|
73
|
+
}
|
|
58
74
|
if (!isRegistryIndex(body)) throw new Error(`registry index ${url}: not a valid index`);
|
|
59
|
-
return
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
async function loadRegistryIndex(source = DEFAULT_REGISTRY_URL, options = {}) {
|
|
78
|
+
const inlined = Reflect.get(globalThis, REGISTRY_GLOBAL);
|
|
79
|
+
if (false !== options.inline && isRegistryIndex(inlined)) return withoutInvalidRemotes(inlined);
|
|
80
|
+
const urls = 'string' == typeof source ? [
|
|
81
|
+
source
|
|
82
|
+
] : source;
|
|
83
|
+
if (0 === urls.length) throw new Error('registry index: no URL given');
|
|
84
|
+
const budget = options.timeoutMs ?? 5000;
|
|
85
|
+
const primaryMs = options.primaryTimeoutMs ?? 1500;
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const failures = [];
|
|
88
|
+
for (const [position, url] of urls.entries()){
|
|
89
|
+
const remaining = budget - (Date.now() - started);
|
|
90
|
+
if (remaining <= 0) {
|
|
91
|
+
failures.push(new Error(`registry index ${url}: no time left`));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const timeoutMs = position === urls.length - 1 ? remaining : Math.min(primaryMs, remaining);
|
|
95
|
+
try {
|
|
96
|
+
const index = await fetchIndex(url, timeoutMs, options);
|
|
97
|
+
const [first] = failures;
|
|
98
|
+
if (void 0 !== first) reportRemoteError('registry', 'registry', new Error(`${REGISTRY_FALLBACK}: ${first.message}; using ${url}`));
|
|
99
|
+
return withoutInvalidRemotes(index);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const [only] = failures;
|
|
105
|
+
if (1 === failures.length && void 0 !== only) throw only;
|
|
106
|
+
throw new Error(`registry index: every source failed (${failures.map((e)=>e.message).join('; ')})`);
|
|
107
|
+
}
|
|
108
|
+
const strings = (value)=>Array.isArray(value) ? value.filter((v)=>'string' == typeof v && '' !== v) : [];
|
|
109
|
+
function registrySourcesFromConfig(config) {
|
|
110
|
+
const urls = strings(config.registryUrls);
|
|
111
|
+
const allowedOrigins = strings(config.registryOrigins).flatMap((o)=>{
|
|
112
|
+
const url = URL.canParse(o) ? new URL(o) : void 0;
|
|
113
|
+
return url?.protocol === 'https:' || url?.protocol === 'http:' ? url.origin : [];
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
urls: urls.length > 0 ? urls : [
|
|
117
|
+
DEFAULT_REGISTRY_URL
|
|
118
|
+
],
|
|
119
|
+
allowedOrigins
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function registrySources(waitMs = 1500) {
|
|
123
|
+
const configInlined = Reflect.has(globalThis, RUNTIME_CONFIG_KEY);
|
|
124
|
+
if (!configInlined && isRegistryIndex(Reflect.get(globalThis, REGISTRY_GLOBAL))) return registrySourcesFromConfig({});
|
|
125
|
+
let timer;
|
|
126
|
+
const late = new Promise((resolve)=>{
|
|
127
|
+
timer = setTimeout(()=>resolve({}), waitMs);
|
|
128
|
+
});
|
|
129
|
+
const config = await Promise.race([
|
|
130
|
+
fetchRuntimeConfig().catch(()=>({})),
|
|
131
|
+
late
|
|
132
|
+
]);
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
return registrySourcesFromConfig(config);
|
|
60
135
|
}
|
|
61
136
|
function toMfRemote(r) {
|
|
62
137
|
if (void 0 === r.remoteEntry) return {
|
|
@@ -102,7 +177,8 @@ function registerRemotesFromIndex(index, options = {}) {
|
|
|
102
177
|
ok.push(remote);
|
|
103
178
|
}
|
|
104
179
|
registerRemotes(ok.map(toMfRemote));
|
|
180
|
+
rememberRemoteVersions(ok);
|
|
105
181
|
registered = ok.map((r)=>pickRegistryFields(r));
|
|
106
182
|
return ok.map((r)=>r.name);
|
|
107
183
|
}
|
|
108
|
-
export { DEFAULT_REGISTRY_URL, INVALID_REMOTE, NO_REMOTE_ENTRY, REGISTRY_GLOBAL, isRegistryIndex, isRegistryRemote, loadRegistryIndex, registerRemotesFromIndex, registeredRemotes };
|
|
184
|
+
export { DEFAULT_REGISTRY_URL, INVALID_REMOTE, NO_REMOTE_ENTRY, REGISTRY_FALLBACK, REGISTRY_GLOBAL, isRegistryIndex, isRegistryRemote, loadRegistryIndex, registerRemotesFromIndex, registeredRemotes, registrySources, registrySourcesFromConfig };
|
package/dist/report.d.ts
CHANGED
|
@@ -5,7 +5,16 @@ export interface RemoteErrorReport {
|
|
|
5
5
|
where: string;
|
|
6
6
|
message: string;
|
|
7
7
|
time: number;
|
|
8
|
+
/** The registered version of the remote, when the registry gave one. */
|
|
9
|
+
version?: string;
|
|
8
10
|
}
|
|
11
|
+
/** The version the registry gave this remote on this page, if any. */
|
|
12
|
+
export declare const remoteVersion: (mfe: string) => string | undefined;
|
|
13
|
+
/** Remembers each remote's version so its reports say `cart@1.2.3`. Replaces what was remembered before. */
|
|
14
|
+
export declare function rememberRemoteVersions(remotes: readonly {
|
|
15
|
+
name: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
}[]): void;
|
|
9
18
|
/** Fired on `window` for every report; telemetry and the dev overlay listen to it. */
|
|
10
19
|
export declare const REMOTE_ERROR_EVENT = "mfeflow:remote-error";
|
|
11
20
|
declare global {
|
package/dist/report.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
+
const VERSIONS = Symbol.for('mfeflow.remote-versions');
|
|
2
|
+
const isVersionMap = (value)=>value instanceof Map;
|
|
3
|
+
function versionMap() {
|
|
4
|
+
const existing = Reflect.get(globalThis, VERSIONS);
|
|
5
|
+
if (isVersionMap(existing)) return existing;
|
|
6
|
+
const created = new Map();
|
|
7
|
+
Reflect.set(globalThis, VERSIONS, created);
|
|
8
|
+
return created;
|
|
9
|
+
}
|
|
10
|
+
const remoteVersion = (mfe)=>versionMap().get(mfe);
|
|
11
|
+
function rememberRemoteVersions(remotes) {
|
|
12
|
+
const versions = versionMap();
|
|
13
|
+
versions.clear();
|
|
14
|
+
for (const { name, version } of remotes)if (void 0 !== version) versions.set(name, version);
|
|
15
|
+
}
|
|
1
16
|
const REMOTE_ERROR_EVENT = 'mfeflow:remote-error';
|
|
2
17
|
function reportRemoteError(mfe, where, error) {
|
|
18
|
+
const version = versionMap().get(mfe);
|
|
3
19
|
const report = {
|
|
4
20
|
mfe,
|
|
5
21
|
where,
|
|
6
22
|
message: error instanceof Error ? error.message : String(error),
|
|
7
|
-
time: Date.now()
|
|
23
|
+
time: Date.now(),
|
|
24
|
+
...void 0 === version ? {} : {
|
|
25
|
+
version
|
|
26
|
+
}
|
|
8
27
|
};
|
|
9
28
|
(globalThis.mfeflowReports ??= []).push(report);
|
|
10
29
|
if ("u" > typeof window && 'function' == typeof CustomEvent) window.dispatchEvent(new CustomEvent(REMOTE_ERROR_EVENT, {
|
|
11
30
|
detail: report
|
|
12
31
|
}));
|
|
13
|
-
console.error(`[mfeflow] ${mfe} ${where}: ${report.message}`);
|
|
32
|
+
console.error(`[mfeflow] ${void 0 === version ? mfe : `${mfe}@${version}`} ${where}: ${report.message}`);
|
|
14
33
|
return report;
|
|
15
34
|
}
|
|
16
|
-
export { REMOTE_ERROR_EVENT, reportRemoteError };
|
|
35
|
+
export { REMOTE_ERROR_EVENT, rememberRemoteVersions, remoteVersion, reportRemoteError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Runtime for mfeflow micro-frontends: remotes from a registry index, RemoteBoundary with timeout and fallback, error reporting, singleton checks, composeProviders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/providers.js",
|
|
34
34
|
"default": "./dist/providers.js"
|
|
35
35
|
},
|
|
36
|
+
"./beacon": {
|
|
37
|
+
"types": "./dist/beacon.d.ts",
|
|
38
|
+
"import": "./dist/beacon.js",
|
|
39
|
+
"default": "./dist/beacon.js"
|
|
40
|
+
},
|
|
36
41
|
"./plugins/report": {
|
|
37
42
|
"types": "./dist/plugins/report.d.ts",
|
|
38
43
|
"import": "./dist/plugins/report.js",
|
|
@@ -55,7 +60,7 @@
|
|
|
55
60
|
"provenance": true
|
|
56
61
|
},
|
|
57
62
|
"dependencies": {
|
|
58
|
-
"@mfeflow/contracts": "0.
|
|
63
|
+
"@mfeflow/contracts": "0.6.0"
|
|
59
64
|
},
|
|
60
65
|
"peerDependencies": {
|
|
61
66
|
"@module-federation/enhanced": "^2.9.0",
|