@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.9 → 3.5.0-ultramodern.0
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.js +1 -1
- package/dist/cjs/runtime/effect/handler.js +1 -1
- 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.mjs +1 -1
- package/dist/esm/runtime/effect/handler.mjs +1 -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.mjs +1 -1
- package/dist/esm-node/runtime/effect/handler.mjs +1 -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/handler.d.ts +2 -5
- package/dist/types/runtime/effect/module.d.ts +3 -16
- package/package.json +12 -12
|
@@ -39,7 +39,6 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
39
39
|
__webpack_require__.d(__webpack_exports__, {
|
|
40
40
|
EffectAdapter: ()=>EffectAdapter
|
|
41
41
|
});
|
|
42
|
-
const bff_core_namespaceObject = require("@modern-js/bff-core");
|
|
43
42
|
const utils_namespaceObject = require("@modern-js/utils");
|
|
44
43
|
const httpapi_namespaceObject = require("effect/unstable/httpapi");
|
|
45
44
|
const external_path_namespaceObject = require("path");
|
|
@@ -94,43 +93,12 @@ class EffectAdapter {
|
|
|
94
93
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
95
94
|
const bffConfig = this.api.getServerConfig()?.bff;
|
|
96
95
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
102
|
-
if (!enableHandleWeb) return true;
|
|
103
|
-
const normalized = normalizePrefix(prefix);
|
|
104
|
-
if (!normalized) return true;
|
|
105
|
-
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
106
|
-
}
|
|
107
|
-
async collectLambdaContractSources() {
|
|
108
|
-
try {
|
|
109
|
-
const serverContext = this.api.getServerContext();
|
|
110
|
-
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
111
|
-
if (!appDir) return [];
|
|
112
|
-
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : external_path_default().resolve(appDir, utils_namespaceObject.API_DIR);
|
|
113
|
-
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : external_path_default().join(apiDir, 'lambda');
|
|
114
|
-
if (!await utils_namespaceObject.fs.pathExists(lambdaDir)) return [];
|
|
115
|
-
const apiRouter = new bff_core_namespaceObject.ApiRouter({
|
|
116
|
-
appDir,
|
|
117
|
-
apiDir,
|
|
118
|
-
lambdaDir,
|
|
119
|
-
prefix: this.prefix,
|
|
120
|
-
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
121
|
-
});
|
|
122
|
-
const handlerInfos = await apiRouter.getApiHandlers();
|
|
123
|
-
return handlerInfos.map((info)=>({
|
|
124
|
-
name: info.name,
|
|
125
|
-
httpMethod: info.httpMethod,
|
|
126
|
-
routePath: info.routePath,
|
|
127
|
-
filename: info.filename,
|
|
128
|
-
handler: info.handler
|
|
129
|
-
}));
|
|
130
|
-
} catch (error) {
|
|
131
|
-
utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
132
|
-
return [];
|
|
96
|
+
if (configuredEntry) {
|
|
97
|
+
const entryWithoutExt = external_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_path_default().resolve(appDirectory || process.cwd(), configuredEntry);
|
|
98
|
+
return resolveJsOrTsEntry(entryWithoutExt);
|
|
133
99
|
}
|
|
100
|
+
const apiRoot = external_path_default().resolve(appDirectory || process.cwd(), apiDirectory || utils_namespaceObject.API_DIR);
|
|
101
|
+
return resolveJsOrTsEntry(external_path_default().resolve(apiRoot, 'index'));
|
|
134
102
|
}
|
|
135
103
|
async refreshCrossProjectPolicy(mod) {
|
|
136
104
|
let contractSources = [];
|
|
@@ -146,24 +114,15 @@ class EffectAdapter {
|
|
|
146
114
|
} catch (error) {
|
|
147
115
|
utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
148
116
|
}
|
|
149
|
-
|
|
150
|
-
if (policy?.enabled) {
|
|
151
|
-
const lambdaSources = await this.collectLambdaContractSources();
|
|
152
|
-
if (lambdaSources.length > 0) {
|
|
153
|
-
contractSources = [
|
|
154
|
-
...contractSources,
|
|
155
|
-
...lambdaSources
|
|
156
|
-
];
|
|
157
|
-
policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
117
|
+
const policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
|
|
160
118
|
this.crossProjectPolicy = policy;
|
|
161
119
|
if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) utils_namespaceObject.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).');
|
|
162
120
|
}
|
|
163
121
|
async loadEffectHandlerFromModule(mod) {
|
|
122
|
+
const effectConfig = this.api.getServerConfig()?.bff?.effect;
|
|
164
123
|
return (0, external_module_js_namespaceObject.resolveEffectBffModuleHandler)(mod, {
|
|
165
|
-
openapi:
|
|
166
|
-
dataPlatform:
|
|
124
|
+
openapi: effectConfig?.openapi,
|
|
125
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
167
126
|
validateRequest: (request)=>(0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(request, this.crossProjectPolicy),
|
|
168
127
|
onWarning: (message)=>{
|
|
169
128
|
utils_namespaceObject.logger.warn(message);
|
|
@@ -197,13 +156,12 @@ class EffectAdapter {
|
|
|
197
156
|
await this.refreshCrossProjectPolicy(mod);
|
|
198
157
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
199
158
|
if (!loaded) {
|
|
200
|
-
utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer }
|
|
159
|
+
utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
|
|
201
160
|
this.handler = null;
|
|
202
161
|
return;
|
|
203
162
|
}
|
|
204
163
|
this.handler = loaded.handler;
|
|
205
164
|
this.dispose = loaded.dispose || null;
|
|
206
|
-
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
207
165
|
}
|
|
208
166
|
async disposeCurrentHandler() {
|
|
209
167
|
if (!this.dispose) return;
|
|
@@ -255,7 +213,6 @@ class EffectAdapter {
|
|
|
255
213
|
this.handler = null;
|
|
256
214
|
this.dispose = null;
|
|
257
215
|
this.prefix = '/api';
|
|
258
|
-
this.policyEnforcedInMiddleware = false;
|
|
259
216
|
this.registerMiddleware = async (options)=>{
|
|
260
217
|
const { prefix, enableHandleWeb } = options;
|
|
261
218
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -266,7 +223,7 @@ class EffectAdapter {
|
|
|
266
223
|
this.prefix = prefix || this.prefix;
|
|
267
224
|
await this.reloadHandler();
|
|
268
225
|
this.effectMiddleware = {
|
|
269
|
-
name: 'effect-
|
|
226
|
+
name: 'effect-api-handler',
|
|
270
227
|
path: enableHandleWeb ? '*' : `${prefix}/*`,
|
|
271
228
|
method: 'all',
|
|
272
229
|
order: 'post',
|
|
@@ -274,11 +231,7 @@ class EffectAdapter {
|
|
|
274
231
|
handler: async (c, next)=>{
|
|
275
232
|
if (!this.handler) {
|
|
276
233
|
if (enableHandleWeb) return void await next();
|
|
277
|
-
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/
|
|
278
|
-
}
|
|
279
|
-
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
280
|
-
const denial = (0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(c.req.raw, this.crossProjectPolicy);
|
|
281
|
-
if (denial) return denial;
|
|
234
|
+
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
|
|
282
235
|
}
|
|
283
236
|
let response;
|
|
284
237
|
try {
|
|
@@ -138,7 +138,7 @@ var __webpack_exports__ = {};
|
|
|
138
138
|
dataPlatform: options.dataPlatform,
|
|
139
139
|
onWarning: options.onWarning
|
|
140
140
|
});
|
|
141
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
141
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
142
142
|
return {
|
|
143
143
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
144
144
|
...dispatchOptions,
|
|
@@ -457,7 +457,7 @@ var __webpack_exports__ = {};
|
|
|
457
457
|
]);
|
|
458
458
|
function createLoaderMaterializedClientPlaceholder() {
|
|
459
459
|
const explain = (property)=>{
|
|
460
|
-
throw new Error(`[BFF][Effect]
|
|
460
|
+
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.`);
|
|
461
461
|
};
|
|
462
462
|
return new Proxy(Object.create(null), {
|
|
463
463
|
get (_target, property) {
|
|
@@ -49,16 +49,18 @@ function isEffectServiceContext(context) {
|
|
|
49
49
|
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
50
50
|
}
|
|
51
51
|
const emptyEffectServiceContext = Context_namespaceObject.empty();
|
|
52
|
+
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.';
|
|
53
|
+
function rejectLegacyEffectModuleShape(options, shape) {
|
|
54
|
+
options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
52
57
|
function callEffectBffRequestHandler(handler, request, context) {
|
|
53
58
|
return void 0 === context ? handler(request) : handler(request, context);
|
|
54
59
|
}
|
|
55
|
-
function createLoadedHandler(webHandler
|
|
60
|
+
function createLoadedHandler(webHandler) {
|
|
56
61
|
return {
|
|
57
62
|
handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
|
|
58
|
-
dispose: webHandler.dispose
|
|
59
|
-
...appliesRequestValidator ? {
|
|
60
|
-
appliesRequestValidator: true
|
|
61
|
-
} : {}
|
|
63
|
+
dispose: webHandler.dispose
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
function createLoadedHttpApiHandler(webHandler) {
|
|
@@ -67,18 +69,23 @@ function createLoadedHttpApiHandler(webHandler) {
|
|
|
67
69
|
const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
68
70
|
return webHandler.handler(request, effectContext);
|
|
69
71
|
},
|
|
70
|
-
dispose: webHandler.dispose
|
|
71
|
-
appliesRequestValidator: true
|
|
72
|
+
dispose: webHandler.dispose
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
75
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
76
|
-
handler
|
|
77
|
-
|
|
76
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
77
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
78
|
+
return {
|
|
79
|
+
handler: normalizedModule.handler
|
|
80
|
+
};
|
|
81
|
+
}
|
|
78
82
|
const entry = normalizedModule.default;
|
|
79
|
-
if (isRequestHandler(entry))
|
|
80
|
-
handler
|
|
81
|
-
|
|
83
|
+
if (isRequestHandler(entry)) {
|
|
84
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
|
|
85
|
+
return {
|
|
86
|
+
handler: entry
|
|
87
|
+
};
|
|
88
|
+
}
|
|
82
89
|
if (isRecord(entry)) normalizedModule = {
|
|
83
90
|
...normalizedModule,
|
|
84
91
|
...entry
|
|
@@ -90,19 +97,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
90
97
|
handler: maybeHandler
|
|
91
98
|
};
|
|
92
99
|
}
|
|
93
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
94
|
-
handler
|
|
95
|
-
|
|
100
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
101
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
102
|
+
return {
|
|
103
|
+
handler: normalizedModule.handler
|
|
104
|
+
};
|
|
105
|
+
}
|
|
96
106
|
if ('function' == typeof normalizedModule.createHandler) {
|
|
97
107
|
const factory = normalizedModule.createHandler;
|
|
98
108
|
const validatorAware = (0, external_handler_js_namespaceObject.isValidatorAwareHandlerFactory)(factory);
|
|
99
|
-
if (!validatorAware &&
|
|
109
|
+
if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
100
110
|
const webHandler = factory({
|
|
101
111
|
openapi: options.openapi,
|
|
102
112
|
dataPlatform: options.dataPlatform,
|
|
103
113
|
validateRequest: options.validateRequest
|
|
104
114
|
});
|
|
105
|
-
return createLoadedHandler(webHandler
|
|
115
|
+
return createLoadedHandler(webHandler);
|
|
106
116
|
}
|
|
107
117
|
if (isEffectApiDefinition(normalizedModule)) {
|
|
108
118
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
@@ -126,17 +136,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
126
136
|
...value
|
|
127
137
|
};
|
|
128
138
|
};
|
|
129
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
130
|
-
handler
|
|
131
|
-
|
|
139
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
140
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
|
|
141
|
+
return Promise.resolve({
|
|
142
|
+
handler: normalizedModule.handler
|
|
143
|
+
});
|
|
144
|
+
}
|
|
132
145
|
const entry = normalizedModule.default;
|
|
133
|
-
if (isRequestHandler(entry))
|
|
134
|
-
handler
|
|
135
|
-
|
|
146
|
+
if (isRequestHandler(entry)) {
|
|
147
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
|
|
148
|
+
return Promise.resolve({
|
|
149
|
+
handler: entry
|
|
150
|
+
});
|
|
151
|
+
}
|
|
136
152
|
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
137
|
-
if (isRequestHandler(out))
|
|
138
|
-
handler
|
|
139
|
-
|
|
153
|
+
if (isRequestHandler(out)) {
|
|
154
|
+
if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
|
|
155
|
+
return {
|
|
156
|
+
handler: out
|
|
157
|
+
};
|
|
158
|
+
}
|
|
140
159
|
mergeRuntimeExports(out);
|
|
141
160
|
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
142
161
|
});
|
|
@@ -172,14 +172,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
|
|
|
172
172
|
const client = __generated.client;
|
|
173
173
|
const operationManifest = __generated.operationManifest;
|
|
174
174
|
const createEffectRequestContext = __generated.createEffectRequestContext;
|
|
175
|
-
const
|
|
175
|
+
const apiModule = {
|
|
176
176
|
client,
|
|
177
177
|
operationManifest,
|
|
178
178
|
createEffectRequestContext,
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
export { client, createEffectRequestContext, operationManifest };
|
|
182
|
-
export default
|
|
182
|
+
export default apiModule;
|
|
183
183
|
`;
|
|
184
184
|
}
|
|
185
185
|
function renderClientShape(endpoints, valueType) {
|
|
@@ -252,13 +252,13 @@ export declare const createEffectRequestContext: (
|
|
|
252
252
|
requestContext: Record<string, unknown>,
|
|
253
253
|
) => EffectRequestContext;
|
|
254
254
|
export declare const operationManifest: GeneratedEffectOperationManifest;
|
|
255
|
-
declare const
|
|
255
|
+
declare const apiModule: {
|
|
256
256
|
client: GeneratedEffectClient;
|
|
257
257
|
createEffectRequestContext: typeof createEffectRequestContext;
|
|
258
258
|
operationManifest: GeneratedEffectOperationManifest;
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
-
export default
|
|
261
|
+
export default apiModule;
|
|
262
262
|
`;
|
|
263
263
|
}
|
|
264
264
|
async function generateEffectClient(options) {
|
|
@@ -281,10 +281,15 @@ async function generateEffectClientCode(options) {
|
|
|
281
281
|
}
|
|
282
282
|
function resolveEffectEntryFile(options) {
|
|
283
283
|
const { appDir, apiDir, effectEntry } = options;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
const resolveEntry = (entryWithoutExt)=>{
|
|
285
|
+
if (external_path_default().extname(entryWithoutExt)) return utils_namespaceObject.fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
|
|
286
|
+
return (0, utils_namespaceObject.findExists)(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
287
|
+
};
|
|
288
|
+
if (effectEntry) {
|
|
289
|
+
const entryWithoutExt = external_path_default().isAbsolute(effectEntry) ? effectEntry : external_path_default().resolve(appDir, effectEntry);
|
|
290
|
+
return resolveEntry(entryWithoutExt);
|
|
291
|
+
}
|
|
292
|
+
return resolveEntry(external_path_default().resolve(apiDir, 'index'));
|
|
288
293
|
}
|
|
289
294
|
exports.generateEffectClient = __webpack_exports__.generateEffectClient;
|
|
290
295
|
exports.generateEffectClientCode = __webpack_exports__.generateEffectClientCode;
|
|
@@ -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 {
|
|
@@ -68,7 +68,7 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
68
68
|
dataPlatform: options.dataPlatform,
|
|
69
69
|
onWarning: options.onWarning
|
|
70
70
|
});
|
|
71
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
71
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
72
72
|
return {
|
|
73
73
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
74
74
|
...dispatchOptions,
|
|
@@ -329,7 +329,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
|
|
|
329
329
|
]);
|
|
330
330
|
function createLoaderMaterializedClientPlaceholder() {
|
|
331
331
|
const explain = (property)=>{
|
|
332
|
-
throw new Error(`[BFF][Effect]
|
|
332
|
+
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
333
|
};
|
|
334
334
|
return new Proxy(Object.create(null), {
|
|
335
335
|
get (_target, property) {
|
|
@@ -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 {
|
|
@@ -69,7 +69,7 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
69
69
|
dataPlatform: options.dataPlatform,
|
|
70
70
|
onWarning: options.onWarning
|
|
71
71
|
});
|
|
72
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
72
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
73
73
|
return {
|
|
74
74
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
75
75
|
...dispatchOptions,
|
|
@@ -330,7 +330,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
|
|
|
330
330
|
]);
|
|
331
331
|
function createLoaderMaterializedClientPlaceholder() {
|
|
332
332
|
const explain = (property)=>{
|
|
333
|
-
throw new Error(`[BFF][Effect]
|
|
333
|
+
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.`);
|
|
334
334
|
};
|
|
335
335
|
return new Proxy(Object.create(null), {
|
|
336
336
|
get (_target, property) {
|
|
@@ -18,16 +18,18 @@ function isEffectServiceContext(context) {
|
|
|
18
18
|
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
19
19
|
}
|
|
20
20
|
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
21
|
+
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.';
|
|
22
|
+
function rejectLegacyEffectModuleShape(options, shape) {
|
|
23
|
+
options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
21
26
|
function callEffectBffRequestHandler(handler, request, context) {
|
|
22
27
|
return void 0 === context ? handler(request) : handler(request, context);
|
|
23
28
|
}
|
|
24
|
-
function createLoadedHandler(webHandler
|
|
29
|
+
function createLoadedHandler(webHandler) {
|
|
25
30
|
return {
|
|
26
31
|
handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
|
|
27
|
-
dispose: webHandler.dispose
|
|
28
|
-
...appliesRequestValidator ? {
|
|
29
|
-
appliesRequestValidator: true
|
|
30
|
-
} : {}
|
|
32
|
+
dispose: webHandler.dispose
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
function createLoadedHttpApiHandler(webHandler) {
|
|
@@ -36,18 +38,23 @@ function createLoadedHttpApiHandler(webHandler) {
|
|
|
36
38
|
const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
37
39
|
return webHandler.handler(request, effectContext);
|
|
38
40
|
},
|
|
39
|
-
dispose: webHandler.dispose
|
|
40
|
-
appliesRequestValidator: true
|
|
41
|
+
dispose: webHandler.dispose
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
44
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
45
|
-
handler
|
|
46
|
-
|
|
45
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
46
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
47
|
+
return {
|
|
48
|
+
handler: normalizedModule.handler
|
|
49
|
+
};
|
|
50
|
+
}
|
|
47
51
|
const entry = normalizedModule.default;
|
|
48
|
-
if (isRequestHandler(entry))
|
|
49
|
-
handler
|
|
50
|
-
|
|
52
|
+
if (isRequestHandler(entry)) {
|
|
53
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
|
|
54
|
+
return {
|
|
55
|
+
handler: entry
|
|
56
|
+
};
|
|
57
|
+
}
|
|
51
58
|
if (isRecord(entry)) normalizedModule = {
|
|
52
59
|
...normalizedModule,
|
|
53
60
|
...entry
|
|
@@ -59,19 +66,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
59
66
|
handler: maybeHandler
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
63
|
-
handler
|
|
64
|
-
|
|
69
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
70
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
71
|
+
return {
|
|
72
|
+
handler: normalizedModule.handler
|
|
73
|
+
};
|
|
74
|
+
}
|
|
65
75
|
if ('function' == typeof normalizedModule.createHandler) {
|
|
66
76
|
const factory = normalizedModule.createHandler;
|
|
67
77
|
const validatorAware = isValidatorAwareHandlerFactory(factory);
|
|
68
|
-
if (!validatorAware &&
|
|
78
|
+
if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
69
79
|
const webHandler = factory({
|
|
70
80
|
openapi: options.openapi,
|
|
71
81
|
dataPlatform: options.dataPlatform,
|
|
72
82
|
validateRequest: options.validateRequest
|
|
73
83
|
});
|
|
74
|
-
return createLoadedHandler(webHandler
|
|
84
|
+
return createLoadedHandler(webHandler);
|
|
75
85
|
}
|
|
76
86
|
if (isEffectApiDefinition(normalizedModule)) {
|
|
77
87
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
@@ -95,17 +105,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
95
105
|
...value
|
|
96
106
|
};
|
|
97
107
|
};
|
|
98
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
99
|
-
handler
|
|
100
|
-
|
|
108
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
109
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
|
|
110
|
+
return Promise.resolve({
|
|
111
|
+
handler: normalizedModule.handler
|
|
112
|
+
});
|
|
113
|
+
}
|
|
101
114
|
const entry = normalizedModule.default;
|
|
102
|
-
if (isRequestHandler(entry))
|
|
103
|
-
handler
|
|
104
|
-
|
|
115
|
+
if (isRequestHandler(entry)) {
|
|
116
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
|
|
117
|
+
return Promise.resolve({
|
|
118
|
+
handler: entry
|
|
119
|
+
});
|
|
120
|
+
}
|
|
105
121
|
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
106
|
-
if (isRequestHandler(out))
|
|
107
|
-
handler
|
|
108
|
-
|
|
122
|
+
if (isRequestHandler(out)) {
|
|
123
|
+
if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
|
|
124
|
+
return {
|
|
125
|
+
handler: out
|
|
126
|
+
};
|
|
127
|
+
}
|
|
109
128
|
mergeRuntimeExports(out);
|
|
110
129
|
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
111
130
|
});
|
|
@@ -129,14 +129,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
|
|
|
129
129
|
const client = __generated.client;
|
|
130
130
|
const operationManifest = __generated.operationManifest;
|
|
131
131
|
const createEffectRequestContext = __generated.createEffectRequestContext;
|
|
132
|
-
const
|
|
132
|
+
const apiModule = {
|
|
133
133
|
client,
|
|
134
134
|
operationManifest,
|
|
135
135
|
createEffectRequestContext,
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
export { client, createEffectRequestContext, operationManifest };
|
|
139
|
-
export default
|
|
139
|
+
export default apiModule;
|
|
140
140
|
`;
|
|
141
141
|
}
|
|
142
142
|
function renderClientShape(endpoints, valueType) {
|
|
@@ -209,13 +209,13 @@ export declare const createEffectRequestContext: (
|
|
|
209
209
|
requestContext: Record<string, unknown>,
|
|
210
210
|
) => EffectRequestContext;
|
|
211
211
|
export declare const operationManifest: GeneratedEffectOperationManifest;
|
|
212
|
-
declare const
|
|
212
|
+
declare const apiModule: {
|
|
213
213
|
client: GeneratedEffectClient;
|
|
214
214
|
createEffectRequestContext: typeof createEffectRequestContext;
|
|
215
215
|
operationManifest: GeneratedEffectOperationManifest;
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
export default
|
|
218
|
+
export default apiModule;
|
|
219
219
|
`;
|
|
220
220
|
}
|
|
221
221
|
async function generateEffectClient(options) {
|
|
@@ -238,9 +238,14 @@ async function generateEffectClientCode(options) {
|
|
|
238
238
|
}
|
|
239
239
|
function resolveEffectEntryFile(options) {
|
|
240
240
|
const { appDir, apiDir, effectEntry } = options;
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
const resolveEntry = (entryWithoutExt)=>{
|
|
242
|
+
if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
|
|
243
|
+
return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
244
|
+
};
|
|
245
|
+
if (effectEntry) {
|
|
246
|
+
const entryWithoutExt = path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry);
|
|
247
|
+
return resolveEntry(entryWithoutExt);
|
|
248
|
+
}
|
|
249
|
+
return resolveEntry(path.resolve(apiDir, 'index'));
|
|
245
250
|
}
|
|
246
251
|
export { generateEffectClient, generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile };
|
|
@@ -12,30 +12,14 @@ export declare class EffectAdapter {
|
|
|
12
12
|
private handler;
|
|
13
13
|
private dispose;
|
|
14
14
|
private prefix;
|
|
15
|
-
/**
|
|
16
|
-
* True when the loaded handler does NOT run the policy seam internally
|
|
17
|
-
* (plain `handler` exports); the adapter middleware enforces it instead.
|
|
18
|
-
*/
|
|
19
|
-
private policyEnforcedInMiddleware;
|
|
20
15
|
constructor(api: ServerPluginAPI);
|
|
21
16
|
registerMiddleware: (options: MiddlewareOptions) => Promise<void>;
|
|
22
17
|
onApiHandlersUpdated: () => Promise<void>;
|
|
23
18
|
private resolveEntryFile;
|
|
24
|
-
private isApiRequestPath;
|
|
25
|
-
/**
|
|
26
|
-
* Contract sources for the lambda-lane handlers hosted alongside the
|
|
27
|
-
* effect entry. Cross-project SDKs generate per-operation contracts for
|
|
28
|
-
* `api/lambda` handlers with the exact same `ApiRouter` derivation used
|
|
29
|
-
* here, so the server-side expected-contract map must include them —
|
|
30
|
-
* otherwise every lambda-lane operation of a hosted producer SDK would be
|
|
31
|
-
* denied as `unknown_operation_contract`.
|
|
32
|
-
*/
|
|
33
|
-
private collectLambdaContractSources;
|
|
34
19
|
/**
|
|
35
|
-
* Resolves the cross-project policy from the reflected HttpApi endpoints
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* SDKs.
|
|
20
|
+
* Resolves the cross-project policy from the reflected HttpApi endpoints so
|
|
21
|
+
* the expected operation contracts match the hashes the client generators
|
|
22
|
+
* stamp into generated SDKs.
|
|
39
23
|
*/
|
|
40
24
|
private refreshCrossProjectPolicy;
|
|
41
25
|
private loadEffectHandlerFromModule;
|
|
@@ -144,11 +144,8 @@ export type EffectRequestValidator = (request: Request) => Response | null | und
|
|
|
144
144
|
* seam runs inside the produced handler, including per batched item.
|
|
145
145
|
*
|
|
146
146
|
* `defineEffectBff` brands its factory automatically. Hand-written factories
|
|
147
|
-
* are NOT assumed to honor `validateRequest
|
|
148
|
-
*
|
|
149
|
-
* for unbranded factories instead of silently skipping the policy. Custom
|
|
150
|
-
* factories that do forward `validateRequest` into `createHttpApiHandler`
|
|
151
|
-
* may opt in by setting this symbol property to `true`.
|
|
147
|
+
* are NOT assumed to honor `validateRequest`, so strict module resolution
|
|
148
|
+
* rejects unbranded factories instead of silently skipping the policy.
|
|
152
149
|
*
|
|
153
150
|
* Registered via `Symbol.for` so independently loaded runtime copies agree.
|
|
154
151
|
*/
|
|
@@ -20,27 +20,14 @@ export type EffectApiModule = {
|
|
|
20
20
|
export type LoadedEffectBffHandler = {
|
|
21
21
|
handler: EffectBffRequestHandler;
|
|
22
22
|
dispose?: () => Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* True when the handler is known to apply `validateRequest` internally
|
|
25
|
-
* (including per batched item): `{ api, layer }` modules resolved through
|
|
26
|
-
* `createHttpApiHandler`, and `createHandler` factories branded by
|
|
27
|
-
* `defineEffectBff` (see `EFFECT_VALIDATOR_AWARE_FACTORY`). Plain
|
|
28
|
-
* `handler` exports and unbranded custom factories leave this unset; the
|
|
29
|
-
* adapter middleware must enforce the policy itself for those.
|
|
30
|
-
*/
|
|
31
|
-
appliesRequestValidator?: boolean;
|
|
32
23
|
};
|
|
33
24
|
export type ResolveEffectBffModuleHandlerOptions = {
|
|
34
25
|
openapi?: EffectBffOpenApiConfig;
|
|
35
26
|
dataPlatform?: EffectDataPlatformValidationOptions;
|
|
36
27
|
/**
|
|
37
|
-
* Server-seam request validator (cross-project policy).
|
|
38
|
-
* `createHttpApiHandler
|
|
39
|
-
*
|
|
40
|
-
* Note: plain `handler` exports and unbranded custom `createHandler`
|
|
41
|
-
* factories bypass this seam — the effect adapter enforces the policy in
|
|
42
|
-
* its middleware for those shapes instead (see
|
|
43
|
-
* `LoadedEffectBffHandler.appliesRequestValidator`).
|
|
28
|
+
* Server-seam request validator (cross-project policy). Strict Effect
|
|
29
|
+
* entries forward it into `createHttpApiHandler`, where it applies to
|
|
30
|
+
* direct, batched and rpc requests.
|
|
44
31
|
*/
|
|
45
32
|
validateRequest?: EffectRequestValidator;
|
|
46
33
|
onWarning?: (message: string) => void;
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.
|
|
20
|
+
"version": "3.5.0-ultramodern.0",
|
|
21
21
|
"types": "./dist/types/cli.d.ts",
|
|
22
22
|
"main": "./dist/cjs/cli.js",
|
|
23
23
|
"exports": {
|
|
@@ -157,12 +157,12 @@
|
|
|
157
157
|
"effect": "4.0.0-beta.89",
|
|
158
158
|
"qs": "^6.15.3",
|
|
159
159
|
"type-is": "^2.1.0",
|
|
160
|
-
"@modern-js/
|
|
161
|
-
"@modern-js/
|
|
162
|
-
"@modern-js/server-
|
|
163
|
-
"@modern-js/
|
|
164
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.
|
|
165
|
-
"@modern-js/
|
|
160
|
+
"@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.5.0-ultramodern.0",
|
|
161
|
+
"@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.5.0-ultramodern.0",
|
|
162
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.0",
|
|
163
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.0",
|
|
164
|
+
"@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.0",
|
|
165
|
+
"@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.5.0-ultramodern.0"
|
|
166
166
|
},
|
|
167
167
|
"devDependencies": {
|
|
168
168
|
"@rsbuild/core": "2.1.0",
|
|
@@ -174,11 +174,11 @@
|
|
|
174
174
|
"memfs": "^4.57.8",
|
|
175
175
|
"typescript": "^6.0.3",
|
|
176
176
|
"zod": "^4.4.3",
|
|
177
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.
|
|
178
|
-
"@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.
|
|
179
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.
|
|
180
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.
|
|
181
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.
|
|
177
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.0",
|
|
178
|
+
"@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.5.0-ultramodern.0",
|
|
179
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.0",
|
|
180
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.0",
|
|
181
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.0",
|
|
182
182
|
"@scripts/rstest-config": "2.66.0"
|
|
183
183
|
},
|
|
184
184
|
"sideEffects": false,
|