@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.8 → 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.
Files changed (120) hide show
  1. package/README.md +21 -4
  2. package/dist/cjs/backend-federation/edge.js +1 -1
  3. package/dist/cjs/backend-federation/identity.js +23 -22
  4. package/dist/cjs/backend-federation/integrity-plugin.js +112 -0
  5. package/dist/cjs/backend-federation/node-shared.js +85 -0
  6. package/dist/cjs/backend-federation/node.js +6 -1
  7. package/dist/cjs/backend-federation/plugin.js +40 -21
  8. package/dist/cjs/backend-federation/runtime.js +16 -84
  9. package/dist/cjs/backend-federation-manifest/load.js +31 -5
  10. package/dist/cjs/backend-federation-manifest/node.js +3 -1
  11. package/dist/cjs/backend-federation-manifest/remote.js +1 -7
  12. package/dist/cjs/backend-federation-manifest/validation.js +11 -7
  13. package/dist/cjs/cross-project-policy/node.js +8 -28
  14. package/dist/cjs/{effect-adapter → effect-server}/cross-project-policy.js +9 -9
  15. package/dist/cjs/effect-server/entry.js +63 -0
  16. package/dist/cjs/{effect-adapter → effect-server}/error-response.js +4 -4
  17. package/dist/cjs/effect-server/index.js +199 -0
  18. package/dist/cjs/effect-source-loader/built-entry.js +4 -5
  19. package/dist/cjs/effect-source-loader/http-api-runtime.js +4 -4
  20. package/dist/cjs/effect-source-loader/loader.js +8 -2
  21. package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
  22. package/dist/cjs/effect-source-loader/paths.js +38 -10
  23. package/dist/esm/backend-federation/edge.mjs +1 -1
  24. package/dist/esm/backend-federation/identity.mjs +18 -23
  25. package/dist/esm/backend-federation/integrity-plugin.mjs +68 -0
  26. package/dist/esm/backend-federation/node-shared.mjs +41 -0
  27. package/dist/esm/backend-federation/node.mjs +4 -2
  28. package/dist/esm/backend-federation/plugin.mjs +37 -18
  29. package/dist/esm/backend-federation/runtime.mjs +16 -84
  30. package/dist/esm/backend-federation-manifest/load.mjs +31 -5
  31. package/dist/esm/backend-federation-manifest/node.mjs +3 -1
  32. package/dist/esm/backend-federation-manifest/remote.mjs +1 -7
  33. package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
  34. package/dist/esm/cross-project-policy/node.mjs +9 -19
  35. package/dist/esm/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
  36. package/dist/esm/effect-server/entry.mjs +15 -0
  37. package/dist/esm/{effect-adapter → effect-server}/error-response.mjs +2 -2
  38. package/dist/esm/effect-server/index.mjs +151 -0
  39. package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
  40. package/dist/esm/effect-source-loader/http-api-runtime.mjs +1 -1
  41. package/dist/esm/effect-source-loader/loader.mjs +8 -2
  42. package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
  43. package/dist/esm/effect-source-loader/paths.mjs +30 -11
  44. package/dist/esm-node/backend-federation/edge.mjs +1 -1
  45. package/dist/esm-node/backend-federation/identity.mjs +18 -23
  46. package/dist/esm-node/backend-federation/integrity-plugin.mjs +69 -0
  47. package/dist/esm-node/backend-federation/node-shared.mjs +42 -0
  48. package/dist/esm-node/backend-federation/node.mjs +4 -2
  49. package/dist/esm-node/backend-federation/plugin.mjs +37 -18
  50. package/dist/esm-node/backend-federation/runtime.mjs +16 -84
  51. package/dist/esm-node/backend-federation-manifest/load.mjs +31 -5
  52. package/dist/esm-node/backend-federation-manifest/node.mjs +3 -1
  53. package/dist/esm-node/backend-federation-manifest/remote.mjs +1 -7
  54. package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
  55. package/dist/esm-node/cross-project-policy/node.mjs +9 -19
  56. package/dist/esm-node/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
  57. package/dist/esm-node/effect-server/entry.mjs +16 -0
  58. package/dist/esm-node/{effect-adapter → effect-server}/error-response.mjs +2 -2
  59. package/dist/esm-node/effect-server/index.mjs +152 -0
  60. package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
  61. package/dist/esm-node/effect-source-loader/http-api-runtime.mjs +1 -1
  62. package/dist/esm-node/effect-source-loader/loader.mjs +8 -2
  63. package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
  64. package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
  65. package/dist/types/backend-federation/edge.d.ts +4 -2
  66. package/dist/types/backend-federation/identity.d.ts +3 -6
  67. package/dist/types/backend-federation/integrity-plugin.d.ts +13 -0
  68. package/dist/types/backend-federation/node-shared.d.ts +20 -0
  69. package/dist/types/backend-federation/node.d.ts +3 -0
  70. package/dist/types/backend-federation/plugin.d.ts +4 -0
  71. package/dist/types/backend-federation/types.d.ts +2 -7
  72. package/dist/types/backend-federation-manifest/types.d.ts +2 -1
  73. package/dist/types/cross-project-policy/node.d.ts +2 -2
  74. package/dist/types/effect-server/cross-project-policy.d.ts +5 -0
  75. package/dist/types/{effect-adapter → effect-server}/entry.d.ts +1 -1
  76. package/dist/types/effect-server/error-response.d.ts +2 -0
  77. package/dist/types/effect-server/index.d.ts +4 -0
  78. package/dist/types/effect-source-loader/paths.d.ts +20 -2
  79. package/package.json +35 -33
  80. package/src/backend-federation/edge.ts +7 -4
  81. package/src/backend-federation/identity.ts +39 -47
  82. package/src/backend-federation/integrity-plugin.ts +157 -0
  83. package/src/backend-federation/load.ts +0 -1
  84. package/src/backend-federation/node-shared.ts +76 -0
  85. package/src/backend-federation/node.ts +7 -3
  86. package/src/backend-federation/plugin.ts +73 -25
  87. package/src/backend-federation/runtime.ts +16 -90
  88. package/src/backend-federation/types.ts +2 -11
  89. package/src/backend-federation-manifest/load.ts +76 -27
  90. package/src/backend-federation-manifest/node.ts +3 -2
  91. package/src/backend-federation-manifest/remote.ts +0 -6
  92. package/src/backend-federation-manifest/types.ts +2 -0
  93. package/src/backend-federation-manifest/validation.ts +22 -29
  94. package/src/cross-project-policy/node.ts +7 -38
  95. package/src/{effect-adapter → effect-server}/cross-project-policy.ts +9 -3
  96. package/src/effect-server/entry.ts +18 -0
  97. package/src/{effect-adapter → effect-server}/error-response.ts +1 -1
  98. package/src/effect-server/index.ts +242 -0
  99. package/src/effect-source-loader/built-entry.ts +11 -9
  100. package/src/effect-source-loader/http-api-runtime.ts +1 -1
  101. package/src/effect-source-loader/loader.ts +17 -6
  102. package/src/effect-source-loader/operation-contracts.ts +7 -41
  103. package/src/effect-source-loader/paths.ts +55 -21
  104. package/src/effect-source-loader/rspack-loader.ts +0 -1
  105. package/dist/cjs/backend-federation/entry.js +0 -94
  106. package/dist/cjs/effect-adapter/entry.js +0 -99
  107. package/dist/cjs/effect-adapter/index.js +0 -194
  108. package/dist/esm/backend-federation/entry.mjs +0 -56
  109. package/dist/esm/effect-adapter/entry.mjs +0 -51
  110. package/dist/esm/effect-adapter/index.mjs +0 -143
  111. package/dist/esm-node/backend-federation/entry.mjs +0 -57
  112. package/dist/esm-node/effect-adapter/entry.mjs +0 -52
  113. package/dist/esm-node/effect-adapter/index.mjs +0 -144
  114. package/dist/types/backend-federation/entry.d.ts +0 -2
  115. package/dist/types/effect-adapter/cross-project-policy.d.ts +0 -4
  116. package/dist/types/effect-adapter/error-response.d.ts +0 -2
  117. package/dist/types/effect-adapter/index.d.ts +0 -21
  118. package/src/backend-federation/entry.ts +0 -125
  119. package/src/effect-adapter/entry.ts +0 -96
  120. 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 { ModuleFederationRuntimePlugin } from '@module-federation/runtime';
