@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.1
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 +21 -4
- package/dist/cjs/backend-federation/edge.js +1 -1
- package/dist/cjs/backend-federation/identity.js +23 -22
- package/dist/cjs/backend-federation/integrity-plugin.js +112 -0
- package/dist/cjs/backend-federation/node-shared.js +85 -0
- package/dist/cjs/backend-federation/node.js +6 -1
- package/dist/cjs/backend-federation/plugin.js +40 -21
- package/dist/cjs/backend-federation/runtime.js +16 -84
- package/dist/cjs/backend-federation-manifest/load.js +31 -5
- package/dist/cjs/backend-federation-manifest/node.js +3 -1
- package/dist/cjs/backend-federation-manifest/remote.js +1 -7
- package/dist/cjs/backend-federation-manifest/validation.js +11 -7
- package/dist/cjs/cross-project-policy/node.js +8 -28
- package/dist/cjs/{effect-adapter → effect-server}/cross-project-policy.js +9 -9
- package/dist/cjs/effect-server/entry.js +63 -0
- package/dist/cjs/{effect-adapter → effect-server}/error-response.js +4 -4
- package/dist/cjs/effect-server/index.js +199 -0
- package/dist/cjs/effect-source-loader/built-entry.js +4 -5
- package/dist/cjs/effect-source-loader/http-api-runtime.js +4 -4
- package/dist/cjs/effect-source-loader/loader.js +8 -2
- package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
- package/dist/cjs/effect-source-loader/paths.js +38 -10
- package/dist/esm/backend-federation/edge.mjs +1 -1
- package/dist/esm/backend-federation/identity.mjs +18 -23
- package/dist/esm/backend-federation/integrity-plugin.mjs +68 -0
- package/dist/esm/backend-federation/node-shared.mjs +41 -0
- package/dist/esm/backend-federation/node.mjs +4 -2
- package/dist/esm/backend-federation/plugin.mjs +37 -18
- package/dist/esm/backend-federation/runtime.mjs +16 -84
- package/dist/esm/backend-federation-manifest/load.mjs +31 -5
- package/dist/esm/backend-federation-manifest/node.mjs +3 -1
- package/dist/esm/backend-federation-manifest/remote.mjs +1 -7
- package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm/cross-project-policy/node.mjs +9 -19
- package/dist/esm/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
- package/dist/esm/effect-server/entry.mjs +15 -0
- package/dist/esm/{effect-adapter → effect-server}/error-response.mjs +2 -2
- package/dist/esm/effect-server/index.mjs +151 -0
- package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm/effect-source-loader/http-api-runtime.mjs +1 -1
- package/dist/esm/effect-source-loader/loader.mjs +8 -2
- package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm/effect-source-loader/paths.mjs +30 -11
- package/dist/esm-node/backend-federation/edge.mjs +1 -1
- package/dist/esm-node/backend-federation/identity.mjs +18 -23
- package/dist/esm-node/backend-federation/integrity-plugin.mjs +69 -0
- package/dist/esm-node/backend-federation/node-shared.mjs +42 -0
- package/dist/esm-node/backend-federation/node.mjs +4 -2
- package/dist/esm-node/backend-federation/plugin.mjs +37 -18
- package/dist/esm-node/backend-federation/runtime.mjs +16 -84
- package/dist/esm-node/backend-federation-manifest/load.mjs +31 -5
- package/dist/esm-node/backend-federation-manifest/node.mjs +3 -1
- package/dist/esm-node/backend-federation-manifest/remote.mjs +1 -7
- package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm-node/cross-project-policy/node.mjs +9 -19
- package/dist/esm-node/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
- package/dist/esm-node/effect-server/entry.mjs +16 -0
- package/dist/esm-node/{effect-adapter → effect-server}/error-response.mjs +2 -2
- package/dist/esm-node/effect-server/index.mjs +152 -0
- package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm-node/effect-source-loader/http-api-runtime.mjs +1 -1
- package/dist/esm-node/effect-source-loader/loader.mjs +8 -2
- package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
- package/dist/types/backend-federation/edge.d.ts +4 -2
- package/dist/types/backend-federation/identity.d.ts +3 -6
- package/dist/types/backend-federation/integrity-plugin.d.ts +13 -0
- package/dist/types/backend-federation/node-shared.d.ts +20 -0
- package/dist/types/backend-federation/node.d.ts +3 -0
- package/dist/types/backend-federation/plugin.d.ts +4 -0
- package/dist/types/backend-federation/types.d.ts +2 -7
- package/dist/types/backend-federation-manifest/types.d.ts +2 -1
- package/dist/types/cross-project-policy/node.d.ts +2 -2
- package/dist/types/effect-server/cross-project-policy.d.ts +5 -0
- package/dist/types/{effect-adapter → effect-server}/entry.d.ts +1 -1
- package/dist/types/effect-server/error-response.d.ts +2 -0
- package/dist/types/effect-server/index.d.ts +4 -0
- package/dist/types/effect-source-loader/paths.d.ts +20 -2
- package/package.json +35 -33
- package/src/backend-federation/edge.ts +7 -4
- package/src/backend-federation/identity.ts +39 -47
- package/src/backend-federation/integrity-plugin.ts +157 -0
- package/src/backend-federation/load.ts +0 -1
- package/src/backend-federation/node-shared.ts +76 -0
- package/src/backend-federation/node.ts +7 -3
- package/src/backend-federation/plugin.ts +73 -25
- package/src/backend-federation/runtime.ts +16 -90
- package/src/backend-federation/types.ts +2 -11
- package/src/backend-federation-manifest/load.ts +76 -27
- package/src/backend-federation-manifest/node.ts +3 -2
- package/src/backend-federation-manifest/remote.ts +0 -6
- package/src/backend-federation-manifest/types.ts +2 -0
- package/src/backend-federation-manifest/validation.ts +22 -29
- package/src/cross-project-policy/node.ts +7 -38
- package/src/{effect-adapter → effect-server}/cross-project-policy.ts +9 -3
- package/src/effect-server/entry.ts +18 -0
- package/src/{effect-adapter → effect-server}/error-response.ts +1 -1
- package/src/effect-server/index.ts +242 -0
- package/src/effect-source-loader/built-entry.ts +11 -9
- package/src/effect-source-loader/http-api-runtime.ts +1 -1
- package/src/effect-source-loader/loader.ts +17 -6
- package/src/effect-source-loader/operation-contracts.ts +7 -41
- package/src/effect-source-loader/paths.ts +55 -21
- package/src/effect-source-loader/rspack-loader.ts +0 -1
- package/dist/cjs/backend-federation/entry.js +0 -94
- package/dist/cjs/effect-adapter/entry.js +0 -99
- package/dist/cjs/effect-adapter/index.js +0 -194
- package/dist/esm/backend-federation/entry.mjs +0 -56
- package/dist/esm/effect-adapter/entry.mjs +0 -51
- package/dist/esm/effect-adapter/index.mjs +0 -143
- package/dist/esm-node/backend-federation/entry.mjs +0 -57
- package/dist/esm-node/effect-adapter/entry.mjs +0 -52
- package/dist/esm-node/effect-adapter/index.mjs +0 -144
- package/dist/types/backend-federation/entry.d.ts +0 -2
- package/dist/types/effect-adapter/cross-project-policy.d.ts +0 -4
- package/dist/types/effect-adapter/error-response.d.ts +0 -2
- package/dist/types/effect-adapter/index.d.ts +0 -21
- package/src/backend-federation/entry.ts +0 -125
- package/src/effect-adapter/entry.ts +0 -96
- package/src/effect-adapter/index.ts +0 -251
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @effect-diagnostics asyncFunction:off globalFetch:off strictBooleanExpressions:off
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
evaluateBackendFederationCommonJsEntry,
|
|
5
|
+
loadVerifiedBackendFederationEntry,
|
|
6
|
+
} from '@modern-js/server-runtime-extensions/backend-federation-security';
|
|
7
|
+
import {
|
|
8
|
+
Module,
|
|
9
|
+
type ModuleFederation,
|
|
10
|
+
type ModuleFederationRuntimePlugin,
|
|
11
|
+
} from '@module-federation/runtime';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BackendFederationEntryExports,
|
|
15
|
+
BackendFederationRemote,
|
|
16
|
+
BackendFederationRuntimeOptions,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
type RemoteInfo = ConstructorParameters<typeof Module>[0]['remoteInfo'];
|
|
20
|
+
|
|
21
|
+
export const PROVIDED_ENTRY_SCHEME = /^(?:binding|service|static):/u;
|
|
22
|
+
|
|
23
|
+
function decodeDataUrl(remote: BackendFederationRemote) {
|
|
24
|
+
const commaIndex = remote.entry.indexOf(',');
|
|
25
|
+
if (commaIndex < 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`[BFF][Effect] Backend federation remote ${remote.name} has an invalid data URL entry.`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return new TextEncoder().encode(
|
|
31
|
+
decodeURIComponent(remote.entry.slice(commaIndex + 1)),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Node caches ES modules by URL, so every runtime importing the same file: or
|
|
36
|
+
// data: module shares one container. Only evaluated CommonJS containers (the
|
|
37
|
+
// verified network path) are isolated per runtime.
|
|
38
|
+
async function importContainer(remote: BackendFederationRemote) {
|
|
39
|
+
const namespace = await import(/* webpackIgnore: true */ remote.entry);
|
|
40
|
+
return (namespace.default ?? namespace) as BackendFederationEntryExports;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One load path per entry scheme; anything else names the remote and fails. */
|
|
44
|
+
function loadContainer(
|
|
45
|
+
remote: BackendFederationRemote,
|
|
46
|
+
options: BackendFederationRuntimeOptions,
|
|
47
|
+
): Promise<BackendFederationEntryExports> {
|
|
48
|
+
const verified = Boolean(
|
|
49
|
+
remote.verification || options.entryPolicy?.expected,
|
|
50
|
+
);
|
|
51
|
+
const scheme = /^[a-z][a-z0-9+.-]*:/iu.exec(remote.entry)?.[0].toLowerCase();
|
|
52
|
+
if (scheme === 'https:' || scheme === 'http:') {
|
|
53
|
+
if (!verified) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[BFF][Effect] Backend federation remote ${remote.name} requires verified entry bytes before network execution.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return loadVerifiedBackendFederationEntry({
|
|
59
|
+
...options.entryPolicy,
|
|
60
|
+
remote,
|
|
61
|
+
...(remote.verification ? { verification: remote.verification } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (verified) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[BFF][Effect] Backend federation remote ${remote.name} declares entry verification, but only http(s) entries are fetched and verified.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (scheme === 'data:' && remote.type !== 'module') {
|
|
70
|
+
return Promise.resolve(
|
|
71
|
+
evaluateBackendFederationCommonJsEntry(
|
|
72
|
+
remote,
|
|
73
|
+
decodeDataUrl(remote),
|
|
74
|
+
options.entryPolicy?.evaluateCommonJs,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (scheme === 'data:' || scheme === 'file:') {
|
|
79
|
+
return importContainer(remote);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[BFF][Effect] Backend federation remote ${remote.name} uses unsupported entry ${remote.entry}. Use a verified http(s) entry, a data: or file: entry, or a binding:, service: or static: entry served by createBackendFederationLoadEntryPlugin().`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function registerRemoteContainer(
|
|
87
|
+
origin: ModuleFederation,
|
|
88
|
+
remoteInfo: RemoteInfo,
|
|
89
|
+
container: BackendFederationEntryExports,
|
|
90
|
+
) {
|
|
91
|
+
const module = new Module({ host: origin, remoteInfo });
|
|
92
|
+
module.remoteEntryExports = container as InstanceType<
|
|
93
|
+
typeof Module
|
|
94
|
+
>['remoteEntryExports'];
|
|
95
|
+
origin.moduleCache.set(remoteInfo.name, module);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Loads each configured remote once per runtime and registers the resulting
|
|
100
|
+
* container with that runtime only, so Module Federation initializes it with
|
|
101
|
+
* the runtime's own share scope. Registered last, it replaces any container a
|
|
102
|
+
* caller plugin supplied for a scheme it owns.
|
|
103
|
+
*/
|
|
104
|
+
export function createBackendFederationIntegrityPlugin(
|
|
105
|
+
options: BackendFederationRuntimeOptions,
|
|
106
|
+
remotes: readonly BackendFederationRemote[],
|
|
107
|
+
): ModuleFederationRuntimePlugin {
|
|
108
|
+
const loading = new Map<string, Promise<void>>();
|
|
109
|
+
|
|
110
|
+
const register = async (origin: ModuleFederation, remoteInfo: RemoteInfo) => {
|
|
111
|
+
const remote = remotes.find(
|
|
112
|
+
candidate => candidate.name === remoteInfo.name,
|
|
113
|
+
);
|
|
114
|
+
if (remote === undefined) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[BFF][Effect] Missing backend federation remote ${remoteInfo.name}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (PROVIDED_ENTRY_SCHEME.test(remote.entry)) {
|
|
120
|
+
if (
|
|
121
|
+
origin.moduleCache.get(remote.name)?.remoteEntryExports === undefined
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`[BFF][Effect] Backend federation remote ${remote.name} has no entry provider for ${remote.entry}. Pass createBackendFederationLoadEntryPlugin() in plugins.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const container = await loadContainer(remote, options);
|
|
130
|
+
if (typeof container?.get !== 'function') {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`[BFF][Effect] Backend federation remote ${remote.name} entry must expose get().`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
registerRemoteContainer(origin, remoteInfo, container);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name: 'modernjs-backend-federation-integrity',
|
|
140
|
+
afterMatchRemote({ origin, remoteInfo }) {
|
|
141
|
+
if (remoteInfo === undefined) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let pending = loading.get(remoteInfo.name);
|
|
145
|
+
if (pending === undefined) {
|
|
146
|
+
pending = register(origin, remoteInfo);
|
|
147
|
+
loading.set(remoteInfo.name, pending);
|
|
148
|
+
pending.catch(() => {
|
|
149
|
+
if (loading.get(remoteInfo.name) === pending) {
|
|
150
|
+
loading.delete(remoteInfo.name);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return pending;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -5,7 +5,6 @@ import { createBackendFederationRuntime } from './runtime';
|
|
|
5
5
|
import type {
|
|
6
6
|
BackendFederatedEffectApiModule,
|
|
7
7
|
BackendFederationIdentityLoadOptions,
|
|
8
|
-
BackendFederationLoadOptions,
|
|
9
8
|
} from './types';
|
|
10
9
|
import { normalizeExpose } from './utils';
|
|
11
10
|
import { validateLoadedBackendFederatedEffectApi } from './validate-loaded';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import type * as EffectContextModule from '@modern-js/bff-effect/context';
|
|
3
|
+
import type * as EffectEdgeModule from '@modern-js/bff-effect/effect-edge';
|
|
4
|
+
|
|
5
|
+
import type { BackendFederationRuntimeOptions } from './types';
|
|
6
|
+
|
|
7
|
+
type HostShared = NonNullable<BackendFederationRuntimeOptions['shared']>;
|
|
8
|
+
|
|
9
|
+
const fromAdapter = createRequire(import.meta.url);
|
|
10
|
+
const effectPackageJson = fromAdapter.resolve(
|
|
11
|
+
'@modern-js/bff-effect/package.json',
|
|
12
|
+
);
|
|
13
|
+
const fromEffectPackage = createRequire(effectPackageJson);
|
|
14
|
+
|
|
15
|
+
let hostShared: HostShared | undefined;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shares the package-private Effect handler factory registry and the Effect
|
|
19
|
+
* request storage with every backend container, so factories a remote creates
|
|
20
|
+
* with defineEffectBff are recognized by this host's module resolver and its
|
|
21
|
+
* endpoints read the context this host's dispatcher enters.
|
|
22
|
+
*
|
|
23
|
+
* Loaded on first call, not when this module loads: a CommonJS require() of
|
|
24
|
+
* the Effect ES modules throws while a concurrent import() is still loading
|
|
25
|
+
* them.
|
|
26
|
+
*/
|
|
27
|
+
export function effectBffHostShared(): HostShared {
|
|
28
|
+
if (hostShared !== undefined) {
|
|
29
|
+
return hostShared;
|
|
30
|
+
}
|
|
31
|
+
const {
|
|
32
|
+
EFFECT_BFF_CONTEXT_STORAGE_SHARE,
|
|
33
|
+
EFFECT_BFF_HANDLER_FACTORY_REGISTRY_SHARE,
|
|
34
|
+
} = fromEffectPackage(
|
|
35
|
+
'@modern-js/bff-effect/effect-edge',
|
|
36
|
+
) as typeof EffectEdgeModule;
|
|
37
|
+
const { getEffectContextStorage } = fromEffectPackage(
|
|
38
|
+
'@modern-js/bff-effect/context',
|
|
39
|
+
) as typeof EffectContextModule;
|
|
40
|
+
const handlerFactoryRegistry: unknown = fromEffectPackage(
|
|
41
|
+
`#effect-entry-shape-${'registry'}`,
|
|
42
|
+
);
|
|
43
|
+
const version = (fromAdapter(effectPackageJson) as { version: string })
|
|
44
|
+
.version;
|
|
45
|
+
hostShared = {
|
|
46
|
+
[EFFECT_BFF_HANDLER_FACTORY_REGISTRY_SHARE]: {
|
|
47
|
+
version,
|
|
48
|
+
lib: () => handlerFactoryRegistry,
|
|
49
|
+
shareConfig: { singleton: true, requiredVersion: false },
|
|
50
|
+
},
|
|
51
|
+
[EFFECT_BFF_CONTEXT_STORAGE_SHARE]: {
|
|
52
|
+
version,
|
|
53
|
+
lib: getEffectContextStorage,
|
|
54
|
+
shareConfig: { singleton: true, requiredVersion: false },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return hostShared;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds the host shares to caller shares. Their names are reserved: a caller
|
|
62
|
+
* replacement would make containers use a registry or a request storage this
|
|
63
|
+
* host never consults.
|
|
64
|
+
*/
|
|
65
|
+
export function withEffectBffHostShared(
|
|
66
|
+
shared: BackendFederationRuntimeOptions['shared'],
|
|
67
|
+
): HostShared {
|
|
68
|
+
const host = effectBffHostShared();
|
|
69
|
+
const reserved = Object.keys(host).find(name => shared && name in shared);
|
|
70
|
+
if (reserved !== undefined) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`[BFF][Effect] Share ${reserved} is reserved for the Node host. Remove it from options.shared.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return { ...shared, ...host };
|
|
76
|
+
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { evaluateNodeBackendFederationCommonJs } from '@modern-js/server-runtime-extensions/backend-federation-security/node';
|
|
2
|
-
|
|
3
2
|
import { loadBackendFederatedEffectApi as loadUniversalBackendFederatedEffectApi } from './load';
|
|
3
|
+
import { effectBffHostShared, withEffectBffHostShared } from './node-shared';
|
|
4
|
+
|
|
5
|
+
/** Call for the `shared` option when building a custom runtime for Node hosts. */
|
|
6
|
+
export { effectBffHostShared };
|
|
7
|
+
|
|
4
8
|
import type {
|
|
5
9
|
BackendFederatedEffectApiModule,
|
|
6
10
|
BackendFederationIdentityLoadOptions,
|
|
7
|
-
BackendFederationLoadOptions,
|
|
8
11
|
} from './types';
|
|
9
12
|
|
|
10
|
-
export function loadBackendFederatedEffectApi(
|
|
13
|
+
export async function loadBackendFederatedEffectApi(
|
|
11
14
|
options: BackendFederationIdentityLoadOptions,
|
|
12
15
|
): Promise<BackendFederatedEffectApiModule> {
|
|
13
16
|
return loadUniversalBackendFederatedEffectApi({
|
|
14
17
|
...options,
|
|
18
|
+
shared: withEffectBffHostShared(options.shared),
|
|
15
19
|
entryPolicy: {
|
|
16
20
|
...options.entryPolicy,
|
|
17
21
|
evaluateCommonJs:
|
|
@@ -1,35 +1,83 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ModuleFederation,
|
|
3
|
+
ModuleFederationRuntimePlugin,
|
|
4
|
+
} from '@module-federation/runtime';
|
|
2
5
|
|
|
3
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
PROVIDED_ENTRY_SCHEME,
|
|
8
|
+
registerRemoteContainer,
|
|
9
|
+
} from './integrity-plugin';
|
|
10
|
+
import type {
|
|
11
|
+
BackendFederationEntryExports,
|
|
12
|
+
BackendFederationLoadEntryPluginOptions,
|
|
13
|
+
} from './types';
|
|
4
14
|
|
|
15
|
+
function toContainer(entry: BackendFederationEntryExports) {
|
|
16
|
+
return {
|
|
17
|
+
get(id: string) {
|
|
18
|
+
return () => Promise.resolve(entry.get(id)).then(factory => factory());
|
|
19
|
+
},
|
|
20
|
+
init(...args: unknown[]) {
|
|
21
|
+
return entry.init?.(...args);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serves binding:, service: and static: entries. The provider resolves each
|
|
28
|
+
* remote once per runtime and registers the container with that runtime only.
|
|
29
|
+
*/
|
|
5
30
|
export function createBackendFederationLoadEntryPlugin(
|
|
6
31
|
options: BackendFederationLoadEntryPluginOptions,
|
|
7
32
|
): ModuleFederationRuntimePlugin {
|
|
33
|
+
const provided = new WeakMap<ModuleFederation, Map<string, Promise<void>>>();
|
|
34
|
+
|
|
8
35
|
return {
|
|
9
36
|
name: 'modernjs-backend-federation-load-entry',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
afterMatchRemote({ origin, remoteInfo }) {
|
|
38
|
+
if (
|
|
39
|
+
remoteInfo === undefined ||
|
|
40
|
+
!PROVIDED_ENTRY_SCHEME.test(remoteInfo.entry)
|
|
41
|
+
) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let entries = provided.get(origin);
|
|
45
|
+
// The first provider that registers a container wins; later ones are
|
|
46
|
+
// not consulted.
|
|
47
|
+
if (
|
|
48
|
+
entries?.has(remoteInfo.name) !== true &&
|
|
49
|
+
origin.moduleCache.get(remoteInfo.name)?.remoteEntryExports !==
|
|
50
|
+
undefined
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (entries === undefined) {
|
|
55
|
+
entries = new Map();
|
|
56
|
+
provided.set(origin, entries);
|
|
57
|
+
}
|
|
58
|
+
const runtimeEntries = entries;
|
|
59
|
+
let pending = runtimeEntries.get(remoteInfo.name);
|
|
60
|
+
if (pending === undefined) {
|
|
61
|
+
pending = Promise.resolve(
|
|
62
|
+
options.resolveEntry({
|
|
63
|
+
name: remoteInfo.name,
|
|
64
|
+
entry: remoteInfo.entry,
|
|
65
|
+
type: remoteInfo.type,
|
|
66
|
+
}),
|
|
67
|
+
).then(entry => {
|
|
68
|
+
if (entry !== undefined) {
|
|
69
|
+
registerRemoteContainer(origin, remoteInfo, toContainer(entry));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const settled = pending;
|
|
73
|
+
runtimeEntries.set(remoteInfo.name, settled);
|
|
74
|
+
settled.catch(() => {
|
|
75
|
+
if (runtimeEntries.get(remoteInfo.name) === settled) {
|
|
76
|
+
runtimeEntries.delete(remoteInfo.name);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return pending;
|
|
33
81
|
},
|
|
34
82
|
};
|
|
35
83
|
}
|
|
@@ -1,99 +1,25 @@
|
|
|
1
1
|
import { ModuleFederation } from '@module-federation/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createBackendFederationLoadEntryPlugin } from './plugin';
|
|
3
|
+
import { createBackendFederationIntegrityPlugin } from './integrity-plugin';
|
|
5
4
|
import { collectRemotes } from './remotes';
|
|
6
|
-
import type {
|
|
7
|
-
BackendFederationRemote,
|
|
8
|
-
BackendFederationRuntimeOptions,
|
|
9
|
-
} from './types';
|
|
10
|
-
|
|
11
|
-
let nextRuntimeId = 0;
|
|
12
|
-
|
|
13
|
-
function parseRemoteRequest(request: string) {
|
|
14
|
-
const [remoteName, ...exposeParts] = request.split('/');
|
|
15
|
-
if (
|
|
16
|
-
remoteName === undefined ||
|
|
17
|
-
remoteName.length === 0 ||
|
|
18
|
-
exposeParts.length === 0
|
|
19
|
-
) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
return remoteName;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function createLocalEntryPlugin(
|
|
26
|
-
options: BackendFederationRuntimeOptions,
|
|
27
|
-
remotes: BackendFederationRemote[],
|
|
28
|
-
) {
|
|
29
|
-
return createBackendFederationLoadEntryPlugin({
|
|
30
|
-
resolveEntry(resolvedRemote) {
|
|
31
|
-
const remote = remotes.find(
|
|
32
|
-
candidate => candidate.name === resolvedRemote.name,
|
|
33
|
-
);
|
|
34
|
-
if (remote === undefined) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
get(expose: string) {
|
|
39
|
-
return () => loadBackendFederationExpose(remote, expose, options);
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isolateRegisteredRemoteEntries(remotes: BackendFederationRemote[]) {
|
|
47
|
-
const runtimeId = nextRuntimeId;
|
|
48
|
-
nextRuntimeId += 1;
|
|
49
|
-
return remotes.map(remote => ({
|
|
50
|
-
...remote,
|
|
51
|
-
entry: `${remote.entry}#modernjs-backend-runtime-${runtimeId}`,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
class BackendFederationRuntime extends ModuleFederation {
|
|
56
|
-
readonly #remoteNames: ReadonlySet<string>;
|
|
57
|
-
|
|
58
|
-
constructor(
|
|
59
|
-
options: BackendFederationRuntimeOptions,
|
|
60
|
-
remotes: BackendFederationRemote[],
|
|
61
|
-
) {
|
|
62
|
-
super({
|
|
63
|
-
name: options.hostName,
|
|
64
|
-
remotes: isolateRegisteredRemoteEntries(remotes),
|
|
65
|
-
shared: {},
|
|
66
|
-
plugins: [createLocalEntryPlugin(options, remotes)],
|
|
67
|
-
});
|
|
68
|
-
this.#remoteNames = new Set(remotes.map(remote => remote.name));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
override loadRemote<T>(
|
|
72
|
-
request: string,
|
|
73
|
-
loadOptions?: NonNullable<Parameters<ModuleFederation['loadRemote']>[1]>,
|
|
74
|
-
): Promise<T | null> {
|
|
75
|
-
const remoteName = parseRemoteRequest(request);
|
|
76
|
-
if (remoteName === undefined) {
|
|
77
|
-
return Promise.reject(
|
|
78
|
-
new Error(
|
|
79
|
-
`[BFF][Effect] Invalid backend federation request ${request}.`,
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (!this.#remoteNames.has(remoteName)) {
|
|
84
|
-
return Promise.reject(
|
|
85
|
-
new Error(
|
|
86
|
-
`[BFF][Effect] Missing backend federation remote ${remoteName}.`,
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
return super.loadRemote<T>(request, loadOptions);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
5
|
+
import type { BackendFederationRuntimeOptions } from './types';
|
|
93
6
|
|
|
94
7
|
export function createBackendFederationRuntime(
|
|
95
8
|
options: BackendFederationRuntimeOptions,
|
|
96
9
|
): ModuleFederation {
|
|
97
10
|
const remotes = collectRemotes(options);
|
|
98
|
-
return new
|
|
11
|
+
return new ModuleFederation({
|
|
12
|
+
name: options.hostName,
|
|
13
|
+
// Remotes use the default share scope, where host shares are registered.
|
|
14
|
+
remotes: remotes.map(({ name, entry, type }) => ({
|
|
15
|
+
name,
|
|
16
|
+
entry,
|
|
17
|
+
...(type ? { type } : {}),
|
|
18
|
+
})),
|
|
19
|
+
shared: options.shared ?? {},
|
|
20
|
+
plugins: [
|
|
21
|
+
...(options.plugins ?? []),
|
|
22
|
+
createBackendFederationIntegrityPlugin(options, remotes),
|
|
23
|
+
],
|
|
24
|
+
});
|
|
99
25
|
}
|
|
@@ -14,8 +14,6 @@ export type BackendFederationRemote = {
|
|
|
14
14
|
name: string;
|
|
15
15
|
entry: string;
|
|
16
16
|
type?: 'commonjs-module' | 'module' | string;
|
|
17
|
-
entryGlobalName?: string;
|
|
18
|
-
shareScope?: string | string[];
|
|
19
17
|
expose?: typeof BACKEND_FEDERATION_EFFECT_EXPOSE;
|
|
20
18
|
verification?: BackendFederationRemoteEntryVerification;
|
|
21
19
|
};
|
|
@@ -25,6 +23,8 @@ export type BackendFederationRuntimeOptions = {
|
|
|
25
23
|
remote?: BackendFederationRemote;
|
|
26
24
|
remotes?: BackendFederationRemote[];
|
|
27
25
|
plugins?: ModuleFederationRuntimePlugin[];
|
|
26
|
+
/** Host shares handed to every remote container's `init()`. */
|
|
27
|
+
shared?: ConstructorParameters<typeof ModuleFederation>[0]['shared'];
|
|
28
28
|
entryPolicy?: BackendFederationRemoteEntryPolicy;
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -86,12 +86,3 @@ export type BackendFederationIdentityLoadOptions =
|
|
|
86
86
|
BackendFederationLoadOptions & {
|
|
87
87
|
expected: BackendFederationExpectedIdentity;
|
|
88
88
|
};
|
|
89
|
-
|
|
90
|
-
export type BackendFederationLoadEntryPlugin = ModuleFederationRuntimePlugin & {
|
|
91
|
-
loadEntry?: (args: {
|
|
92
|
-
remoteInfo: BackendFederationRemote;
|
|
93
|
-
}) =>
|
|
94
|
-
| BackendFederationEntryExports
|
|
95
|
-
| undefined
|
|
96
|
-
| Promise<BackendFederationEntryExports | undefined>;
|
|
97
|
-
};
|
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
validateLoadedBackendFederationContract,
|
|
26
26
|
} from './validation';
|
|
27
27
|
|
|
28
|
+
// One manifest+entry budget. Explicit outer 0 opts out; nested 0 cannot
|
|
29
|
+
// disable a positive nested deadline.
|
|
30
|
+
const DEFAULT_SHARED_MANIFEST_LOAD_TIMEOUT_MS = 10_000;
|
|
31
|
+
|
|
28
32
|
function createManifestLoadScope(
|
|
29
33
|
options: BackendFederationManifestAdapterOptions,
|
|
30
34
|
) {
|
|
@@ -44,9 +48,14 @@ function createManifestLoadScope(
|
|
|
44
48
|
);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
51
|
+
const positivePolicyTimeouts = policyTimeouts.filter(timeout => timeout > 0);
|
|
47
52
|
const timeoutMs =
|
|
48
53
|
options.timeoutMs ??
|
|
49
|
-
(policyTimeouts.length
|
|
54
|
+
(policyTimeouts.length === 0
|
|
55
|
+
? DEFAULT_SHARED_MANIFEST_LOAD_TIMEOUT_MS
|
|
56
|
+
: positivePolicyTimeouts.length > 0
|
|
57
|
+
? Math.min(...positivePolicyTimeouts)
|
|
58
|
+
: 0);
|
|
50
59
|
const inputSignals = [
|
|
51
60
|
options.signal,
|
|
52
61
|
options.manifestPolicy?.signal,
|
|
@@ -140,6 +149,39 @@ function throwIfManifestLoadAborted(signal: AbortSignal) {
|
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
// Stop waiting even when trusted plugin work ignores cancellation. The race
|
|
153
|
+
// observes late rejections; it cannot stop that work from running.
|
|
154
|
+
function settleOnSharedAbort<T>(
|
|
155
|
+
promise: Promise<T>,
|
|
156
|
+
signal: AbortSignal,
|
|
157
|
+
): Promise<T> {
|
|
158
|
+
if (signal.aborted) {
|
|
159
|
+
// Observe work even when the pre-aborted path never reaches Promise.race.
|
|
160
|
+
promise.catch(() => {});
|
|
161
|
+
throwIfManifestLoadAborted(signal);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let onAbort: (() => void) | undefined;
|
|
165
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
166
|
+
onAbort = () => {
|
|
167
|
+
reject(
|
|
168
|
+
new BackendFederationManifestAdapterError(
|
|
169
|
+
'remote_unavailable',
|
|
170
|
+
'[BFF][Effect] Backend federation load was aborted before remote execution.',
|
|
171
|
+
signal.reason,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return Promise.race([promise, aborted]).finally(() => {
|
|
179
|
+
if (onAbort) {
|
|
180
|
+
signal.removeEventListener('abort', onAbort);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
143
185
|
export async function loadBackendFederatedEffectApiFromManifest(
|
|
144
186
|
options: BackendFederationManifestAdapterOptions,
|
|
145
187
|
): Promise<BackendFederatedEffectApiModule> {
|
|
@@ -150,16 +192,19 @@ export async function loadBackendFederatedEffectApiFromManifest(
|
|
|
150
192
|
try {
|
|
151
193
|
scope = createManifestLoadScope(options);
|
|
152
194
|
throwIfManifestLoadAborted(scope.signal);
|
|
153
|
-
manifest = await
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
manifest = await settleOnSharedAbort(
|
|
196
|
+
loadBackendFederationManifest({
|
|
197
|
+
...options,
|
|
198
|
+
manifestPolicy: {
|
|
199
|
+
...options.manifestPolicy,
|
|
200
|
+
signal: scope.signal,
|
|
201
|
+
timeoutMs: 0,
|
|
202
|
+
},
|
|
157
203
|
signal: scope.signal,
|
|
158
204
|
timeoutMs: 0,
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
205
|
+
}),
|
|
206
|
+
scope.signal,
|
|
207
|
+
);
|
|
163
208
|
validateBackendFederationManifest(manifest, options.expected);
|
|
164
209
|
remote = resolveBackendFederationRemoteFromManifest(
|
|
165
210
|
manifest,
|
|
@@ -223,24 +268,28 @@ export async function loadBackendFederatedEffectApiFromManifest(
|
|
|
223
268
|
'manifest_invalid',
|
|
224
269
|
'Backend federation manifest requires delivery-unit identity.',
|
|
225
270
|
);
|
|
226
|
-
const loaded = await
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
...options.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
271
|
+
const loaded = await settleOnSharedAbort(
|
|
272
|
+
loadBackendFederatedEffectApi({
|
|
273
|
+
hostName: options.hostName,
|
|
274
|
+
remote,
|
|
275
|
+
...(options.plugins ? { plugins: options.plugins } : {}),
|
|
276
|
+
...(options.shared ? { shared: options.shared } : {}),
|
|
277
|
+
entryPolicy: {
|
|
278
|
+
...options.entryPolicy,
|
|
279
|
+
signal: scope.signal,
|
|
280
|
+
timeoutMs: 0,
|
|
281
|
+
},
|
|
282
|
+
...(options.runtime ? { runtime: options.runtime } : {}),
|
|
283
|
+
expected: { unitId, buildMarker },
|
|
284
|
+
}).catch((error: unknown) => {
|
|
285
|
+
throw new BackendFederationManifestAdapterError(
|
|
286
|
+
scope?.timedOut() === true ? 'timeout' : classifyLoadError(error),
|
|
287
|
+
`[BFF][Effect] Backend federation remote ${remote?.name ?? 'unknown'} could not load ${remote?.expose ?? BACKEND_FEDERATION_EFFECT_EXPOSE}.`,
|
|
288
|
+
error,
|
|
289
|
+
);
|
|
290
|
+
}),
|
|
291
|
+
scope.signal,
|
|
292
|
+
);
|
|
244
293
|
|
|
245
294
|
validateLoadedBackendFederationContract(loaded, manifest, remote);
|
|
246
295
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { evaluateNodeBackendFederationCommonJs } from '@modern-js/server-runtime-extensions/backend-federation-security/node';
|
|
2
|
-
|
|
3
2
|
import type { BackendFederatedEffectApiModule } from '../backend-federation';
|
|
3
|
+
import { withEffectBffHostShared } from '../backend-federation/node-shared';
|
|
4
4
|
import { loadBackendFederatedEffectApiFromManifest as loadUniversalBackendFederatedEffectApiFromManifest } from './load';
|
|
5
5
|
import type { BackendFederationManifestAdapterOptions } from './types';
|
|
6
6
|
|
|
7
|
-
export function loadBackendFederatedEffectApiFromManifest(
|
|
7
|
+
export async function loadBackendFederatedEffectApiFromManifest(
|
|
8
8
|
options: BackendFederationManifestAdapterOptions,
|
|
9
9
|
): Promise<BackendFederatedEffectApiModule> {
|
|
10
10
|
return loadUniversalBackendFederatedEffectApiFromManifest({
|
|
11
11
|
...options,
|
|
12
|
+
shared: withEffectBffHostShared(options.shared),
|
|
12
13
|
entryPolicy: {
|
|
13
14
|
...options.entryPolicy,
|
|
14
15
|
evaluateCommonJs:
|