@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.
@@ -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
- const defaultEntry = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR, 'effect', 'index');
56
- const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
57
- return resolveJsOrTsEntry(entryWithoutExt);
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
- let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
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: this.api.getServerConfig()?.bff?.effect?.openapi,
124
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.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 } or handler.`);
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-bff-handler',
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/effect/index or configure bff.effect.entry.'), c);
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 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
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 }, createHandler, or handler.');
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] effectBff.client.${String(property)} is not available here: the typed client only exists when this module is imported through the "@api/effect/*" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
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 ?? emptyEffectServiceContext);
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 ?? emptyEffectServiceContext);
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, appliesRequestValidator) {
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)) return {
44
- handler: normalizedModule.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)) return {
48
- handler: entry
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)) return {
62
- handler: normalizedModule.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 && void 0 !== options.validateRequest) options.onWarning?.('[BFF][Effect] Custom createHandler export detected: it cannot be verified to apply validateRequest (cross-project policy), so the policy is enforced by the adapter middleware on the outer request. Batched calls will be denied at the batch POST (it carries no per-operation contract); export defineEffectBff(...) to get per-batch-item enforcement.');
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, validatorAware);
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)) return Promise.resolve({
98
- handler: normalizedModule.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)) return Promise.resolve({
102
- handler: entry
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)) return {
106
- handler: out
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 effectBffModule = {
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 effectBffModule;
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 effectBffModule: {
210
+ declare const apiModule: {
211
211
  client: GeneratedEffectClient;
212
212
  createEffectRequestContext: typeof createEffectRequestContext;
213
213
  operationManifest: GeneratedEffectOperationManifest;
214
214
  };
215
215
 
216
- export default effectBffModule;
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 defaultEntry = path.resolve(apiDir, 'effect', 'index');
240
- const entryWithoutExt = effectEntry ? path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry) : defaultEntry;
241
- if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
242
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
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
- const defaultEntry = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR, 'effect', 'index');
58
- const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
59
- return resolveJsOrTsEntry(entryWithoutExt);
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
- let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
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: this.api.getServerConfig()?.bff?.effect?.openapi,
126
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.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 } or handler.`);
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-bff-handler',
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/effect/index or configure bff.effect.entry.'), c);
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 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
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 }, createHandler, or handler.');
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 };