1
+ import type {
2
+ ModuleFederation,
3
+ ModuleFederationRuntimePlugin,
4
+ } from '@module-federation/runtime';
2
5
 
3
- import type { BackendFederationLoadEntryPluginOptions } from './types';
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
- loadEntry({ remoteInfo }) {
11
- return Promise.resolve(
12
- options.resolveEntry({
13
- name: remoteInfo.name,
14
- entry: remoteInfo.entry,
15
- type: remoteInfo.type,
16
- entryGlobalName: remoteInfo.entryGlobalName,
17
- shareScope: remoteInfo.shareScope,
18
- }),
19
- ).then(entry => {
20
- if (entry === undefined) {
21
- return;
22
- }
23
- return {
24
- get(id: string) {
25
- return () =>
26
- Promise.resolve(entry.get(id)).then(factory => factory());
27
- },
28
- init(...args: unknown[]) {
29
- return entry.init?.(...args);
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 { loadBackendFederationExpose } from './entry';
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 BackendFederationRuntime(options, remotes);
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 > 0 ? Math.min(...policyTimeouts) : 0);
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 loadBackendFederationManifest({
154
- ...options,
155
- manifestPolicy: {
156
- ...options.manifestPolicy,
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
- signal: scope.signal,
161
- timeoutMs: 0,
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 loadBackendFederatedEffectApi({
227
- hostName: options.hostName,
228
- remote,
229
- ...(options.plugins ? { plugins: options.plugins } : {}),
230
- entryPolicy: {
231
- ...options.entryPolicy,
232
- signal: scope.signal,
233
- timeoutMs: 0,
234
- },
235
- ...(options.runtime ? { runtime: options.runtime } : {}),
236
- expected: { unitId, buildMarker },
237
- }).catch((error: unknown) => {
238
- throw new BackendFederationManifestAdapterError(
239
- scope?.timedOut() === true ? 'timeout' : classifyLoadError(error),
240
- `[BFF][Effect] Backend federation remote ${remote?.name ?? 'unknown'} could not load ${remote?.expose ?? BACKEND_FEDERATION_EFFECT_EXPOSE}.`,
241
- error,
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: