@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) 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 +200 -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 +10 -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/cjs/server-global-vars/index.js +61 -0
  24. package/dist/esm/backend-federation/edge.mjs +1 -1
  25. package/dist/esm/backend-federation/identity.mjs +18 -23
  26. package/dist/esm/backend-federation/integrity-plugin.mjs +68 -0
  27. package/dist/esm/backend-federation/node-shared.mjs +41 -0
  28. package/dist/esm/backend-federation/node.mjs +4 -2
  29. package/dist/esm/backend-federation/plugin.mjs +37 -18
  30. package/dist/esm/backend-federation/runtime.mjs +16 -84
  31. package/dist/esm/backend-federation-manifest/load.mjs +31 -5
  32. package/dist/esm/backend-federation-manifest/node.mjs +3 -1
  33. package/dist/esm/backend-federation-manifest/remote.mjs +1 -7
  34. package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
  35. package/dist/esm/cross-project-policy/node.mjs +9 -19
  36. package/dist/esm/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
  37. package/dist/esm/effect-server/entry.mjs +15 -0
  38. package/dist/esm/{effect-adapter → effect-server}/error-response.mjs +2 -2
  39. package/dist/esm/effect-server/index.mjs +152 -0
  40. package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
  41. package/dist/esm/effect-source-loader/http-api-runtime.mjs +1 -1
  42. package/dist/esm/effect-source-loader/loader.mjs +10 -2
  43. package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
  44. package/dist/esm/effect-source-loader/paths.mjs +30 -11
  45. package/dist/esm/server-global-vars/index.mjs +23 -0
  46. package/dist/esm-node/backend-federation/edge.mjs +1 -1
  47. package/dist/esm-node/backend-federation/identity.mjs +18 -23
  48. package/dist/esm-node/backend-federation/integrity-plugin.mjs +69 -0
  49. package/dist/esm-node/backend-federation/node-shared.mjs +42 -0
  50. package/dist/esm-node/backend-federation/node.mjs +4 -2
  51. package/dist/esm-node/backend-federation/plugin.mjs +37 -18
  52. package/dist/esm-node/backend-federation/runtime.mjs +16 -84
  53. package/dist/esm-node/backend-federation-manifest/load.mjs +31 -5
  54. package/dist/esm-node/backend-federation-manifest/node.mjs +3 -1
  55. package/dist/esm-node/backend-federation-manifest/remote.mjs +1 -7
  56. package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
  57. package/dist/esm-node/cross-project-policy/node.mjs +9 -19
  58. package/dist/esm-node/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
  59. package/dist/esm-node/effect-server/entry.mjs +16 -0
  60. package/dist/esm-node/{effect-adapter → effect-server}/error-response.mjs +2 -2
  61. package/dist/esm-node/effect-server/index.mjs +153 -0
  62. package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
  63. package/dist/esm-node/effect-source-loader/http-api-runtime.mjs +1 -1
  64. package/dist/esm-node/effect-source-loader/loader.mjs +10 -2
  65. package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
  66. package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
  67. package/dist/esm-node/server-global-vars/index.mjs +24 -0
  68. package/dist/types/backend-federation/edge.d.ts +4 -2
  69. package/dist/types/backend-federation/identity.d.ts +3 -6
  70. package/dist/types/backend-federation/integrity-plugin.d.ts +13 -0
  71. package/dist/types/backend-federation/node-shared.d.ts +20 -0
  72. package/dist/types/backend-federation/node.d.ts +3 -0
  73. package/dist/types/backend-federation/plugin.d.ts +4 -0
  74. package/dist/types/backend-federation/types.d.ts +2 -7
  75. package/dist/types/backend-federation-manifest/types.d.ts +2 -1
  76. package/dist/types/cross-project-policy/node.d.ts +2 -2
  77. package/dist/types/effect-server/cross-project-policy.d.ts +5 -0
  78. package/dist/types/{effect-adapter → effect-server}/entry.d.ts +1 -1
  79. package/dist/types/effect-server/error-response.d.ts +2 -0
  80. package/dist/types/effect-server/index.d.ts +4 -0
  81. package/dist/types/effect-source-loader/http-api-runtime.d.ts +2 -0
  82. package/dist/types/effect-source-loader/loader.d.ts +3 -0
  83. package/dist/types/effect-source-loader/operation-contracts.d.ts +2 -0
  84. package/dist/types/effect-source-loader/paths.d.ts +20 -2
  85. package/dist/types/server-global-vars/index.d.ts +11 -0
  86. package/package.json +47 -33
  87. package/src/backend-federation/edge.ts +7 -4
  88. package/src/backend-federation/identity.ts +39 -47
  89. package/src/backend-federation/integrity-plugin.ts +157 -0
  90. package/src/backend-federation/load.ts +0 -1
  91. package/src/backend-federation/node-shared.ts +76 -0
  92. package/src/backend-federation/node.ts +7 -3
  93. package/src/backend-federation/plugin.ts +73 -25
  94. package/src/backend-federation/runtime.ts +16 -90
  95. package/src/backend-federation/types.ts +2 -11
  96. package/src/backend-federation-manifest/load.ts +76 -27
  97. package/src/backend-federation-manifest/node.ts +3 -2
  98. package/src/backend-federation-manifest/remote.ts +0 -6
  99. package/src/backend-federation-manifest/types.ts +2 -0
  100. package/src/backend-federation-manifest/validation.ts +22 -29
  101. package/src/cross-project-policy/node.ts +7 -38
  102. package/src/{effect-adapter → effect-server}/cross-project-policy.ts +9 -3
  103. package/src/effect-server/entry.ts +18 -0
  104. package/src/{effect-adapter → effect-server}/error-response.ts +1 -1
  105. package/src/effect-server/index.ts +243 -0
  106. package/src/effect-source-loader/built-entry.ts +11 -9
  107. package/src/effect-source-loader/http-api-runtime.ts +3 -1
  108. package/src/effect-source-loader/loader.ts +24 -6
  109. package/src/effect-source-loader/operation-contracts.ts +9 -41
  110. package/src/effect-source-loader/paths.ts +55 -21
  111. package/src/effect-source-loader/rspack-loader.ts +0 -1
  112. package/src/server-global-vars/index.ts +46 -0
  113. package/dist/cjs/backend-federation/entry.js +0 -94
  114. package/dist/cjs/effect-adapter/entry.js +0 -99
  115. package/dist/cjs/effect-adapter/index.js +0 -194
  116. package/dist/esm/backend-federation/entry.mjs +0 -56
  117. package/dist/esm/effect-adapter/entry.mjs +0 -51
  118. package/dist/esm/effect-adapter/index.mjs +0 -143
  119. package/dist/esm-node/backend-federation/entry.mjs +0 -57
  120. package/dist/esm-node/effect-adapter/entry.mjs +0 -52
  121. package/dist/esm-node/effect-adapter/index.mjs +0 -144
  122. package/dist/types/backend-federation/entry.d.ts +0 -2
  123. package/dist/types/effect-adapter/cross-project-policy.d.ts +0 -4
  124. package/dist/types/effect-adapter/error-response.d.ts +0 -2
  125. package/dist/types/effect-adapter/index.d.ts +0 -21
  126. package/src/backend-federation/entry.ts +0 -125
  127. package/src/effect-adapter/entry.ts +0 -96
  128. package/src/effect-adapter/index.ts +0 -251
