@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.9 → 3.5.0-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/dist/cjs/runtime/effect/adapter.js +12 -59
- package/dist/cjs/runtime/effect/edge-context.js +81 -0
- package/dist/cjs/runtime/effect/edge.js +35 -13
- package/dist/cjs/runtime/effect/handler.js +5 -3
- package/dist/cjs/runtime/effect/index.js +25 -5
- package/dist/cjs/runtime/effect/module.js +46 -27
- package/dist/cjs/utils/effectClientGenerator.js +13 -8
- package/dist/esm/runtime/effect/adapter.mjs +12 -59
- package/dist/esm/runtime/effect/edge-context.mjs +34 -0
- package/dist/esm/runtime/effect/edge.mjs +5 -3
- package/dist/esm/runtime/effect/handler.mjs +5 -3
- package/dist/esm/runtime/effect/index.mjs +2 -1
- package/dist/esm/runtime/effect/module.mjs +46 -27
- package/dist/esm/utils/effectClientGenerator.mjs +13 -8
- package/dist/esm-node/runtime/effect/adapter.mjs +12 -59
- package/dist/esm-node/runtime/effect/edge-context.mjs +35 -0
- package/dist/esm-node/runtime/effect/edge.mjs +5 -3
- package/dist/esm-node/runtime/effect/handler.mjs +5 -3
- package/dist/esm-node/runtime/effect/index.mjs +2 -1
- package/dist/esm-node/runtime/effect/module.mjs +46 -27
- package/dist/esm-node/utils/effectClientGenerator.mjs +13 -8
- package/dist/types/runtime/effect/adapter.d.ts +3 -19
- package/dist/types/runtime/effect/edge-context.d.ts +5 -0
- package/dist/types/runtime/effect/edge.d.ts +2 -0
- package/dist/types/runtime/effect/handler.d.ts +2 -5
- package/dist/types/runtime/effect/index.d.ts +2 -1
- package/dist/types/runtime/effect/module.d.ts +3 -16
- package/package.json +13 -13
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ApiRouter } from "@modern-js/bff-core";
|
|
2
1
|
import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
|
|
3
2
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
4
3
|
import path from "path";
|
|
@@ -52,43 +51,12 @@ class EffectAdapter {
|
|
|
52
51
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
53
52
|
const bffConfig = this.api.getServerConfig()?.bff;
|
|
54
53
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
60
|
-
if (!enableHandleWeb) return true;
|
|
61
|
-
const normalized = normalizePrefix(prefix);
|
|
62
|
-
if (!normalized) return true;
|
|
63
|
-
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
64
|
-
}
|
|
65
|
-
async collectLambdaContractSources() {
|
|
66
|
-
try {
|
|
67
|
-
const serverContext = this.api.getServerContext();
|
|
68
|
-
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
69
|
-
if (!appDir) return [];
|
|
70
|
-
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
|
|
71
|
-
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
|
|
72
|
-
if (!await fs.pathExists(lambdaDir)) return [];
|
|
73
|
-
const apiRouter = new ApiRouter({
|
|
74
|
-
appDir,
|
|
75
|
-
apiDir,
|
|
76
|
-
lambdaDir,
|
|
77
|
-
prefix: this.prefix,
|
|
78
|
-
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
79
|
-
});
|
|
80
|
-
const handlerInfos = await apiRouter.getApiHandlers();
|
|
81
|
-
return handlerInfos.map((info)=>({
|
|
82
|
-
name: info.name,
|
|
83
|
-
httpMethod: info.httpMethod,
|
|
84
|
-
routePath: info.routePath,
|
|
85
|
-
filename: info.filename,
|
|
86
|
-
handler: info.handler
|
|
87
|
-
}));
|
|
88
|
-
} catch (error) {
|
|
89
|
-
logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
90
|
-
return [];
|
|
54
|
+
if (configuredEntry) {
|
|
55
|
+
const entryWithoutExt = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry);
|
|
56
|
+
return resolveJsOrTsEntry(entryWithoutExt);
|
|
91
57
|
}
|
|
58
|
+
const apiRoot = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR);
|
|
59
|
+
return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
|
|
92
60
|
}
|
|
93
61
|
async refreshCrossProjectPolicy(mod) {
|
|
94
62
|
let contractSources = [];
|
|
@@ -104,24 +72,15 @@ class EffectAdapter {
|
|
|
104
72
|
} catch (error) {
|
|
105
73
|
logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
106
74
|
}
|
|
107
|
-
|
|
108
|
-
if (policy?.enabled) {
|
|
109
|
-
const lambdaSources = await this.collectLambdaContractSources();
|
|
110
|
-
if (lambdaSources.length > 0) {
|
|
111
|
-
contractSources = [
|
|
112
|
-
...contractSources,
|
|
113
|
-
...lambdaSources
|
|
114
|
-
];
|
|
115
|
-
policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
75
|
+
const policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
118
76
|
this.crossProjectPolicy = policy;
|
|
119
77
|
if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy is enabled but no HttpApi endpoints could be reflected; operation-contract matching is disabled for this server (envelope and operation-context checks still apply).');
|
|
120
78
|
}
|
|
121
79
|
async loadEffectHandlerFromModule(mod) {
|
|
80
|
+
const effectConfig = this.api.getServerConfig()?.bff?.effect;
|
|
122
81
|
return resolveEffectBffModuleHandler(mod, {
|
|
123
|
-
openapi:
|
|
124
|
-
dataPlatform:
|
|
82
|
+
openapi: effectConfig?.openapi,
|
|
83
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
125
84
|
validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
|
|
126
85
|
onWarning: (message)=>{
|
|
127
86
|
logger.warn(message);
|
|
@@ -155,13 +114,12 @@ class EffectAdapter {
|
|
|
155
114
|
await this.refreshCrossProjectPolicy(mod);
|
|
156
115
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
157
116
|
if (!loaded) {
|
|
158
|
-
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer }
|
|
117
|
+
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
|
|
159
118
|
this.handler = null;
|
|
160
119
|
return;
|
|
161
120
|
}
|
|
162
121
|
this.handler = loaded.handler;
|
|
163
122
|
this.dispose = loaded.dispose || null;
|
|
164
|
-
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
165
123
|
}
|
|
166
124
|
async disposeCurrentHandler() {
|
|
167
125
|
if (!this.dispose) return;
|
|
@@ -213,7 +171,6 @@ class EffectAdapter {
|
|
|
213
171
|
this.handler = null;
|
|
214
172
|
this.dispose = null;
|
|
215
173
|
this.prefix = '/api';
|
|
216
|
-
this.policyEnforcedInMiddleware = false;
|
|
217
174
|
this.registerMiddleware = async (options)=>{
|
|
218
175
|
const { prefix, enableHandleWeb } = options;
|
|
219
176
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -224,7 +181,7 @@ class EffectAdapter {
|
|
|
224
181
|
this.prefix = prefix || this.prefix;
|
|
225
182
|
await this.reloadHandler();
|
|
226
183
|
this.effectMiddleware = {
|
|
227
|
-
name: 'effect-
|
|
184
|
+
name: 'effect-api-handler',
|
|
228
185
|
path: enableHandleWeb ? '*' : `${prefix}/*`,
|
|
229
186
|
method: 'all',
|
|
230
187
|
order: 'post',
|
|
@@ -232,11 +189,7 @@ class EffectAdapter {
|
|
|
232
189
|
handler: async (c, next)=>{
|
|
233
190
|
if (!this.handler) {
|
|
234
191
|
if (enableHandleWeb) return void await next();
|
|
235
|
-
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/
|
|
236
|
-
}
|
|
237
|
-
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
238
|
-
const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
|
|
239
|
-
if (denial) return denial;
|
|
192
|
+
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
|
|
240
193
|
}
|
|
241
194
|
let response;
|
|
242
195
|
try {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const kEffectContext = Symbol.for('modernjs.plugin-bff.edgeEffectContext');
|
|
2
|
+
const kEffectContextStorage = Symbol.for('modernjs.plugin-bff.edgeEffectContextStorage');
|
|
3
|
+
const globalStore = globalThis;
|
|
4
|
+
const isPromiseLike = (value)=>'object' == typeof value && null !== value && 'then' in value && 'function' == typeof value.then;
|
|
5
|
+
const getRuntimeStorage = ()=>{
|
|
6
|
+
if (globalStore[kEffectContextStorage]) return globalStore[kEffectContextStorage];
|
|
7
|
+
if ('function' == typeof globalStore.AsyncLocalStorage) globalStore[kEffectContextStorage] = new globalStore.AsyncLocalStorage();
|
|
8
|
+
return globalStore[kEffectContextStorage];
|
|
9
|
+
};
|
|
10
|
+
const runWithEffectContext = (context, cb)=>{
|
|
11
|
+
const storage = getRuntimeStorage();
|
|
12
|
+
if (storage) return storage.run(context, cb);
|
|
13
|
+
const previous = globalStore[kEffectContext];
|
|
14
|
+
globalStore[kEffectContext] = context;
|
|
15
|
+
try {
|
|
16
|
+
const result = cb();
|
|
17
|
+
if (isPromiseLike(result)) return Promise.resolve(result).finally(()=>{
|
|
18
|
+
globalStore[kEffectContext] = previous;
|
|
19
|
+
});
|
|
20
|
+
globalStore[kEffectContext] = previous;
|
|
21
|
+
return result;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
globalStore[kEffectContext] = previous;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const useEffectContext = ()=>{
|
|
28
|
+
const context = getRuntimeStorage()?.getStore() ?? globalStore[kEffectContext];
|
|
29
|
+
if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
|
|
30
|
+
return context;
|
|
31
|
+
};
|
|
32
|
+
const useOperationContext = ()=>useEffectContext().operationContext;
|
|
33
|
+
export { createEffectOperationContext } from "./operation-context.mjs";
|
|
34
|
+
export { runWithEffectContext, useEffectContext, useOperationContext };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
2
|
+
import { runWithEffectContext, useEffectContext, useOperationContext } from "./edge-context.mjs";
|
|
2
3
|
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
3
4
|
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
4
5
|
export * from "./handler.mjs";
|
|
@@ -50,7 +51,7 @@ async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
|
50
51
|
const effectRequest = createRequestForMountedPrefix(request, options.prefix);
|
|
51
52
|
const effectContext = createEdgeEffectContext(request, effectRequest, options);
|
|
52
53
|
try {
|
|
53
|
-
const response = handler.length > 1 ?
|
|
54
|
+
const response = await runWithEffectContext(effectContext, ()=>handler.length > 1 ? handler(effectRequest, effectContext) : handler(effectRequest));
|
|
54
55
|
if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
|
|
55
56
|
return new Response(response.body, response);
|
|
56
57
|
} catch (error) {
|
|
@@ -62,13 +63,14 @@ async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
|
62
63
|
return createRuntimeErrorResponse(error);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
const createEffectBffTestHandler = createEffectBffEdgeHandler;
|
|
65
67
|
async function createEffectBffEdgeHandler(options) {
|
|
66
68
|
const loaded = await resolveEffectBffModuleHandler(options.module, {
|
|
67
69
|
openapi: options.openapi,
|
|
68
70
|
dataPlatform: options.dataPlatform,
|
|
69
71
|
onWarning: options.onWarning
|
|
70
72
|
});
|
|
71
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
73
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
72
74
|
return {
|
|
73
75
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
74
76
|
...dispatchOptions,
|
|
@@ -80,4 +82,4 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
80
82
|
}
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
|
-
export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };
|
|
85
|
+
export { createEffectBffEdgeHandler, createEffectBffTestHandler, createEffectOperationContext, dispatchEffectBffRequest, runWithEffectContext, useEffectContext, useOperationContext };
|
|
@@ -12,6 +12,8 @@ export * from "effect/unstable/httpapi";
|
|
|
12
12
|
export * from "effect/unstable/rpc";
|
|
13
13
|
import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
|
|
14
14
|
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
15
|
+
const isEffectServiceContext = (context)=>'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
16
|
+
const toEffectServiceContext = (context)=>isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
15
17
|
const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
|
|
16
18
|
function isValidatorAwareHandlerFactory(factory) {
|
|
17
19
|
return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
|
|
@@ -329,7 +331,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
|
|
|
329
331
|
]);
|
|
330
332
|
function createLoaderMaterializedClientPlaceholder() {
|
|
331
333
|
const explain = (property)=>{
|
|
332
|
-
throw new Error(`[BFF][Effect]
|
|
334
|
+
throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
|
|
333
335
|
};
|
|
334
336
|
return new Proxy(Object.create(null), {
|
|
335
337
|
get (_target, property) {
|
|
@@ -370,7 +372,7 @@ function createHttpApiHandler(options) {
|
|
|
370
372
|
if (preparedRequest instanceof Response) return preparedRequest;
|
|
371
373
|
const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
|
|
372
374
|
if (validationError) return validationError;
|
|
373
|
-
return httpApiHandler.handler(preparedRequest, context
|
|
375
|
+
return httpApiHandler.handler(preparedRequest, toEffectServiceContext(context));
|
|
374
376
|
};
|
|
375
377
|
const handleBatchRequest = async (request, context)=>{
|
|
376
378
|
const mountedPrefix = getMountedPrefixFromContext(request, context);
|
|
@@ -492,7 +494,7 @@ function createHttpApiHandler(options) {
|
|
|
492
494
|
if (isRpcRequest(request, rpcPath)) {
|
|
493
495
|
const policyDenial = options.validateRequest?.(request);
|
|
494
496
|
if (policyDenial) return policyDenial;
|
|
495
|
-
return rpcHandler.handler(request, context
|
|
497
|
+
return rpcHandler.handler(request, toEffectServiceContext(context));
|
|
496
498
|
}
|
|
497
499
|
return handleHttpApiRequest(request);
|
|
498
500
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
|
|
2
2
|
export * from "./handler.mjs";
|
|
3
|
-
export { createEffectOperationContext, useEffectContext, useOperationContext } from "./context.mjs";
|
|
3
|
+
export { createEffectOperationContext, runWithEffectContext, useEffectContext, useOperationContext } from "./context.mjs";
|
|
4
|
+
export { createEffectBffEdgeHandler, createEffectBffTestHandler, dispatchEffectBffRequest } from "./edge.mjs";
|
|
4
5
|
export { __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry };
|
|
@@ -17,16 +17,18 @@ function isEffectServiceContext(context) {
|
|
|
17
17
|
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
18
18
|
}
|
|
19
19
|
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
20
|
+
const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries must export defineEffectBff(...) or a { api, layer } HttpApi module. Raw handler exports, default request handlers, and unbranded custom createHandler factories are not valid Effect API entries.';
|
|
21
|
+
function rejectLegacyEffectModuleShape(options, shape) {
|
|
22
|
+
options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
20
25
|
function callEffectBffRequestHandler(handler, request, context) {
|
|
21
26
|
return void 0 === context ? handler(request) : handler(request, context);
|
|
22
27
|
}
|
|
23
|
-
function createLoadedHandler(webHandler
|
|
28
|
+
function createLoadedHandler(webHandler) {
|
|
24
29
|
return {
|
|
25
30
|
handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
|
|
26
|
-
dispose: webHandler.dispose
|
|
27
|
-
...appliesRequestValidator ? {
|
|
28
|
-
appliesRequestValidator: true
|
|
29
|
-
} : {}
|
|
31
|
+
dispose: webHandler.dispose
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
function createLoadedHttpApiHandler(webHandler) {
|
|
@@ -35,18 +37,23 @@ function createLoadedHttpApiHandler(webHandler) {
|
|
|
35
37
|
const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
36
38
|
return webHandler.handler(request, effectContext);
|
|
37
39
|
},
|
|
38
|
-
dispose: webHandler.dispose
|
|
39
|
-
appliesRequestValidator: true
|
|
40
|
+
dispose: webHandler.dispose
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
43
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
44
|
-
handler
|
|
45
|
-
|
|
44
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
45
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
46
|
+
return {
|
|
47
|
+
handler: normalizedModule.handler
|
|
48
|
+
};
|
|
49
|
+
}
|
|
46
50
|
const entry = normalizedModule.default;
|
|
47
|
-
if (isRequestHandler(entry))
|
|
48
|
-
handler
|
|
49
|
-
|
|
51
|
+
if (isRequestHandler(entry)) {
|
|
52
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
|
|
53
|
+
return {
|
|
54
|
+
handler: entry
|
|
55
|
+
};
|
|
56
|
+
}
|
|
50
57
|
if (isRecord(entry)) normalizedModule = {
|
|
51
58
|
...normalizedModule,
|
|
52
59
|
...entry
|
|
@@ -58,19 +65,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
58
65
|
handler: maybeHandler
|
|
59
66
|
};
|
|
60
67
|
}
|
|
61
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
62
|
-
handler
|
|
63
|
-
|
|
68
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
69
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
70
|
+
return {
|
|
71
|
+
handler: normalizedModule.handler
|
|
72
|
+
};
|
|
73
|
+
}
|
|
64
74
|
if ('function' == typeof normalizedModule.createHandler) {
|
|
65
75
|
const factory = normalizedModule.createHandler;
|
|
66
76
|
const validatorAware = isValidatorAwareHandlerFactory(factory);
|
|
67
|
-
if (!validatorAware &&
|
|
77
|
+
if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
68
78
|
const webHandler = factory({
|
|
69
79
|
openapi: options.openapi,
|
|
70
80
|
dataPlatform: options.dataPlatform,
|
|
71
81
|
validateRequest: options.validateRequest
|
|
72
82
|
});
|
|
73
|
-
return createLoadedHandler(webHandler
|
|
83
|
+
return createLoadedHandler(webHandler);
|
|
74
84
|
}
|
|
75
85
|
if (isEffectApiDefinition(normalizedModule)) {
|
|
76
86
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
@@ -94,17 +104,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
94
104
|
...value
|
|
95
105
|
};
|
|
96
106
|
};
|
|
97
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
98
|
-
handler
|
|
99
|
-
|
|
107
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
108
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
|
|
109
|
+
return Promise.resolve({
|
|
110
|
+
handler: normalizedModule.handler
|
|
111
|
+
});
|
|
112
|
+
}
|
|
100
113
|
const entry = normalizedModule.default;
|
|
101
|
-
if (isRequestHandler(entry))
|
|
102
|
-
handler
|
|
103
|
-
|
|
114
|
+
if (isRequestHandler(entry)) {
|
|
115
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
|
|
116
|
+
return Promise.resolve({
|
|
117
|
+
handler: entry
|
|
118
|
+
});
|
|
119
|
+
}
|
|
104
120
|
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
105
|
-
if (isRequestHandler(out))
|
|
106
|
-
handler
|
|
107
|
-
|
|
121
|
+
if (isRequestHandler(out)) {
|
|
122
|
+
if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
|
|
123
|
+
return {
|
|
124
|
+
handler: out
|
|
125
|
+
};
|
|
126
|
+
}
|
|
108
127
|
mergeRuntimeExports(out);
|
|
109
128
|
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
110
129
|
});
|
|
@@ -127,14 +127,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
|
|
|
127
127
|
const client = __generated.client;
|
|
128
128
|
const operationManifest = __generated.operationManifest;
|
|
129
129
|
const createEffectRequestContext = __generated.createEffectRequestContext;
|
|
130
|
-
const
|
|
130
|
+
const apiModule = {
|
|
131
131
|
client,
|
|
132
132
|
operationManifest,
|
|
133
133
|
createEffectRequestContext,
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
export { client, createEffectRequestContext, operationManifest };
|
|
137
|
-
export default
|
|
137
|
+
export default apiModule;
|
|
138
138
|
`;
|
|
139
139
|
}
|
|
140
140
|
function renderClientShape(endpoints, valueType) {
|
|
@@ -207,13 +207,13 @@ export declare const createEffectRequestContext: (
|
|
|
207
207
|
requestContext: Record<string, unknown>,
|
|
208
208
|
) => EffectRequestContext;
|
|
209
209
|
export declare const operationManifest: GeneratedEffectOperationManifest;
|
|
210
|
-
declare const
|
|
210
|
+
declare const apiModule: {
|
|
211
211
|
client: GeneratedEffectClient;
|
|
212
212
|
createEffectRequestContext: typeof createEffectRequestContext;
|
|
213
213
|
operationManifest: GeneratedEffectOperationManifest;
|
|
214
214
|
};
|
|
215
215
|
|
|
216
|
-
export default
|
|
216
|
+
export default apiModule;
|
|
217
217
|
`;
|
|
218
218
|
}
|
|
219
219
|
async function generateEffectClient(options) {
|
|
@@ -236,9 +236,14 @@ async function generateEffectClientCode(options) {
|
|
|
236
236
|
}
|
|
237
237
|
function resolveEffectEntryFile(options) {
|
|
238
238
|
const { appDir, apiDir, effectEntry } = options;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
const resolveEntry = (entryWithoutExt)=>{
|
|
240
|
+
if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
|
|
241
|
+
return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
242
|
+
};
|
|
243
|
+
if (effectEntry) {
|
|
244
|
+
const entryWithoutExt = path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry);
|
|
245
|
+
return resolveEntry(entryWithoutExt);
|
|
246
|
+
}
|
|
247
|
+
return resolveEntry(path.resolve(apiDir, 'index'));
|
|
243
248
|
}
|
|
244
249
|
export { generateEffectClient, generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import __rslib_shim_module__ from "node:module";
|
|
2
2
|
const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
|
|
3
|
-
import { ApiRouter } from "@modern-js/bff-core";
|
|
4
3
|
import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
|
|
5
4
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
6
5
|
import path from "path";
|
|
@@ -54,43 +53,12 @@ class EffectAdapter {
|
|
|
54
53
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
55
54
|
const bffConfig = this.api.getServerConfig()?.bff;
|
|
56
55
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
62
|
-
if (!enableHandleWeb) return true;
|
|
63
|
-
const normalized = normalizePrefix(prefix);
|
|
64
|
-
if (!normalized) return true;
|
|
65
|
-
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
66
|
-
}
|
|
67
|
-
async collectLambdaContractSources() {
|
|
68
|
-
try {
|
|
69
|
-
const serverContext = this.api.getServerContext();
|
|
70
|
-
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
71
|
-
if (!appDir) return [];
|
|
72
|
-
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
|
|
73
|
-
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
|
|
74
|
-
if (!await fs.pathExists(lambdaDir)) return [];
|
|
75
|
-
const apiRouter = new ApiRouter({
|
|
76
|
-
appDir,
|
|
77
|
-
apiDir,
|
|
78
|
-
lambdaDir,
|
|
79
|
-
prefix: this.prefix,
|
|
80
|
-
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
81
|
-
});
|
|
82
|
-
const handlerInfos = await apiRouter.getApiHandlers();
|
|
83
|
-
return handlerInfos.map((info)=>({
|
|
84
|
-
name: info.name,
|
|
85
|
-
httpMethod: info.httpMethod,
|
|
86
|
-
routePath: info.routePath,
|
|
87
|
-
filename: info.filename,
|
|
88
|
-
handler: info.handler
|
|
89
|
-
}));
|
|
90
|
-
} catch (error) {
|
|
91
|
-
logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
92
|
-
return [];
|
|
56
|
+
if (configuredEntry) {
|
|
57
|
+
const entryWithoutExt = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry);
|
|
58
|
+
return resolveJsOrTsEntry(entryWithoutExt);
|
|
93
59
|
}
|
|
60
|
+
const apiRoot = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR);
|
|
61
|
+
return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
|
|
94
62
|
}
|
|
95
63
|
async refreshCrossProjectPolicy(mod) {
|
|
96
64
|
let contractSources = [];
|
|
@@ -106,24 +74,15 @@ class EffectAdapter {
|
|
|
106
74
|
} catch (error) {
|
|
107
75
|
logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
108
76
|
}
|
|
109
|
-
|
|
110
|
-
if (policy?.enabled) {
|
|
111
|
-
const lambdaSources = await this.collectLambdaContractSources();
|
|
112
|
-
if (lambdaSources.length > 0) {
|
|
113
|
-
contractSources = [
|
|
114
|
-
...contractSources,
|
|
115
|
-
...lambdaSources
|
|
116
|
-
];
|
|
117
|
-
policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
77
|
+
const policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
120
78
|
this.crossProjectPolicy = policy;
|
|
121
79
|
if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy is enabled but no HttpApi endpoints could be reflected; operation-contract matching is disabled for this server (envelope and operation-context checks still apply).');
|
|
122
80
|
}
|
|
123
81
|
async loadEffectHandlerFromModule(mod) {
|
|
82
|
+
const effectConfig = this.api.getServerConfig()?.bff?.effect;
|
|
124
83
|
return resolveEffectBffModuleHandler(mod, {
|
|
125
|
-
openapi:
|
|
126
|
-
dataPlatform:
|
|
84
|
+
openapi: effectConfig?.openapi,
|
|
85
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
127
86
|
validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
|
|
128
87
|
onWarning: (message)=>{
|
|
129
88
|
logger.warn(message);
|
|
@@ -157,13 +116,12 @@ class EffectAdapter {
|
|
|
157
116
|
await this.refreshCrossProjectPolicy(mod);
|
|
158
117
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
159
118
|
if (!loaded) {
|
|
160
|
-
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer }
|
|
119
|
+
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
|
|
161
120
|
this.handler = null;
|
|
162
121
|
return;
|
|
163
122
|
}
|
|
164
123
|
this.handler = loaded.handler;
|
|
165
124
|
this.dispose = loaded.dispose || null;
|
|
166
|
-
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
167
125
|
}
|
|
168
126
|
async disposeCurrentHandler() {
|
|
169
127
|
if (!this.dispose) return;
|
|
@@ -215,7 +173,6 @@ class EffectAdapter {
|
|
|
215
173
|
this.handler = null;
|
|
216
174
|
this.dispose = null;
|
|
217
175
|
this.prefix = '/api';
|
|
218
|
-
this.policyEnforcedInMiddleware = false;
|
|
219
176
|
this.registerMiddleware = async (options)=>{
|
|
220
177
|
const { prefix, enableHandleWeb } = options;
|
|
221
178
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -226,7 +183,7 @@ class EffectAdapter {
|
|
|
226
183
|
this.prefix = prefix || this.prefix;
|
|
227
184
|
await this.reloadHandler();
|
|
228
185
|
this.effectMiddleware = {
|
|
229
|
-
name: 'effect-
|
|
186
|
+
name: 'effect-api-handler',
|
|
230
187
|
path: enableHandleWeb ? '*' : `${prefix}/*`,
|
|
231
188
|
method: 'all',
|
|
232
189
|
order: 'post',
|
|
@@ -234,11 +191,7 @@ class EffectAdapter {
|
|
|
234
191
|
handler: async (c, next)=>{
|
|
235
192
|
if (!this.handler) {
|
|
236
193
|
if (enableHandleWeb) return void await next();
|
|
237
|
-
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/
|
|
238
|
-
}
|
|
239
|
-
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
240
|
-
const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
|
|
241
|
-
if (denial) return denial;
|
|
194
|
+
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
|
|
242
195
|
}
|
|
243
196
|
let response;
|
|
244
197
|
try {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const kEffectContext = Symbol.for('modernjs.plugin-bff.edgeEffectContext');
|
|
3
|
+
const kEffectContextStorage = Symbol.for('modernjs.plugin-bff.edgeEffectContextStorage');
|
|
4
|
+
const globalStore = globalThis;
|
|
5
|
+
const isPromiseLike = (value)=>'object' == typeof value && null !== value && 'then' in value && 'function' == typeof value.then;
|
|
6
|
+
const getRuntimeStorage = ()=>{
|
|
7
|
+
if (globalStore[kEffectContextStorage]) return globalStore[kEffectContextStorage];
|
|
8
|
+
if ('function' == typeof globalStore.AsyncLocalStorage) globalStore[kEffectContextStorage] = new globalStore.AsyncLocalStorage();
|
|
9
|
+
return globalStore[kEffectContextStorage];
|
|
10
|
+
};
|
|
11
|
+
const runWithEffectContext = (context, cb)=>{
|
|
12
|
+
const storage = getRuntimeStorage();
|
|
13
|
+
if (storage) return storage.run(context, cb);
|
|
14
|
+
const previous = globalStore[kEffectContext];
|
|
15
|
+
globalStore[kEffectContext] = context;
|
|
16
|
+
try {
|
|
17
|
+
const result = cb();
|
|
18
|
+
if (isPromiseLike(result)) return Promise.resolve(result).finally(()=>{
|
|
19
|
+
globalStore[kEffectContext] = previous;
|
|
20
|
+
});
|
|
21
|
+
globalStore[kEffectContext] = previous;
|
|
22
|
+
return result;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
globalStore[kEffectContext] = previous;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const useEffectContext = ()=>{
|
|
29
|
+
const context = getRuntimeStorage()?.getStore() ?? globalStore[kEffectContext];
|
|
30
|
+
if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
|
|
31
|
+
return context;
|
|
32
|
+
};
|
|
33
|
+
const useOperationContext = ()=>useEffectContext().operationContext;
|
|
34
|
+
export { createEffectOperationContext } from "./operation-context.mjs";
|
|
35
|
+
export { runWithEffectContext, useEffectContext, useOperationContext };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
3
|
+
import { runWithEffectContext, useEffectContext, useOperationContext } from "./edge-context.mjs";
|
|
3
4
|
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
4
5
|
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
5
6
|
export * from "./handler.mjs";
|
|
@@ -51,7 +52,7 @@ async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
|
51
52
|
const effectRequest = createRequestForMountedPrefix(request, options.prefix);
|
|
52
53
|
const effectContext = createEdgeEffectContext(request, effectRequest, options);
|
|
53
54
|
try {
|
|
54
|
-
const response = handler.length > 1 ?
|
|
55
|
+
const response = await runWithEffectContext(effectContext, ()=>handler.length > 1 ? handler(effectRequest, effectContext) : handler(effectRequest));
|
|
55
56
|
if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
|
|
56
57
|
return new Response(response.body, response);
|
|
57
58
|
} catch (error) {
|
|
@@ -63,13 +64,14 @@ async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
|
63
64
|
return createRuntimeErrorResponse(error);
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
const createEffectBffTestHandler = createEffectBffEdgeHandler;
|
|
66
68
|
async function createEffectBffEdgeHandler(options) {
|
|
67
69
|
const loaded = await resolveEffectBffModuleHandler(options.module, {
|
|
68
70
|
openapi: options.openapi,
|
|
69
71
|
dataPlatform: options.dataPlatform,
|
|
70
72
|
onWarning: options.onWarning
|
|
71
73
|
});
|
|
72
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
74
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
73
75
|
return {
|
|
74
76
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
75
77
|
...dispatchOptions,
|
|
@@ -81,4 +83,4 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
81
83
|
}
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
|
-
export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };
|
|
86
|
+
export { createEffectBffEdgeHandler, createEffectBffTestHandler, createEffectOperationContext, dispatchEffectBffRequest, runWithEffectContext, useEffectContext, useOperationContext };
|