@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.10
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 +200 -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 +10 -2
- package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
- package/dist/cjs/effect-source-loader/paths.js +38 -10
- package/dist/cjs/server-global-vars/index.js +61 -0
- 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 +152 -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 +10 -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/server-global-vars/index.mjs +23 -0
- 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 +153 -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 +10 -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/esm-node/server-global-vars/index.mjs +24 -0
- 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/http-api-runtime.d.ts +2 -0
- package/dist/types/effect-source-loader/loader.d.ts +3 -0
- package/dist/types/effect-source-loader/operation-contracts.d.ts +2 -0
- package/dist/types/effect-source-loader/paths.d.ts +20 -2
- package/dist/types/hono/node.d.ts +1 -1
- package/dist/types/server-global-vars/index.d.ts +11 -0
- package/package.json +47 -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 +243 -0
- package/src/effect-source-loader/built-entry.ts +11 -9
- package/src/effect-source-loader/http-api-runtime.ts +3 -1
- package/src/effect-source-loader/loader.ts +24 -6
- package/src/effect-source-loader/operation-contracts.ts +9 -41
- package/src/effect-source-loader/paths.ts +55 -21
- package/src/effect-source-loader/rspack-loader.ts +0 -1
- package/src/server-global-vars/index.ts +46 -0
- 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
|
@@ -17,59 +17,51 @@ export type BackendFederationIdentityIssue = {
|
|
|
17
17
|
message: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
* Compares the expose's `backendFederationContract.compatibility` identity
|
|
23
|
-
* (`unitId`, `build`) against the consumer's expected delivery-unit identity.
|
|
24
|
-
* Missing identity metadata is an error.
|
|
25
|
-
*/
|
|
26
|
-
export function validateExpectedBackendFederationIdentity(
|
|
27
|
-
loaded: unknown,
|
|
28
|
-
expected: BackendFederationExpectedIdentity,
|
|
29
|
-
): BackendFederationIdentityIssue[] {
|
|
30
|
-
const issues: BackendFederationIdentityIssue[] = [];
|
|
20
|
+
/** Decode independently at each received-manifest / executed-expose boundary. */
|
|
21
|
+
export function backendFederationCompatibility(loaded: unknown) {
|
|
31
22
|
const contract = isRecord(loaded)
|
|
32
23
|
? loaded.backendFederationContract
|
|
33
24
|
: undefined;
|
|
34
25
|
const compatibility = isRecord(contract) ? contract.compatibility : undefined;
|
|
26
|
+
return isRecord(compatibility) ? compatibility : undefined;
|
|
27
|
+
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
path: 'backendFederationContract.compatibility.unitId',
|
|
55
|
-
message: `expected ${expected.unitId}, received ${unitId}`,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const build = compatibility.build;
|
|
60
|
-
if (typeof build !== 'string' || build.length === 0) {
|
|
61
|
-
issues.push({
|
|
62
|
-
path: 'backendFederationContract.compatibility.build',
|
|
63
|
-
message: `missing build marker; expected ${expected.buildMarker}`,
|
|
64
|
-
});
|
|
65
|
-
} else if (build !== expected.buildMarker) {
|
|
66
|
-
issues.push({
|
|
67
|
-
path: 'backendFederationContract.compatibility.build',
|
|
68
|
-
message: `expected ${expected.buildMarker}, received ${build}`,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
29
|
+
export function validateBackendFederationCompatibility(
|
|
30
|
+
compatibility: Record<string, unknown>,
|
|
31
|
+
expected: Record<string, string | undefined>,
|
|
32
|
+
): BackendFederationIdentityIssue[] {
|
|
33
|
+
return Object.entries(expected).flatMap(([field, value]) =>
|
|
34
|
+
value === undefined ||
|
|
35
|
+
(typeof compatibility[field] === 'string' &&
|
|
36
|
+
compatibility[field].length > 0 &&
|
|
37
|
+
compatibility[field] === value)
|
|
38
|
+
? []
|
|
39
|
+
: [
|
|
40
|
+
{
|
|
41
|
+
path: `backendFederationContract.compatibility.${field}`,
|
|
42
|
+
message: `expected ${value}, received ${typeof compatibility[field] === 'string' ? compatibility[field] : typeof compatibility[field]}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
}
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
export function validateExpectedBackendFederationIdentity(
|
|
49
|
+
loaded: unknown,
|
|
50
|
+
expected: BackendFederationExpectedIdentity,
|
|
51
|
+
): BackendFederationIdentityIssue[] {
|
|
52
|
+
const compatibility = backendFederationCompatibility(loaded);
|
|
53
|
+
return compatibility
|
|
54
|
+
? validateBackendFederationCompatibility(compatibility, {
|
|
55
|
+
unitId: expected.unitId,
|
|
56
|
+
build: expected.buildMarker,
|
|
57
|
+
})
|
|
58
|
+
: [
|
|
59
|
+
{
|
|
60
|
+
path: 'backendFederationContract.compatibility',
|
|
61
|
+
message:
|
|
62
|
+
'loaded module declares no compatibility metadata; delivery-unit identity cannot be validated',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
export function formatBackendFederationIdentityIssues(
|
|
@@ -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
|
-
};
|