@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- package/dist/cjs/backend-federation/edge.js +1 -1
- package/dist/cjs/backend-federation/identity.js +23 -22
- package/dist/cjs/backend-federation/integrity-plugin.js +112 -0
- package/dist/cjs/backend-federation/node-shared.js +85 -0
- package/dist/cjs/backend-federation/node.js +6 -1
- package/dist/cjs/backend-federation/plugin.js +40 -21
- package/dist/cjs/backend-federation/runtime.js +16 -84
- package/dist/cjs/backend-federation-manifest/load.js +31 -5
- package/dist/cjs/backend-federation-manifest/node.js +3 -1
- package/dist/cjs/backend-federation-manifest/remote.js +1 -7
- package/dist/cjs/backend-federation-manifest/validation.js +11 -7
- package/dist/cjs/cross-project-policy/node.js +8 -28
- package/dist/cjs/{effect-adapter → effect-server}/cross-project-policy.js +9 -9
- package/dist/cjs/effect-server/entry.js +63 -0
- package/dist/cjs/{effect-adapter → effect-server}/error-response.js +4 -4
- package/dist/cjs/effect-server/index.js +199 -0
- package/dist/cjs/effect-source-loader/built-entry.js +4 -5
- package/dist/cjs/effect-source-loader/http-api-runtime.js +4 -4
- package/dist/cjs/effect-source-loader/loader.js +8 -2
- package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
- package/dist/cjs/effect-source-loader/paths.js +38 -10
- package/dist/esm/backend-federation/edge.mjs +1 -1
- package/dist/esm/backend-federation/identity.mjs +18 -23
- package/dist/esm/backend-federation/integrity-plugin.mjs +68 -0
- package/dist/esm/backend-federation/node-shared.mjs +41 -0
- package/dist/esm/backend-federation/node.mjs +4 -2
- package/dist/esm/backend-federation/plugin.mjs +37 -18
- package/dist/esm/backend-federation/runtime.mjs +16 -84
- package/dist/esm/backend-federation-manifest/load.mjs +31 -5
- package/dist/esm/backend-federation-manifest/node.mjs +3 -1
- package/dist/esm/backend-federation-manifest/remote.mjs +1 -7
- package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm/cross-project-policy/node.mjs +9 -19
- package/dist/esm/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
- package/dist/esm/effect-server/entry.mjs +15 -0
- package/dist/esm/{effect-adapter → effect-server}/error-response.mjs +2 -2
- package/dist/esm/effect-server/index.mjs +151 -0
- package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm/effect-source-loader/http-api-runtime.mjs +1 -1
- package/dist/esm/effect-source-loader/loader.mjs +8 -2
- package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm/effect-source-loader/paths.mjs +30 -11
- package/dist/esm-node/backend-federation/edge.mjs +1 -1
- package/dist/esm-node/backend-federation/identity.mjs +18 -23
- package/dist/esm-node/backend-federation/integrity-plugin.mjs +69 -0
- package/dist/esm-node/backend-federation/node-shared.mjs +42 -0
- package/dist/esm-node/backend-federation/node.mjs +4 -2
- package/dist/esm-node/backend-federation/plugin.mjs +37 -18
- package/dist/esm-node/backend-federation/runtime.mjs +16 -84
- package/dist/esm-node/backend-federation-manifest/load.mjs +31 -5
- package/dist/esm-node/backend-federation-manifest/node.mjs +3 -1
- package/dist/esm-node/backend-federation-manifest/remote.mjs +1 -7
- package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm-node/cross-project-policy/node.mjs +9 -19
- package/dist/esm-node/{effect-adapter → effect-server}/cross-project-policy.mjs +4 -4
- package/dist/esm-node/effect-server/entry.mjs +16 -0
- package/dist/esm-node/{effect-adapter → effect-server}/error-response.mjs +2 -2
- package/dist/esm-node/effect-server/index.mjs +152 -0
- package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm-node/effect-source-loader/http-api-runtime.mjs +1 -1
- package/dist/esm-node/effect-source-loader/loader.mjs +8 -2
- package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
- package/dist/types/backend-federation/edge.d.ts +4 -2
- package/dist/types/backend-federation/identity.d.ts +3 -6
- package/dist/types/backend-federation/integrity-plugin.d.ts +13 -0
- package/dist/types/backend-federation/node-shared.d.ts +20 -0
- package/dist/types/backend-federation/node.d.ts +3 -0
- package/dist/types/backend-federation/plugin.d.ts +4 -0
- package/dist/types/backend-federation/types.d.ts +2 -7
- package/dist/types/backend-federation-manifest/types.d.ts +2 -1
- package/dist/types/cross-project-policy/node.d.ts +2 -2
- package/dist/types/effect-server/cross-project-policy.d.ts +5 -0
- package/dist/types/{effect-adapter → effect-server}/entry.d.ts +1 -1
- package/dist/types/effect-server/error-response.d.ts +2 -0
- package/dist/types/effect-server/index.d.ts +4 -0
- package/dist/types/effect-source-loader/paths.d.ts +20 -2
- package/package.json +35 -33
- package/src/backend-federation/edge.ts +7 -4
- package/src/backend-federation/identity.ts +39 -47
- package/src/backend-federation/integrity-plugin.ts +157 -0
- package/src/backend-federation/load.ts +0 -1
- package/src/backend-federation/node-shared.ts +76 -0
- package/src/backend-federation/node.ts +7 -3
- package/src/backend-federation/plugin.ts +73 -25
- package/src/backend-federation/runtime.ts +16 -90
- package/src/backend-federation/types.ts +2 -11
- package/src/backend-federation-manifest/load.ts +76 -27
- package/src/backend-federation-manifest/node.ts +3 -2
- package/src/backend-federation-manifest/remote.ts +0 -6
- package/src/backend-federation-manifest/types.ts +2 -0
- package/src/backend-federation-manifest/validation.ts +22 -29
- package/src/cross-project-policy/node.ts +7 -38
- package/src/{effect-adapter → effect-server}/cross-project-policy.ts +9 -3
- package/src/effect-server/entry.ts +18 -0
- package/src/{effect-adapter → effect-server}/error-response.ts +1 -1
- package/src/effect-server/index.ts +242 -0
- package/src/effect-source-loader/built-entry.ts +11 -9
- package/src/effect-source-loader/http-api-runtime.ts +1 -1
- package/src/effect-source-loader/loader.ts +17 -6
- package/src/effect-source-loader/operation-contracts.ts +7 -41
- package/src/effect-source-loader/paths.ts +55 -21
- package/src/effect-source-loader/rspack-loader.ts +0 -1
- package/dist/cjs/backend-federation/entry.js +0 -94
- package/dist/cjs/effect-adapter/entry.js +0 -99
- package/dist/cjs/effect-adapter/index.js +0 -194
- package/dist/esm/backend-federation/entry.mjs +0 -56
- package/dist/esm/effect-adapter/entry.mjs +0 -51
- package/dist/esm/effect-adapter/index.mjs +0 -143
- package/dist/esm-node/backend-federation/entry.mjs +0 -57
- package/dist/esm-node/effect-adapter/entry.mjs +0 -52
- package/dist/esm-node/effect-adapter/index.mjs +0 -144
- package/dist/types/backend-federation/entry.d.ts +0 -2
- package/dist/types/effect-adapter/cross-project-policy.d.ts +0 -4
- package/dist/types/effect-adapter/error-response.d.ts +0 -2
- package/dist/types/effect-adapter/index.d.ts +0 -21
- package/src/backend-federation/entry.ts +0 -125
- package/src/effect-adapter/entry.ts +0 -96
- package/src/effect-adapter/index.ts +0 -251
|
@@ -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 =
|
|
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
|
-
|
|
217
|
-
stringValue(
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
'
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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/
|
|
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
|
|
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(
|
|
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
|
|
5
|
+
export function createEffectServerRuntimeErrorResponse(
|
|
6
6
|
api: ServerPluginAPI,
|
|
7
7
|
error: unknown,
|
|
8
8
|
context: Context,
|
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
}))) as EffectApiModule;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(
|
|
84
|
+
`[BFF][Effect] Failed to load Effect entry: ${entryFile}\n${String(error)}`,
|
|
85
|
+
);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const producer = resolveOperationProducer({
|
|
90
|
+
directories: [path.dirname(entryFile), appDirectory],
|
|
91
|
+
requestId: api.getServerConfig()?.bff?.requestId,
|
|
92
|
+
});
|
|
93
|
+
const crossProjectPolicies = new Map(
|
|
94
|
+
await Promise.all(
|
|
95
|
+
prefixes.map(
|
|
96
|
+
async prefix =>
|
|
97
|
+
[
|
|
98
|
+
prefix,
|
|
99
|
+
await resolveEffectServerCrossProjectPolicy(
|
|
100
|
+
api,
|
|
101
|
+
prefix,
|
|
102
|
+
mod,
|
|
103
|
+
producer,
|
|
104
|
+
),
|
|
105
|
+
] as const,
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
const effectConfig = api.getServerConfig()?.bff?.effect;
|
|
110
|
+
const loaded = await resolveEffectBffModuleHandler(mod, {
|
|
111
|
+
openapi: effectConfig?.openapi,
|
|
112
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
113
|
+
validateRequest: request => {
|
|
114
|
+
const { path: mountedPathname } = useEffectContext();
|
|
115
|
+
const prefix = matchingPrefixes.find(
|
|
116
|
+
item =>
|
|
117
|
+
item === '/' ||
|
|
118
|
+
mountedPathname === item ||
|
|
119
|
+
mountedPathname.startsWith(`${item}/`),
|
|
120
|
+
);
|
|
121
|
+
if (!prefix) return null;
|
|
122
|
+
const requestUrl = new URL(request.url);
|
|
123
|
+
if (prefix !== '/') {
|
|
124
|
+
requestUrl.pathname =
|
|
125
|
+
requestUrl.pathname === '/'
|
|
126
|
+
? prefix
|
|
127
|
+
: `${prefix}${requestUrl.pathname}`;
|
|
128
|
+
}
|
|
129
|
+
return checkCrossProjectPolicyForRequest(
|
|
130
|
+
new Request(requestUrl, request),
|
|
131
|
+
crossProjectPolicies.get(prefix),
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
onWarning: message => logger.warn(message),
|
|
135
|
+
});
|
|
136
|
+
if (!loaded) {
|
|
137
|
+
const error = new Error(
|
|
138
|
+
`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`,
|
|
139
|
+
);
|
|
140
|
+
logger.warn(error.message);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
const candidateOwner = {};
|
|
144
|
+
if (loaded.dispose)
|
|
145
|
+
registerServerRuntimeDisposer(candidateOwner, loaded.dispose);
|
|
146
|
+
const candidate = createDisposableServerRuntimeHandle(
|
|
147
|
+
candidateOwner,
|
|
148
|
+
loaded.handler,
|
|
149
|
+
);
|
|
150
|
+
if (retired) {
|
|
151
|
+
await candidate.dispose();
|
|
152
|
+
throw new Error('Cannot initialize a retired Effect server.');
|
|
153
|
+
}
|
|
154
|
+
handler = candidate;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
api.onPrepare(async () => {
|
|
158
|
+
const { middlewares: globalMiddlewares } = api.getServerContext();
|
|
159
|
+
const configuredPrefix = api.getServerConfig()?.bff?.prefix;
|
|
160
|
+
const enableHandleWeb = api.getServerConfig()?.bff?.enableHandleWeb;
|
|
161
|
+
const configuredPrefixes = Array.isArray(configuredPrefix)
|
|
162
|
+
? configuredPrefix.filter(Boolean)
|
|
163
|
+
: [configuredPrefix || '/api'];
|
|
164
|
+
prefixes = [
|
|
165
|
+
...new Set(
|
|
166
|
+
configuredPrefixes.length > 0 ? configuredPrefixes : ['/api'],
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
matchingPrefixes = [...prefixes].sort(
|
|
170
|
+
(left, right) => right.length - left.length,
|
|
171
|
+
);
|
|
172
|
+
prefix = prefixes[0] || '/api';
|
|
173
|
+
const { serverBase } = api.getServerContext() as { serverBase?: object };
|
|
174
|
+
if (serverBase)
|
|
175
|
+
unregisterRuntimeDisposer = registerServerRuntimeDisposer(
|
|
176
|
+
serverBase,
|
|
177
|
+
dispose,
|
|
178
|
+
);
|
|
179
|
+
try {
|
|
180
|
+
await loadHandler();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
await dispose();
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const middlewarePrefixes = enableHandleWeb ? [prefix] : matchingPrefixes;
|
|
187
|
+
const middlewares = middlewarePrefixes.map(middlewarePrefix => ({
|
|
188
|
+
name: 'effect-api-handler',
|
|
189
|
+
path: enableHandleWeb ? '*' : `${middlewarePrefix}/*`,
|
|
190
|
+
method: 'all' as const,
|
|
191
|
+
order: 'post' as const,
|
|
192
|
+
before: EFFECT_MIDDLEWARE_BEFORE,
|
|
193
|
+
handler: async (context: Context, next: Next) => {
|
|
194
|
+
const activeHandler = handler;
|
|
195
|
+
if (!activeHandler) {
|
|
196
|
+
if (enableHandleWeb) {
|
|
197
|
+
await next();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
return createEffectServerRuntimeErrorResponse(
|
|
201
|
+
api,
|
|
202
|
+
new Error(
|
|
203
|
+
'[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.',
|
|
204
|
+
),
|
|
205
|
+
context,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const mountedPrefix = enableHandleWeb
|
|
210
|
+
? matchingPrefixes.find(
|
|
211
|
+
item =>
|
|
212
|
+
item === '/' ||
|
|
213
|
+
context.req.path === item ||
|
|
214
|
+
context.req.path.startsWith(`${item}/`),
|
|
215
|
+
) || prefix
|
|
216
|
+
: middlewarePrefix;
|
|
217
|
+
const response = await dispatchEffectBffRequest(
|
|
218
|
+
activeHandler as EffectBffRequestHandler,
|
|
219
|
+
context.req.raw,
|
|
220
|
+
{
|
|
221
|
+
prefix: mountedPrefix,
|
|
222
|
+
env: context.env as Record<string, unknown>,
|
|
223
|
+
path: context.req.path,
|
|
224
|
+
method: context.req.method,
|
|
225
|
+
onError: error =>
|
|
226
|
+
createEffectServerRuntimeErrorResponse(api, error, context),
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (response.status === 404 && enableHandleWeb) {
|
|
231
|
+
await next();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return new Response(response.body, response);
|
|
236
|
+
},
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
globalMiddlewares.push(...middlewares);
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { fs, upath as path } from '@modern-js/utils';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
emittedEffectEntry,
|
|
5
|
+
relativeEffectAppPath,
|
|
6
|
+
resolveEffectEntryFile,
|
|
7
|
+
} from './paths';
|
|
6
8
|
|
|
7
9
|
export function resolveBuiltEffectEntry(
|
|
8
10
|
appDirectory: string,
|
|
@@ -12,15 +14,15 @@ export function resolveBuiltEffectEntry(
|
|
|
12
14
|
if (sourceEntry === undefined || sourceEntry.length === 0) {
|
|
13
15
|
return undefined;
|
|
14
16
|
}
|
|
15
|
-
const relativeEntry =
|
|
16
|
-
if (relativeEntry ===
|
|
17
|
+
const relativeEntry = relativeEffectAppPath(appDirectory, sourceEntry);
|
|
18
|
+
if (relativeEntry === undefined) {
|
|
17
19
|
throw new Error(
|
|
18
20
|
`Effect BFF entry must be inside the application directory: ${sourceEntry}`,
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
|
-
const builtEntry =
|
|
22
|
-
.resolve(distDirectory, relativeEntry)
|
|
23
|
-
|
|
23
|
+
const builtEntry = emittedEffectEntry(
|
|
24
|
+
path.resolve(distDirectory, relativeEntry),
|
|
25
|
+
);
|
|
24
26
|
return fs.existsSync(builtEntry) ? builtEntry : undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -62,7 +64,7 @@ export function resolveEffectEntryPaths(options: {
|
|
|
62
64
|
});
|
|
63
65
|
const relativeEffectEntry =
|
|
64
66
|
sourceEffectEntry !== undefined && sourceEffectEntry.length > 0
|
|
65
|
-
? path.relative(appDir, sourceEffectEntry)
|
|
67
|
+
? emittedEffectEntry(path.relative(appDir, sourceEffectEntry))
|
|
66
68
|
: '';
|
|
67
69
|
return { sourceEffectEntry, relativeEffectEntry };
|
|
68
70
|
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type HttpApiLike,
|
|
4
4
|
type HttpApiReflect,
|
|
5
5
|
} from '@modern-js/bff-effect/effect';
|
|
6
|
-
import { HttpApi } from 'effect/
|
|
6
|
+
import { HttpApi } from 'effect/http-api';
|
|
7
7
|
import { loadEffectSourceModule } from './loader';
|
|
8
8
|
|
|
9
9
|
type HttpApiRuntime = {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
transform,
|
|
14
14
|
} from 'esbuild';
|
|
15
15
|
|
|
16
|
-
const NODE_TARGET = 'node26.
|
|
16
|
+
const NODE_TARGET = 'node26.10';
|
|
17
17
|
const SOURCE_LOADERS = new Map<string, Loader>([
|
|
18
18
|
['.js', 'js'],
|
|
19
19
|
['.jsx', 'jsx'],
|
|
@@ -79,6 +79,9 @@ const nodeBuiltins = new Set([
|
|
|
79
79
|
...builtinModules.map(name => `node:${name}`),
|
|
80
80
|
]);
|
|
81
81
|
const dependencyResolutionMarker = 'modern-js-effect-dependency-resolution';
|
|
82
|
+
// The BFF server runs lambdas inside these packages' Effect context storage.
|
|
83
|
+
// A bundled copy would own a second storage that the server never enters.
|
|
84
|
+
const BFF_RUNTIME_PACKAGE = /^@modern-js\/(?:bff-effect|plugin-bff)(?:\/|$)/;
|
|
82
85
|
|
|
83
86
|
async function isEsmOnlyFile(filename: string) {
|
|
84
87
|
const extension = path.extname(filename).toLowerCase();
|
|
@@ -125,10 +128,9 @@ function isBareSpecifier(specifier: string) {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/**
|
|
128
|
-
* Keep installed
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* generated entry is relocatable and directly executable by Node 26.7.
|
|
131
|
+
* Keep installed executable dependencies external to preserve runtime identity.
|
|
132
|
+
* Source requiring compilation is bundled even when pnpm injects a workspace
|
|
133
|
+
* package into node_modules: Node does not strip types inside that directory.
|
|
132
134
|
*/
|
|
133
135
|
function externalizeInstalledDependencies(options: {
|
|
134
136
|
runtimeResolveDir: string;
|
|
@@ -137,6 +139,9 @@ function externalizeInstalledDependencies(options: {
|
|
|
137
139
|
name: 'modern-js-effect-installed-dependencies',
|
|
138
140
|
setup(buildApi) {
|
|
139
141
|
buildApi.onResolve({ filter: /.*/ }, async args => {
|
|
142
|
+
if (BFF_RUNTIME_PACKAGE.test(args.path)) {
|
|
143
|
+
return { external: true, path: args.path };
|
|
144
|
+
}
|
|
140
145
|
if (
|
|
141
146
|
!isBareSpecifier(args.path) ||
|
|
142
147
|
nodeBuiltins.has(args.path) ||
|
|
@@ -167,7 +172,13 @@ function externalizeInstalledDependencies(options: {
|
|
|
167
172
|
const realPath = await fs.promises
|
|
168
173
|
.realpath(resolution.path)
|
|
169
174
|
.catch(() => resolution.path);
|
|
170
|
-
|
|
175
|
+
const loader = SOURCE_LOADERS.get(path.extname(realPath).toLowerCase());
|
|
176
|
+
if (
|
|
177
|
+
realPath.includes(`${path.sep}node_modules${path.sep}`) &&
|
|
178
|
+
loader !== 'ts' &&
|
|
179
|
+
loader !== 'tsx' &&
|
|
180
|
+
loader !== 'jsx'
|
|
181
|
+
) {
|
|
171
182
|
const runtimeResolution = await buildApi.resolve(args.path, {
|
|
172
183
|
importer: '',
|
|
173
184
|
kind: args.kind,
|
|
@@ -4,42 +4,11 @@ import {
|
|
|
4
4
|
} from '@modern-js/bff-effect/effect';
|
|
5
5
|
import {
|
|
6
6
|
buildOperationContractMap,
|
|
7
|
-
|
|
7
|
+
resolveOperationProducer,
|
|
8
8
|
} from '@modern-js/server-runtime-extensions/bff-policy/node';
|
|
9
|
-
import {
|
|
9
|
+
import { upath as path } from '@modern-js/utils';
|
|
10
10
|
import { getHttpApiRuntime, loadEffectApi } from './http-api-runtime';
|
|
11
11
|
|
|
12
|
-
function getPackageInfo(
|
|
13
|
-
resourcePath: string,
|
|
14
|
-
appDir: string,
|
|
15
|
-
onDependency?: (dependency: string) => void,
|
|
16
|
-
): { name?: string; version?: string } {
|
|
17
|
-
for (const startDir of [path.dirname(resourcePath), appDir]) {
|
|
18
|
-
let current = path.resolve(startDir);
|
|
19
|
-
for (let depth = 0; depth < 32; depth += 1) {
|
|
20
|
-
const packageJsonPath = path.join(current, 'package.json');
|
|
21
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
22
|
-
onDependency?.(packageJsonPath);
|
|
23
|
-
try {
|
|
24
|
-
const packageJson = fs.readJSONSync(packageJsonPath) as {
|
|
25
|
-
name?: string;
|
|
26
|
-
version?: string;
|
|
27
|
-
};
|
|
28
|
-
return { name: packageJson.name, version: packageJson.version };
|
|
29
|
-
} catch {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const parent = path.dirname(current);
|
|
34
|
-
if (parent === current) {
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
current = parent;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
12
|
/** Derives server policy metadata without emitting or typing a client. */
|
|
44
13
|
export async function resolveEffectOperationContracts(options: {
|
|
45
14
|
appDir: string;
|
|
@@ -58,15 +27,12 @@ export async function resolveEffectOperationContracts(options: {
|
|
|
58
27
|
api,
|
|
59
28
|
options.prefix,
|
|
60
29
|
);
|
|
61
|
-
const packageInfo = getPackageInfo(
|
|
62
|
-
options.resourcePath,
|
|
63
|
-
options.appDir,
|
|
64
|
-
options.onDependency,
|
|
65
|
-
);
|
|
66
|
-
const requestId = options.requestId?.trim() || packageInfo.name || 'default';
|
|
67
30
|
return buildOperationContractMap({
|
|
68
31
|
handlers: toOperationContractSources(endpoints),
|
|
69
|
-
|
|
70
|
-
|
|
32
|
+
...resolveOperationProducer({
|
|
33
|
+
directories: [path.dirname(options.resourcePath), options.appDir],
|
|
34
|
+
requestId: options.requestId,
|
|
35
|
+
onDependency: options.onDependency,
|
|
36
|
+
}),
|
|
71
37
|
});
|
|
72
38
|
}
|