@@ -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:
@@ -91,12 +91,6 @@ export function resolveBackendFederationRemoteFromManifest(
91
91
  entry: remoteEntryUrl,
92
92
  type: remoteType,
93
93
  ...(verification ? { verification } : {}),
94
- ...(remoteOverride.entryGlobalName
95
- ? { entryGlobalName: remoteOverride.entryGlobalName }
96
- : {}),
97
94
  expose,
98
- ...(remoteOverride.shareScope
99
- ? { shareScope: remoteOverride.shareScope }
100
- : {}),
101
95
  };
102
96
  }
@@ -12,6 +12,7 @@ import type {
12
12
  import type {
13
13
  BackendFederatedEffectApiModule,
14
14
  BackendFederationRemote,
15
+ BackendFederationRuntimeOptions,
15
16
  } from '../backend-federation';
16
17
  import type { BackendFederationManifestAdapterError } from './errors';
17
18
 
@@ -68,6 +69,7 @@ export type BackendFederationManifestAdapterOptions = {
68
69
  plugins?: ModuleFederationRuntimePlugin[];
69
70
  remote?: Partial<BackendFederationRemote>;
70
71
  runtime?: ModuleFederation;
72
+ shared?: BackendFederationRuntimeOptions['shared'];
71
73
  signal?: AbortSignal;
72
74
  timeoutMs?: number;
73
75
  };
@@ -10,6 +10,11 @@ import {
10
10
  type BackendFederatedEffectApiModule,
11
11
  type BackendFederationRemote,
12
12
  } from '../backend-federation';
13
+ import {
14
+ backendFederationCompatibility,
15
+ formatBackendFederationIdentityIssues,
16
+ validateBackendFederationCompatibility,
17
+ } from '../backend-federation/identity';
13
18
  import {
14
19
  assertConsistentValue,
15
20
  assertManifestAdapter,
@@ -19,7 +24,6 @@ import {
19
24
  import {
20
25
  backendFederationMetadata,
21
26
  manifestDeliveryUnit,
22
- recordField,
23
27
  stringValue,
24
28
  versionBoundaryMetadata,
25
29
  } from './metadata';
@@ -166,7 +170,7 @@ export function validateLoadedBackendFederationContract(
166
170
  const loadedContract = loaded.backendFederationContract;
167
171
  const backendFederation = backendFederationMetadata(manifest);
168
172
  const boundary = versionBoundaryMetadata(manifest);
169
- const compatibility = recordField(loadedContract, 'compatibility');
173
+ const compatibility = backendFederationCompatibility(loaded);
170
174
 
171
175
  assertManifestAdapter(
172
176
  compatibility,
@@ -186,26 +190,6 @@ export function validateLoadedBackendFederationContract(
186
190
  remote.name,
187
191
  'expose name',
188
192
  );
189
- assertVersionValue(
190
- stringValue(compatibility.contractVersion),
191
- stringValue(backendFederation?.contractVersion),
192
- 'expose contractVersion',
193
- );
194
- assertVersionValue(
195
- stringValue(compatibility.nodeAdapterVersion),
196
- stringValue(backendFederation?.nodeAdapterVersion),
197
- 'expose nodeAdapterVersion',
198
- );
199
- assertVersionValue(
200
- stringValue(compatibility.packageName),
201
- stringValue(boundary?.packageName),
202
- 'expose packageName',
203
- );
204
- assertVersionValue(
205
- stringValue(compatibility.build),
206
- stringValue(boundary?.buildVersion),
207
- 'expose buildVersion',
208
- );
209
193
 
210
194
  const deliveryUnit = manifestDeliveryUnit(backendFederation, boundary);
211
195
  const manifestUnitId =
@@ -213,15 +197,24 @@ export function validateLoadedBackendFederationContract(
213
197
  stringValue(deliveryUnit.top?.unitId);
214
198
  const manifestBuildMarker = stringValue(deliveryUnit.boundary?.buildMarker);
215
199
 
216
- assertVersionValue(
217
- stringValue(compatibility.unitId),
218
- manifestUnitId,
219
- 'deliveryUnit.unitId vs expose compatibility.unitId',
220
- );
200
+ const issues = validateBackendFederationCompatibility(compatibility, {
201
+ contractVersion: stringValue(backendFederation?.contractVersion),
202
+ nodeAdapterVersion: stringValue(backendFederation?.nodeAdapterVersion),
203
+ packageName: stringValue(boundary?.packageName),
204
+ unitId: manifestUnitId,
205
+ build: manifestBuildMarker ?? stringValue(boundary?.buildVersion),
206
+ });
207
+ // Manifest validation establishes buildVersion/buildMarker consistency; the
208
+ // executed expose is still checked independently against that trusted record.
221
209
  assertVersionValue(
222
210
  stringValue(compatibility.build),
223
- manifestBuildMarker,
224
- 'deliveryUnit.buildMarker vs expose compatibility.build',
211
+ stringValue(boundary?.buildVersion),
212
+ 'expose buildVersion',
213
+ );
214
+ assertManifestAdapter(
215
+ issues.length === 0,
216
+ 'version_mismatch',
217
+ `[BFF][Effect] ${formatBackendFederationIdentityIssues(issues)}.`,
225
218
  );
226
219
  }
227
220
 
@@ -1,48 +1,17 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
1
  import '@modern-js/server-runtime-extensions/server-config';
4
2
  import type { ServerPluginAPI } from '@modern-js/server-core';
5
3
  import {
6
- deriveOperationVersion,
7
4
  type OperationContractSource,
8
5
  resolveCrossProjectPolicy,
6
+ resolveOperationProducer,
9
7
  } from '@modern-js/server-runtime-extensions/bff-policy/node';
10
8
 
11
9
  import type { ResolvedCrossProjectPolicy } from './evaluation';
12
10
 
13
- const readNearestPackageVersion = (
14
- startDir: string | undefined,
15
- ): string | undefined => {
16
- if (!startDir) {
17
- return undefined;
18
- }
19
-
20
- let current = path.resolve(startDir);
21
- for (let depth = 0; depth < 10; depth += 1) {
22
- try {
23
- const packageJson = JSON.parse(
24
- readFileSync(path.join(current, 'package.json'), 'utf8'),
25
- ) as { version?: unknown };
26
- if (typeof packageJson.version === 'string') {
27
- return packageJson.version;
28
- }
29
- } catch {
30
- // The producer package may be nested below its package.json.
31
- }
32
-
33
- const parent = path.dirname(current);
34
- if (parent === current) {
35
- return undefined;
36
- }
37
- current = parent;
38
- }
39
-
40
- return undefined;
41
- };
42
-
43
11
  export const resolveAdapterCrossProjectPolicy = (
44
12
  api: ServerPluginAPI,
45
13
  handlers: OperationContractSource[],
14
+ producer?: ReturnType<typeof resolveOperationProducer>,
46
15
  ): ResolvedCrossProjectPolicy | undefined => {
47
16
  const bff = api.getServerConfig()?.bff;
48
17
  const { apiDirectory, appDirectory } = api.getServerContext() as {
@@ -53,11 +22,11 @@ export const resolveAdapterCrossProjectPolicy = (
53
22
  return resolveCrossProjectPolicy({
54
23
  crossProjectPolicy: bff?.crossProjectPolicy,
55
24
  handlers,
56
- requestId: bff?.requestId,
57
25
  isCrossProjectServer: bff?.isCrossProjectServer,
58
- operationVersion: deriveOperationVersion(
59
- readNearestPackageVersion(apiDirectory) ??
60
- readNearestPackageVersion(appDirectory),
61
- ),
26
+ ...(producer ??
27
+ resolveOperationProducer({
28
+ directories: [apiDirectory, appDirectory],
29
+ requestId: bff?.requestId || 'default',
30
+ })),
62
31
  });
63
32
  };
@@ -6,16 +6,18 @@ import {
6
6
  toOperationContractSources,
7
7
  } from '@modern-js/bff-effect/effect';
8
8
  import type { ServerPluginAPI } from '@modern-js/server-core';
9
+ import type { resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
9
10
  import { logger } from '@modern-js/utils';
10
- import { HttpApi } from 'effect/unstable/httpapi';
11
+ import { HttpApi } from 'effect/http-api';
11
12
 
12
13
  import { resolveAdapterCrossProjectPolicy } from '../cross-project-policy';
13
14
  import type { ResolvedCrossProjectPolicy } from '../cross-project-policy/evaluation';
14
15
 
15
- export function resolveEffectAdapterCrossProjectPolicy(
16
+ export function resolveEffectServerCrossProjectPolicy(
16
17
  api: ServerPluginAPI,
17
18
  prefix: string,
18
19
  mod: EffectApiModule | null,
20
+ producer?: ReturnType<typeof resolveOperationProducer>,
19
21
  ): Promise<ResolvedCrossProjectPolicy | undefined> {
20
22
  const contractSourcesPromise =
21
23
  mod === null
@@ -52,7 +54,11 @@ export function resolveEffectAdapterCrossProjectPolicy(
52
54
  });
53
55
 
54
56
  return contractSourcesPromise.then(contractSources => {
55
- const policy = resolveAdapterCrossProjectPolicy(api, contractSources);
57
+ const policy = resolveAdapterCrossProjectPolicy(
58
+ api,
59
+ contractSources,
60
+ producer,
61
+ );
56
62
  if (policy?.enabled === true && contractSources.length === 0) {
57
63
  logger.warn(
58
64
  '[BFF][Effect] Cross-project policy enabled but no HttpApi endpoints could be reflected; requests fail operation-contract matching unless allowUnknownOperations is enabled.',
@@ -0,0 +1,18 @@
1
+ // @effect-diagnostics nodeBuiltinImport:off strictBooleanExpressions:off
2
+ import path from 'node:path';
3
+ import '@modern-js/server-runtime-extensions/server-config';
4
+ import type { ServerPluginAPI } from '@modern-js/server-core';
5
+ import { API_DIR, isProd } from '@modern-js/utils';
6
+ import { resolveEffectEntry } from '../effect-source-loader/paths';
7
+
8
+ export function resolveEffectServerEntryFile(api: ServerPluginAPI) {
9
+ const { appDirectory, apiDirectory, distDirectory } = api.getServerContext();
10
+ const appDir = path.resolve(appDirectory || process.cwd());
11
+ return resolveEffectEntry({
12
+ appDir,
13
+ apiDir: path.resolve(appDir, apiDirectory || API_DIR),
14
+ effectEntry: api.getServerConfig()?.bff?.effect?.entry,
15
+ distDir:
16
+ isProd() && distDirectory ? path.resolve(distDirectory) : undefined,
17
+ }).path;
18
+ }
@@ -2,7 +2,7 @@ import { createSafeFailureResponse } from '@modern-js/runtime-extensions/safe-fa
2
2
  import type { Context, ServerPluginAPI } from '@modern-js/server-core';
3
3
  import { logger } from '@modern-js/utils';
4
4
 
5
- export function createEffectAdapterRuntimeErrorResponse(
5
+ export function createEffectServerRuntimeErrorResponse(
6
6
  api: ServerPluginAPI,
7
7
  error: unknown,
8
8
  context: Context,
@@ -0,0 +1,243 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off nodeBuiltinImport:off strictBooleanExpressions:off
2
+
3
+ import path from 'node:path';
4
+ import '@modern-js/server-runtime-extensions/server-config';
5
+ import {
6
+ dispatchEffectBffRequest,
7
+ type EffectApiModule,
8
+ type EffectBffRequestHandler,
9
+ resolveEffectBffModuleHandler,
10
+ useEffectContext,
11
+ } from '@modern-js/bff-effect/effect';
12
+ import type {
13
+ Context,
14
+ Next,
15
+ ServerPlugin,
16
+ ServerPluginAPI,
17
+ } from '@modern-js/server-core';
18
+ import { resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
19
+ import {
20
+ createDisposableServerRuntimeHandle,
21
+ type DisposableServerRuntimeHandle,
22
+ registerServerRuntimeDisposer,
23
+ } from '@modern-js/server-runtime-extensions/runtime-lifecycle';
24
+ import { fs, isProd, logger } from '@modern-js/utils';
25
+
26
+ import { checkCrossProjectPolicyForRequest } from '../cross-project-policy/evaluation';
27
+ import {
28
+ loadEffectBuiltModule,
29
+ loadEffectSourceModule,
30
+ } from '../effect-source-loader/loader';
31
+ import { resolveEffectServerCrossProjectPolicy } from './cross-project-policy';
32
+ import { resolveEffectServerEntryFile } from './entry';
33
+ import { createEffectServerRuntimeErrorResponse } from './error-response';
34
+
35
+ const EFFECT_MIDDLEWARE_BEFORE = [
36
+ 'custom-server-hook',
37
+ 'custom-server-middleware',
38
+ 'render',
39
+ ];
40
+
41
+ export default (): ServerPlugin => ({
42
+ name: '@modern-js/plugin-bff-extensions/effect-server',
43
+ setup(api: ServerPluginAPI) {
44
+ const runtimeFramework = api.getServerContext().bffRuntimeFramework;
45
+ if (runtimeFramework !== 'effect') {
46
+ throw new Error(
47
+ `Effect BFF server plugin requires Effect; received "${runtimeFramework}".`,
48
+ );
49
+ }
50
+ let handler: DisposableServerRuntimeHandle | null = null;
51
+ let unregisterRuntimeDisposer: (() => void) | undefined;
52
+ let prefix = '/api';
53
+ let prefixes = ['/api'];
54
+ let matchingPrefixes = ['/api'];
55
+ let retired = false;
56
+
57
+ const dispose = async () => {
58
+ retired = true;
59
+ unregisterRuntimeDisposer?.();
60
+ unregisterRuntimeDisposer = undefined;
61
+ const previous = handler;
62
+ handler = null;
63
+ await previous?.dispose();
64
+ };
65
+
66
+ const loadHandler = async () => {
67
+ const entryFile = resolveEffectServerEntryFile(api);
68
+ if (!entryFile || !(await fs.pathExists(entryFile))) {
69
+ handler = null;
70
+ return;
71
+ }
72
+
73
+ const { appDirectory } = api.getServerContext();
74
+ let mod: EffectApiModule;
75
+ try {
76
+ mod = (await (isProd()
77
+ ? loadEffectBuiltModule(entryFile)
78
+ : loadEffectSourceModule({
79
+ resourcePath: entryFile,
80
+ appDir: appDirectory || path.dirname(entryFile),
81
+ globalVars: api.getServerConfig()?.source?.globalVars,
82
+ }))) as EffectApiModule;
83
+ } catch (error) {
84
+ logger.error(
85
+ `[BFF][Effect] Failed to load Effect entry: ${entryFile}\n${String(error)}`,
86
+ );
87
+ throw error;
88
+ }
89
+
90
+ const producer = resolveOperationProducer({
91
+ directories: [path.dirname(entryFile), appDirectory],
92
+ requestId: api.getServerConfig()?.bff?.requestId,
93
+ });
94
+ const crossProjectPolicies = new Map(
95
+ await Promise.all(
96
+ prefixes.map(
97
+ async prefix =>
98
+ [
99
+ prefix,
100
+ await resolveEffectServerCrossProjectPolicy(
101
+ api,
102
+ prefix,
103
+ mod,
104
+ producer,
105
+ ),
106
+ ] as const,
107
+ ),
108
+ ),
109
+ );
110
+ const effectConfig = api.getServerConfig()?.bff?.effect;
111
+ const loaded = await resolveEffectBffModuleHandler(mod, {
112
+ openapi: effectConfig?.openapi,
113
+ dataPlatform: effectConfig?.dataPlatform,
114
+ validateRequest: request => {
115
+ const { path: mountedPathname } = useEffectContext();
116
+ const prefix = matchingPrefixes.find(
117
+ item =>
118
+ item === '/' ||
119
+ mountedPathname === item ||
120
+ mountedPathname.startsWith(`${item}/`),
121
+ );
122
+ if (!prefix) return null;
123
+ const requestUrl = new URL(request.url);
124
+ if (prefix !== '/') {
125
+ requestUrl.pathname =
126
+ requestUrl.pathname === '/'
127
+ ? prefix
128
+ : `${prefix}${requestUrl.pathname}`;
129
+ }
130
+ return checkCrossProjectPolicyForRequest(
131
+ new Request(requestUrl, request),
132
+ crossProjectPolicies.get(prefix),
133
+ );
134
+ },
135
+ onWarning: message => logger.warn(message),
136
+ });
137
+ if (!loaded) {
138
+ const error = new Error(
139
+ `[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`,
140
+ );
141
+ logger.warn(error.message);
142
+ throw error;
143
+ }
144
+ const candidateOwner = {};
145
+ if (loaded.dispose)
146
+ registerServerRuntimeDisposer(candidateOwner, loaded.dispose);
147
+ const candidate = createDisposableServerRuntimeHandle(
148
+ candidateOwner,
149
+ loaded.handler,
150
+ );
151
+ if (retired) {
152
+ await candidate.dispose();
153
+ throw new Error('Cannot initialize a retired Effect server.');
154
+ }
155
+ handler = candidate;
156
+ };
157
+
158
+ api.onPrepare(async () => {
159
+ const { middlewares: globalMiddlewares } = api.getServerContext();
160
+ const configuredPrefix = api.getServerConfig()?.bff?.prefix;
161
+ const enableHandleWeb = api.getServerConfig()?.bff?.enableHandleWeb;
162
+ const configuredPrefixes = Array.isArray(configuredPrefix)
163
+ ? configuredPrefix.filter(Boolean)
164
+ : [configuredPrefix || '/api'];
165
+ prefixes = [
166
+ ...new Set(
167
+ configuredPrefixes.length > 0 ? configuredPrefixes : ['/api'],
168
+ ),
169
+ ];
170
+ matchingPrefixes = [...prefixes].sort(
171
+ (left, right) => right.length - left.length,
172
+ );
173
+ prefix = prefixes[0] || '/api';
174
+ const { serverBase } = api.getServerContext() as { serverBase?: object };
175
+ if (serverBase)
176
+ unregisterRuntimeDisposer = registerServerRuntimeDisposer(
177
+ serverBase,
178
+ dispose,
179
+ );
180
+ try {
181
+ await loadHandler();
182
+ } catch (error) {
183
+ await dispose();
184
+ throw error;
185
+ }
186
+
187
+ const middlewarePrefixes = enableHandleWeb ? [prefix] : matchingPrefixes;
188
+ const middlewares = middlewarePrefixes.map(middlewarePrefix => ({
189
+ name: 'effect-api-handler',
190
+ path: enableHandleWeb ? '*' : `${middlewarePrefix}/*`,
191
+ method: 'all' as const,
192
+ order: 'post' as const,
193
+ before: EFFECT_MIDDLEWARE_BEFORE,
194
+ handler: async (context: Context, next: Next) => {
195
+ const activeHandler = handler;
196
+ if (!activeHandler) {
197
+ if (enableHandleWeb) {
198
+ await next();
199
+ return;
200
+ }
201
+ return createEffectServerRuntimeErrorResponse(
202
+ api,
203
+ new Error(
204
+ '[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.',
205
+ ),
206
+ context,
207
+ );
208
+ }
209
+
210
+ const mountedPrefix = enableHandleWeb
211
+ ? matchingPrefixes.find(
212
+ item =>
213
+ item === '/' ||
214
+ context.req.path === item ||
215
+ context.req.path.startsWith(`${item}/`),
216
+ ) || prefix
217
+ : middlewarePrefix;
218
+ const response = await dispatchEffectBffRequest(
219
+ activeHandler as EffectBffRequestHandler,
220
+ context.req.raw,
221
+ {
222
+ prefix: mountedPrefix,
223
+ env: context.env as Record<string, unknown>,
224
+ path: context.req.path,
225
+ method: context.req.method,
226
+ onError: error =>
227
+ createEffectServerRuntimeErrorResponse(api, error, context),
228
+ },
229
+ );
230
+
231
+ if (response.status === 404 && enableHandleWeb) {
232
+ await next();
233
+ return;
234
+ }
235
+
236
+ return new Response(response.body, response);
237
+ },
238
+ }));
239
+
240
+ globalMiddlewares.push(...middlewares);
241
+ });
242
+ },
243
+ });