@bleedingdev/modern-js-plugin-bff 3.5.0-ultramodern.100 → 3.5.0-ultramodern.102
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/cli/generator.js +12 -1
- package/dist/cjs/cli.js +6 -2
- package/dist/cjs/loader.js +5 -4
- package/dist/cjs/runtime/effect/adapter/entry.js +13 -5
- package/dist/cjs/runtime/effect/handler/definition.js +1 -0
- package/dist/cjs/runtime/effect/handler/http.js +17 -6
- package/dist/cjs/utils/crossProjectApiPlugin.js +21 -2
- package/dist/cjs/utils/effectSourceLoader.js +63 -7
- package/dist/cjs/utils/pluginGenerator.js +4 -4
- package/dist/esm/cli/generator.mjs +13 -2
- package/dist/esm/cli.mjs +2 -0
- package/dist/esm/loader.mjs +5 -4
- package/dist/esm/runtime/effect/adapter/entry.mjs +13 -5
- package/dist/esm/runtime/effect/handler/definition.mjs +1 -0
- package/dist/esm/runtime/effect/handler/http.mjs +17 -6
- package/dist/esm/utils/crossProjectApiPlugin.mjs +16 -3
- package/dist/esm/utils/effectSourceLoader.mjs +64 -8
- package/dist/esm/utils/pluginGenerator.mjs +5 -5
- package/dist/esm-node/cli/generator.mjs +13 -2
- package/dist/esm-node/cli.mjs +2 -0
- package/dist/esm-node/loader.mjs +5 -4
- package/dist/esm-node/runtime/effect/adapter/entry.mjs +13 -5
- package/dist/esm-node/runtime/effect/handler/definition.mjs +1 -0
- package/dist/esm-node/runtime/effect/handler/http.mjs +17 -6
- package/dist/esm-node/utils/crossProjectApiPlugin.mjs +16 -3
- package/dist/esm-node/utils/effectSourceLoader.mjs +64 -8
- package/dist/esm-node/utils/pluginGenerator.mjs +5 -5
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/runtime/effect/handler/definition.d.ts +2 -12
- package/dist/types/runtime/effect/handler/http.d.ts +2 -1
- package/dist/types/runtime/effect/handler/types.d.ts +5 -0
- package/dist/types/utils/crossProjectApiPlugin.d.ts +2 -0
- package/dist/types/utils/pluginGenerator.d.ts +3 -1
- package/package.json +12 -12
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import node_crypto from "node:crypto";
|
|
2
2
|
import node_fs from "node:fs";
|
|
3
|
-
import { builtinModules } from "node:module";
|
|
3
|
+
import { builtinModules, createRequire } from "node:module";
|
|
4
4
|
import node_path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { build, transform } from "esbuild";
|
|
@@ -75,10 +75,29 @@ const nodeBuiltins = new Set([
|
|
|
75
75
|
...builtinModules.map((name)=>`node:${name}`)
|
|
76
76
|
]);
|
|
77
77
|
const dependencyResolutionMarker = 'modern-js-effect-dependency-resolution';
|
|
78
|
+
async function isEsmOnlyFile(filename) {
|
|
79
|
+
const extension = node_path.extname(filename).toLowerCase();
|
|
80
|
+
if ('.mjs' === extension || '.mts' === extension) return true;
|
|
81
|
+
if ('.cjs' === extension || '.cts' === extension) return false;
|
|
82
|
+
let directory = node_path.dirname(filename);
|
|
83
|
+
while(true){
|
|
84
|
+
const packagePath = node_path.join(directory, 'package.json');
|
|
85
|
+
try {
|
|
86
|
+
const packageJson = JSON.parse(await node_fs.promises.readFile(packagePath, 'utf8'));
|
|
87
|
+
return 'module' === packageJson.type;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!error || 'object' != typeof error || !('code' in error) || 'ENOENT' !== error.code) throw error;
|
|
90
|
+
}
|
|
91
|
+
const parent = node_path.dirname(directory);
|
|
92
|
+
if (parent === directory) return false;
|
|
93
|
+
directory = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
78
96
|
function isBareSpecifier(specifier) {
|
|
79
97
|
return !specifier.startsWith('.') && !specifier.startsWith('/') && !node_path.isAbsolute(specifier);
|
|
80
98
|
}
|
|
81
|
-
function externalizeInstalledDependencies() {
|
|
99
|
+
function externalizeInstalledDependencies(options) {
|
|
100
|
+
const runtimeRequire = createRequire(node_path.join(options.runtimeResolveDir, '__modern_effect_entry.cjs'));
|
|
82
101
|
return {
|
|
83
102
|
name: 'modern-js-effect-installed-dependencies',
|
|
84
103
|
setup (buildApi) {
|
|
@@ -108,10 +127,41 @@ function externalizeInstalledDependencies() {
|
|
|
108
127
|
path: args.path
|
|
109
128
|
};
|
|
110
129
|
const realPath = await node_fs.promises.realpath(resolution.path).catch(()=>resolution.path);
|
|
111
|
-
if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`)) {
|
|
131
|
+
const runtimeResolution = await buildApi.resolve(args.path, {
|
|
132
|
+
importer: '',
|
|
133
|
+
kind: args.kind,
|
|
134
|
+
namespace: 'file',
|
|
135
|
+
pluginData: {
|
|
136
|
+
[dependencyResolutionMarker]: true
|
|
137
|
+
},
|
|
138
|
+
resolveDir: options.runtimeResolveDir
|
|
139
|
+
});
|
|
140
|
+
if (0 === runtimeResolution.errors.length && runtimeResolution.path) {
|
|
141
|
+
if ('cjs' === options.format) {
|
|
142
|
+
let requiredPath;
|
|
143
|
+
try {
|
|
144
|
+
requiredPath = runtimeRequire.resolve(args.path);
|
|
145
|
+
} catch {
|
|
146
|
+
requiredPath = runtimeResolution.path;
|
|
147
|
+
}
|
|
148
|
+
if (await isEsmOnlyFile(requiredPath)) return {
|
|
149
|
+
namespace: resolution.namespace,
|
|
150
|
+
path: resolution.path,
|
|
151
|
+
pluginData: resolution.pluginData,
|
|
152
|
+
sideEffects: resolution.sideEffects,
|
|
153
|
+
suffix: resolution.suffix,
|
|
154
|
+
warnings: resolution.warnings,
|
|
155
|
+
watchDirs: resolution.watchDirs,
|
|
156
|
+
watchFiles: resolution.watchFiles
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
external: true,
|
|
161
|
+
path: args.path
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
115
165
|
return {
|
|
116
166
|
namespace: resolution.namespace,
|
|
117
167
|
path: resolution.path,
|
|
@@ -139,7 +189,10 @@ async function bundleEffectEntryForNode(options) {
|
|
|
139
189
|
outfile: options.entryPath,
|
|
140
190
|
platform: 'node',
|
|
141
191
|
plugins: [
|
|
142
|
-
externalizeInstalledDependencies(
|
|
192
|
+
externalizeInstalledDependencies({
|
|
193
|
+
format: options.format,
|
|
194
|
+
runtimeResolveDir: options.appDir
|
|
195
|
+
}),
|
|
143
196
|
preserveSourceModuleSemantics()
|
|
144
197
|
],
|
|
145
198
|
target: 'node20'
|
|
@@ -181,7 +234,10 @@ async function loadEffectSourceModule(options) {
|
|
|
181
234
|
outfile: outputPath,
|
|
182
235
|
platform: 'node',
|
|
183
236
|
plugins: [
|
|
184
|
-
externalizeInstalledDependencies(
|
|
237
|
+
externalizeInstalledDependencies({
|
|
238
|
+
format: 'esm',
|
|
239
|
+
runtimeResolveDir: appDir
|
|
240
|
+
}),
|
|
185
241
|
preserveSourceModuleSemantics()
|
|
186
242
|
],
|
|
187
243
|
sourcemap: 'inline',
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { fs, logger, normalizeToPosixPath } from "@modern-js/utils";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { API_DIR, DIST_DIR, LAMBDA_DIR, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK } from "./crossProjectApiPlugin.mjs";
|
|
4
|
-
function replaceContent(source, packageName, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework) {
|
|
5
|
-
const updatedSource = source.replace(new RegExp(PACKAGE_NAME, 'g'), packageName).replace(new RegExp(PREFIX, 'g'), prefix).replace(new RegExp(DIST_DIR, 'g'), normalizeToPosixPath(relativeDistPath)).replace(new RegExp(API_DIR, 'g'), normalizeToPosixPath(relativeApiPath)).replace(new RegExp(LAMBDA_DIR, 'g'), normalizeToPosixPath(relativeLambdaPath)).replace(new RegExp(RUNTIME_FRAMEWORK, 'g'), runtimeFramework);
|
|
3
|
+
import { API_DIR, DIST_DIR, EFFECT_ENTRY, LAMBDA_DIR, OPERATION_CONTRACTS_JSON, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK } from "./crossProjectApiPlugin.mjs";
|
|
4
|
+
function replaceContent(source, packageName, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts) {
|
|
5
|
+
const updatedSource = source.replace(new RegExp(PACKAGE_NAME, 'g'), packageName).replace(new RegExp(PREFIX, 'g'), prefix).replace(new RegExp(DIST_DIR, 'g'), normalizeToPosixPath(relativeDistPath)).replace(new RegExp(API_DIR, 'g'), normalizeToPosixPath(relativeApiPath)).replace(new RegExp(LAMBDA_DIR, 'g'), normalizeToPosixPath(relativeLambdaPath)).replace(new RegExp(RUNTIME_FRAMEWORK, 'g'), runtimeFramework).replace(new RegExp(EFFECT_ENTRY, 'g'), normalizeToPosixPath(relativeEffectEntry)).replace(`'${OPERATION_CONTRACTS_JSON}'`, ()=>JSON.stringify(JSON.stringify(operationContracts)));
|
|
6
6
|
return updatedSource;
|
|
7
7
|
}
|
|
8
|
-
async function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework }) {
|
|
8
|
+
async function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts }) {
|
|
9
9
|
try {
|
|
10
10
|
const packageContent = await fs.readFile(path.resolve(appDirectory, './package.json'), 'utf8');
|
|
11
11
|
const packageJson = JSON.parse(packageContent);
|
|
12
12
|
const pluginDir = path.resolve(appDirectory, `./${relativeDistPath}`, 'plugin');
|
|
13
13
|
const pluginPath = path.join(pluginDir, 'index.js');
|
|
14
14
|
const pluginTemplate = await fs.readFile(path.resolve(__dirname, 'crossProjectApiPlugin.js'), 'utf8');
|
|
15
|
-
const updatedPlugin = replaceContent(pluginTemplate, packageJson.name, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework);
|
|
15
|
+
const updatedPlugin = replaceContent(pluginTemplate, packageJson.name, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts);
|
|
16
16
|
await fs.ensureFile(pluginPath);
|
|
17
17
|
await fs.writeFile(pluginPath, updatedPlugin);
|
|
18
18
|
const typeContent = `import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import { ApiRouter } from "@modern-js/bff-core";
|
|
2
|
+
import { ApiRouter, buildOperationContractMap, deriveOperationVersion } from "@modern-js/bff-core";
|
|
3
3
|
import { compile } from "@modern-js/server-utils";
|
|
4
4
|
import { API_DIR, SHARED_DIR, fs, resolveServerTsconfig } from "@modern-js/utils";
|
|
5
5
|
import path from "path";
|
|
@@ -86,16 +86,27 @@ const createBffGenerator = (api)=>{
|
|
|
86
86
|
});
|
|
87
87
|
const lambdaDir = apiRouter.getLambdaDir();
|
|
88
88
|
const existLambda = apiRouter.isExistLambda();
|
|
89
|
+
const packageJson = fs.readJSONSync(path.resolve(appDirectory, 'package.json'));
|
|
90
|
+
const requestId = packageJson.name || process.env.npm_package_name;
|
|
91
|
+
const operationContracts = existLambda ? buildOperationContractMap({
|
|
92
|
+
handlers: await apiRouter.getApiHandlers(),
|
|
93
|
+
requestId,
|
|
94
|
+
operationVersion: deriveOperationVersion(packageJson.version)
|
|
95
|
+
}) : {};
|
|
89
96
|
const runtime = bff?.runtimeCreateRequest || RUNTIME_CREATE_REQUEST;
|
|
90
97
|
const relativeApiPath = path.relative(appDirectory, apiDirectory);
|
|
91
98
|
const relativeLambdaPath = path.relative(appDirectory, lambdaDir);
|
|
99
|
+
const sourceEffectEntry = 'effect' === bffRuntimeFramework ? resolveEffectSourceEntry(appDirectory, apiDirectory, bff?.effect?.entry) : void 0;
|
|
100
|
+
const relativeEffectEntry = sourceEffectEntry ? path.relative(appDirectory, sourceEffectEntry).replace(/\.(?:[cm]?ts|tsx|jsx)$/u, '.js') : '';
|
|
92
101
|
await pluginGenerator({
|
|
93
102
|
prefix,
|
|
94
103
|
appDirectory,
|
|
95
104
|
relativeDistPath,
|
|
96
105
|
relativeApiPath,
|
|
97
106
|
relativeLambdaPath,
|
|
98
|
-
runtimeFramework: 'hono' === bffRuntimeFramework ? 'hono' : 'effect'
|
|
107
|
+
runtimeFramework: 'hono' === bffRuntimeFramework ? 'hono' : 'effect',
|
|
108
|
+
relativeEffectEntry,
|
|
109
|
+
operationContracts
|
|
99
110
|
});
|
|
100
111
|
await clientGenerator({
|
|
101
112
|
prefix,
|
package/dist/esm-node/cli.mjs
CHANGED
package/dist/esm-node/loader.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import path from "path";
|
|
|
6
6
|
import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
|
|
7
7
|
const EFFECT_BFF_WORKER_RUNTIME_QUERY = 'modern-bff-runtime';
|
|
8
8
|
const EFFECT_BFF_WORKER_RUNTIME_SOURCE_QUERY = 'modern-bff-runtime-source';
|
|
9
|
+
const createErrorModule = (message)=>`throw new Error(${JSON.stringify(message)});`;
|
|
9
10
|
async function transformEffectRuntimeSource(source, filename) {
|
|
10
11
|
const swc = await import("@swc/core");
|
|
11
12
|
const result = await swc.transform(source, {
|
|
@@ -68,13 +69,13 @@ async function loader(source) {
|
|
|
68
69
|
onDependency: (dependency)=>this.addDependency(dependency)
|
|
69
70
|
});
|
|
70
71
|
if (code) return void callback(void 0, code);
|
|
71
|
-
callback(void 0, `
|
|
72
|
+
callback(void 0, createErrorModule(`Failed to generate Effect client for ${resourcePath}`));
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
74
75
|
const warning = `The file ${resourcePath} is not allowed to be imported in src directory, only API definition files are allowed.`;
|
|
75
76
|
if (!draftOptions.existLambda) {
|
|
76
77
|
logger.warn(warning);
|
|
77
|
-
callback(null,
|
|
78
|
+
callback(null, createErrorModule(warning));
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
const options = {
|
|
@@ -91,14 +92,14 @@ async function loader(source) {
|
|
|
91
92
|
const { lambdaDir } = draftOptions;
|
|
92
93
|
if (!resourcePath.startsWith(lambdaDir)) {
|
|
93
94
|
logger.warn(warning);
|
|
94
|
-
callback(null,
|
|
95
|
+
callback(null, createErrorModule(warning));
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
if (draftOptions.fetcher) options.fetcher = draftOptions.fetcher;
|
|
98
99
|
if (draftOptions.requestCreator) options.requestCreator = draftOptions.requestCreator;
|
|
99
100
|
options.requireResolve = require.resolve;
|
|
100
101
|
const result = await generateClient(options);
|
|
101
|
-
callback(void 0, result.isOk ? result.value :
|
|
102
|
+
callback(void 0, result.isOk ? result.value : createErrorModule(result.value));
|
|
102
103
|
}
|
|
103
104
|
const src_loader = loader;
|
|
104
105
|
export default src_loader;
|
|
@@ -23,23 +23,31 @@ function relativeAppPath(appDirectory, entryPath) {
|
|
|
23
23
|
const relativePath = path.relative(appDirectory, entryPath);
|
|
24
24
|
return '..' === relativePath || relativePath.startsWith(`..${path.sep}`) ? void 0 : relativePath;
|
|
25
25
|
}
|
|
26
|
+
function isDependencyPath(relativePath) {
|
|
27
|
+
return relativePath?.split(/[\\/]/u).includes('node_modules') ?? false;
|
|
28
|
+
}
|
|
26
29
|
function resolveEffectAdapterEntryFile(api) {
|
|
27
30
|
const { appDirectory, apiDirectory, distDirectory } = api.getServerContext();
|
|
28
31
|
const appRoot = path.resolve(appDirectory || process.cwd());
|
|
29
|
-
const productionRoot = isProd() && distDirectory ? distDirectory : void 0;
|
|
32
|
+
const productionRoot = isProd() && distDirectory ? path.resolve(distDirectory) : void 0;
|
|
30
33
|
const bffConfig = api.getServerConfig()?.bff;
|
|
31
34
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
32
35
|
if (configuredEntry) {
|
|
33
36
|
const sourceEntry = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appRoot, configuredEntry);
|
|
34
37
|
if (productionRoot) {
|
|
35
38
|
const relativeEntry = relativeAppPath(appRoot, sourceEntry);
|
|
39
|
+
if (void 0 === relativeEntry || isDependencyPath(relativeEntry)) return resolveJsOrTsEntry(sourceEntry);
|
|
36
40
|
return relativeEntry ? resolveJsOrTsEntry(builtEntryPath(path.resolve(productionRoot, relativeEntry))) : void 0;
|
|
37
41
|
}
|
|
38
42
|
return resolveJsOrTsEntry(sourceEntry);
|
|
39
43
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const resolvedApiDirectory = path.resolve(appRoot, apiDirectory || API_DIR);
|
|
45
|
+
const productionApiDirectory = productionRoot ? void 0 !== relativeAppPath(productionRoot, resolvedApiDirectory) ? resolvedApiDirectory : (()=>{
|
|
46
|
+
const relativeApiDirectory = relativeAppPath(appRoot, resolvedApiDirectory);
|
|
47
|
+
if (isDependencyPath(relativeApiDirectory)) return resolvedApiDirectory;
|
|
48
|
+
return relativeApiDirectory ? path.resolve(productionRoot, relativeApiDirectory) : void 0;
|
|
49
|
+
})() : resolvedApiDirectory;
|
|
50
|
+
if (!productionApiDirectory) return;
|
|
51
|
+
return resolveJsOrTsEntry(path.resolve(productionApiDirectory, 'index'));
|
|
44
52
|
}
|
|
45
53
|
export { resolveEffectAdapterEntryFile };
|
|
@@ -17,6 +17,7 @@ function defineEffectBff(definition) {
|
|
|
17
17
|
openapi: options?.openapi,
|
|
18
18
|
rpc: mergedRpcOptions,
|
|
19
19
|
dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform),
|
|
20
|
+
interceptRequest: definition.interceptRequest,
|
|
20
21
|
validateRequest: options?.validateRequest
|
|
21
22
|
});
|
|
22
23
|
};
|
|
@@ -13,23 +13,30 @@ function createHttpApiHandler(options) {
|
|
|
13
13
|
const openApiLayer = createOpenApiLayer(options.api, options.openapi);
|
|
14
14
|
const mergedLayer = openApiLayer ? __rspack_external_effect_Layer_69c46453.mergeAll(apiLayer, openApiLayer) : apiLayer;
|
|
15
15
|
const httpApiHandler = HttpRouter.toWebHandler(mergedLayer);
|
|
16
|
-
const
|
|
17
|
-
const policyDenial = options.validateRequest?.(request);
|
|
18
|
-
if (policyDenial) return policyDenial;
|
|
16
|
+
const dispatchHttpApiRequest = async (request, context)=>{
|
|
19
17
|
const preparedRequest = await prepareJsonRequestBody(request);
|
|
20
18
|
if (preparedRequest instanceof Response) return preparedRequest;
|
|
21
19
|
const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
|
|
22
20
|
if (validationError) return validationError;
|
|
23
21
|
return httpApiHandler.handler(preparedRequest, toEffectServiceContext(context));
|
|
24
22
|
};
|
|
23
|
+
const withRequestPolicy = async (request, context)=>{
|
|
24
|
+
const policyDenial = options.validateRequest?.(request);
|
|
25
|
+
if (policyDenial) return policyDenial;
|
|
26
|
+
const next = ()=>dispatchHttpApiRequest(request, context);
|
|
27
|
+
return options.interceptRequest ? options.interceptRequest({
|
|
28
|
+
request,
|
|
29
|
+
next
|
|
30
|
+
}) : next();
|
|
31
|
+
};
|
|
25
32
|
const batchHandler = createDataPlatformBatchRequestHandler({
|
|
26
33
|
dataPlatform: options.dataPlatform,
|
|
27
|
-
handleItem:
|
|
34
|
+
handleItem: withRequestPolicy
|
|
28
35
|
});
|
|
29
36
|
const handleHttpApiRequest = async (request, context)=>{
|
|
30
37
|
const pathname = getRequestPathname(request);
|
|
31
38
|
if (batchHandler.enabled && pathname === batchHandler.path) return batchHandler.handle(request, context);
|
|
32
|
-
return
|
|
39
|
+
return withRequestPolicy(request, context);
|
|
33
40
|
};
|
|
34
41
|
if (!options.rpc) return {
|
|
35
42
|
handler: handleHttpApiRequest,
|
|
@@ -44,7 +51,11 @@ function createHttpApiHandler(options) {
|
|
|
44
51
|
if (isRpcRequest(request, rpcPath)) {
|
|
45
52
|
const policyDenial = options.validateRequest?.(request);
|
|
46
53
|
if (policyDenial) return policyDenial;
|
|
47
|
-
|
|
54
|
+
const next = ()=>rpcHandler.handler(request, toEffectServiceContext(context));
|
|
55
|
+
return options.interceptRequest ? options.interceptRequest({
|
|
56
|
+
request,
|
|
57
|
+
next
|
|
58
|
+
}) : next();
|
|
48
59
|
}
|
|
49
60
|
return handleHttpApiRequest(request, context);
|
|
50
61
|
},
|
|
@@ -6,6 +6,8 @@ const API_DIR = '{apiDirectory}';
|
|
|
6
6
|
const LAMBDA_DIR = '{lambdaDirectory}';
|
|
7
7
|
const DIST_DIR = '{distDirectory}';
|
|
8
8
|
const RUNTIME_FRAMEWORK = '{runtimeFramework}';
|
|
9
|
+
const EFFECT_ENTRY = '{effectEntry}';
|
|
10
|
+
const OPERATION_CONTRACTS_JSON = '{operationContracts}';
|
|
9
11
|
const NODE_MODULES = 'node_modules';
|
|
10
12
|
const crossProjectApiPlugin = ()=>({
|
|
11
13
|
name: '@modern-js/plugin-independent-bff',
|
|
@@ -17,7 +19,8 @@ const crossProjectApiPlugin = ()=>({
|
|
|
17
19
|
const { appDirectory: originAppDirectory } = api.getAppContext();
|
|
18
20
|
const sdkPath = path.join(originAppDirectory, NODE_MODULES, PACKAGE_NAME);
|
|
19
21
|
const sdkDistPath = path.join(sdkPath, DIST_DIR);
|
|
20
|
-
const
|
|
22
|
+
const effectEntry = EFFECT_ENTRY ? path.join(sdkDistPath, EFFECT_ENTRY) : void 0;
|
|
23
|
+
const apiDirectory = effectEntry ? path.dirname(effectEntry) : path.join(sdkDistPath, API_DIR);
|
|
21
24
|
const lambdaDirectory = path.resolve(sdkDistPath, LAMBDA_DIR);
|
|
22
25
|
api.updateAppContext({
|
|
23
26
|
apiDirectory,
|
|
@@ -32,10 +35,16 @@ const crossProjectApiPlugin = ()=>({
|
|
|
32
35
|
}
|
|
33
36
|
const configuredRuntimeFramework = config?.bff?.runtimeFramework;
|
|
34
37
|
if (configuredRuntimeFramework && configuredRuntimeFramework !== RUNTIME_FRAMEWORK) throw new Error(`[${PACKAGE_NAME}] Runtime framework mismatch for cross-project BFF. Detected "${configuredRuntimeFramework}", but producer SDK requires "${RUNTIME_FRAMEWORK}".`);
|
|
38
|
+
resolvedConfig.bff ??= {};
|
|
35
39
|
resolvedConfig.bff.prefix = PREFIX;
|
|
36
40
|
resolvedConfig.bff.runtimeFramework = RUNTIME_FRAMEWORK;
|
|
41
|
+
if (EFFECT_ENTRY) resolvedConfig.bff.effect = {
|
|
42
|
+
...resolvedConfig.bff.effect,
|
|
43
|
+
entry: effectEntry
|
|
44
|
+
};
|
|
37
45
|
resolvedConfig.bff.isCrossProjectServer = true;
|
|
38
46
|
resolvedConfig.bff.requestId = resolvedConfig.bff.requestId || config?.bff?.requestId || PACKAGE_NAME || 'default';
|
|
47
|
+
const generatedOperationContracts = "{operationContracts}" === OPERATION_CONTRACTS_JSON ? {} : JSON.parse(OPERATION_CONTRACTS_JSON);
|
|
39
48
|
resolvedConfig.bff.crossProjectPolicy = {
|
|
40
49
|
...resolvedConfig.bff.crossProjectPolicy || {},
|
|
41
50
|
enabled: resolvedConfig.bff.crossProjectPolicy?.enabled ?? true,
|
|
@@ -44,7 +53,11 @@ const crossProjectApiPlugin = ()=>({
|
|
|
44
53
|
requireOperationContextDetails: resolvedConfig.bff.crossProjectPolicy?.requireOperationContextDetails ?? true,
|
|
45
54
|
requireOperationSchemaHash: resolvedConfig.bff.crossProjectPolicy?.requireOperationSchemaHash ?? true,
|
|
46
55
|
requireOperationVersion: resolvedConfig.bff.crossProjectPolicy?.requireOperationVersion ?? true,
|
|
47
|
-
allowUnknownOperations: resolvedConfig.bff.crossProjectPolicy?.allowUnknownOperations ?? false
|
|
56
|
+
allowUnknownOperations: resolvedConfig.bff.crossProjectPolicy?.allowUnknownOperations ?? false,
|
|
57
|
+
expectedOperationContracts: {
|
|
58
|
+
...resolvedConfig.bff.crossProjectPolicy?.expectedOperationContracts || {},
|
|
59
|
+
...generatedOperationContracts
|
|
60
|
+
}
|
|
48
61
|
};
|
|
49
62
|
return resolvedConfig;
|
|
50
63
|
});
|
|
@@ -52,4 +65,4 @@ const crossProjectApiPlugin = ()=>({
|
|
|
52
65
|
});
|
|
53
66
|
const utils_crossProjectApiPlugin = crossProjectApiPlugin;
|
|
54
67
|
export default utils_crossProjectApiPlugin;
|
|
55
|
-
export { API_DIR, DIST_DIR, LAMBDA_DIR, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK, crossProjectApiPlugin };
|
|
68
|
+
export { API_DIR, DIST_DIR, EFFECT_ENTRY, LAMBDA_DIR, OPERATION_CONTRACTS_JSON, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK, crossProjectApiPlugin };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import node_crypto from "node:crypto";
|
|
3
3
|
import node_fs from "node:fs";
|
|
4
|
-
import { builtinModules } from "node:module";
|
|
4
|
+
import { builtinModules, createRequire } from "node:module";
|
|
5
5
|
import node_path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { build, transform } from "esbuild";
|
|
@@ -76,10 +76,29 @@ const nodeBuiltins = new Set([
|
|
|
76
76
|
...builtinModules.map((name)=>`node:${name}`)
|
|
77
77
|
]);
|
|
78
78
|
const dependencyResolutionMarker = 'modern-js-effect-dependency-resolution';
|
|
79
|
+
async function isEsmOnlyFile(filename) {
|
|
80
|
+
const extension = node_path.extname(filename).toLowerCase();
|
|
81
|
+
if ('.mjs' === extension || '.mts' === extension) return true;
|
|
82
|
+
if ('.cjs' === extension || '.cts' === extension) return false;
|
|
83
|
+
let directory = node_path.dirname(filename);
|
|
84
|
+
while(true){
|
|
85
|
+
const packagePath = node_path.join(directory, 'package.json');
|
|
86
|
+
try {
|
|
87
|
+
const packageJson = JSON.parse(await node_fs.promises.readFile(packagePath, 'utf8'));
|
|
88
|
+
return 'module' === packageJson.type;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!error || 'object' != typeof error || !('code' in error) || 'ENOENT' !== error.code) throw error;
|
|
91
|
+
}
|
|
92
|
+
const parent = node_path.dirname(directory);
|
|
93
|
+
if (parent === directory) return false;
|
|
94
|
+
directory = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
79
97
|
function isBareSpecifier(specifier) {
|
|
80
98
|
return !specifier.startsWith('.') && !specifier.startsWith('/') && !node_path.isAbsolute(specifier);
|
|
81
99
|
}
|
|
82
|
-
function externalizeInstalledDependencies() {
|
|
100
|
+
function externalizeInstalledDependencies(options) {
|
|
101
|
+
const runtimeRequire = createRequire(node_path.join(options.runtimeResolveDir, '__modern_effect_entry.cjs'));
|
|
83
102
|
return {
|
|
84
103
|
name: 'modern-js-effect-installed-dependencies',
|
|
85
104
|
setup (buildApi) {
|
|
@@ -109,10 +128,41 @@ function externalizeInstalledDependencies() {
|
|
|
109
128
|
path: args.path
|
|
110
129
|
};
|
|
111
130
|
const realPath = await node_fs.promises.realpath(resolution.path).catch(()=>resolution.path);
|
|
112
|
-
if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`)) {
|
|
132
|
+
const runtimeResolution = await buildApi.resolve(args.path, {
|
|
133
|
+
importer: '',
|
|
134
|
+
kind: args.kind,
|
|
135
|
+
namespace: 'file',
|
|
136
|
+
pluginData: {
|
|
137
|
+
[dependencyResolutionMarker]: true
|
|
138
|
+
},
|
|
139
|
+
resolveDir: options.runtimeResolveDir
|
|
140
|
+
});
|
|
141
|
+
if (0 === runtimeResolution.errors.length && runtimeResolution.path) {
|
|
142
|
+
if ('cjs' === options.format) {
|
|
143
|
+
let requiredPath;
|
|
144
|
+
try {
|
|
145
|
+
requiredPath = runtimeRequire.resolve(args.path);
|
|
146
|
+
} catch {
|
|
147
|
+
requiredPath = runtimeResolution.path;
|
|
148
|
+
}
|
|
149
|
+
if (await isEsmOnlyFile(requiredPath)) return {
|
|
150
|
+
namespace: resolution.namespace,
|
|
151
|
+
path: resolution.path,
|
|
152
|
+
pluginData: resolution.pluginData,
|
|
153
|
+
sideEffects: resolution.sideEffects,
|
|
154
|
+
suffix: resolution.suffix,
|
|
155
|
+
warnings: resolution.warnings,
|
|
156
|
+
watchDirs: resolution.watchDirs,
|
|
157
|
+
watchFiles: resolution.watchFiles
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
external: true,
|
|
162
|
+
path: args.path
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
116
166
|
return {
|
|
117
167
|
namespace: resolution.namespace,
|
|
118
168
|
path: resolution.path,
|
|
@@ -140,7 +190,10 @@ async function bundleEffectEntryForNode(options) {
|
|
|
140
190
|
outfile: options.entryPath,
|
|
141
191
|
platform: 'node',
|
|
142
192
|
plugins: [
|
|
143
|
-
externalizeInstalledDependencies(
|
|
193
|
+
externalizeInstalledDependencies({
|
|
194
|
+
format: options.format,
|
|
195
|
+
runtimeResolveDir: options.appDir
|
|
196
|
+
}),
|
|
144
197
|
preserveSourceModuleSemantics()
|
|
145
198
|
],
|
|
146
199
|
target: 'node20'
|
|
@@ -182,7 +235,10 @@ async function loadEffectSourceModule(options) {
|
|
|
182
235
|
outfile: outputPath,
|
|
183
236
|
platform: 'node',
|
|
184
237
|
plugins: [
|
|
185
|
-
externalizeInstalledDependencies(
|
|
238
|
+
externalizeInstalledDependencies({
|
|
239
|
+
format: 'esm',
|
|
240
|
+
runtimeResolveDir: appDir
|
|
241
|
+
}),
|
|
186
242
|
preserveSourceModuleSemantics()
|
|
187
243
|
],
|
|
188
244
|
sourcemap: 'inline',
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { fs, logger, normalizeToPosixPath } from "@modern-js/utils";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { API_DIR, DIST_DIR, LAMBDA_DIR, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK } from "./crossProjectApiPlugin.mjs";
|
|
4
|
+
import { API_DIR, DIST_DIR, EFFECT_ENTRY, LAMBDA_DIR, OPERATION_CONTRACTS_JSON, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK } from "./crossProjectApiPlugin.mjs";
|
|
5
5
|
import { fileURLToPath as __rspack_fileURLToPath } from "node:url";
|
|
6
6
|
import { dirname as __rspack_dirname } from "node:path";
|
|
7
7
|
var __rspack_import_meta_dirname__ = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
|
|
8
|
-
function replaceContent(source, packageName, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework) {
|
|
9
|
-
const updatedSource = source.replace(new RegExp(PACKAGE_NAME, 'g'), packageName).replace(new RegExp(PREFIX, 'g'), prefix).replace(new RegExp(DIST_DIR, 'g'), normalizeToPosixPath(relativeDistPath)).replace(new RegExp(API_DIR, 'g'), normalizeToPosixPath(relativeApiPath)).replace(new RegExp(LAMBDA_DIR, 'g'), normalizeToPosixPath(relativeLambdaPath)).replace(new RegExp(RUNTIME_FRAMEWORK, 'g'), runtimeFramework);
|
|
8
|
+
function replaceContent(source, packageName, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts) {
|
|
9
|
+
const updatedSource = source.replace(new RegExp(PACKAGE_NAME, 'g'), packageName).replace(new RegExp(PREFIX, 'g'), prefix).replace(new RegExp(DIST_DIR, 'g'), normalizeToPosixPath(relativeDistPath)).replace(new RegExp(API_DIR, 'g'), normalizeToPosixPath(relativeApiPath)).replace(new RegExp(LAMBDA_DIR, 'g'), normalizeToPosixPath(relativeLambdaPath)).replace(new RegExp(RUNTIME_FRAMEWORK, 'g'), runtimeFramework).replace(new RegExp(EFFECT_ENTRY, 'g'), normalizeToPosixPath(relativeEffectEntry)).replace(`'${OPERATION_CONTRACTS_JSON}'`, ()=>JSON.stringify(JSON.stringify(operationContracts)));
|
|
10
10
|
return updatedSource;
|
|
11
11
|
}
|
|
12
|
-
async function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework }) {
|
|
12
|
+
async function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts }) {
|
|
13
13
|
try {
|
|
14
14
|
const packageContent = await fs.readFile(path.resolve(appDirectory, './package.json'), 'utf8');
|
|
15
15
|
const packageJson = JSON.parse(packageContent);
|
|
16
16
|
const pluginDir = path.resolve(appDirectory, `./${relativeDistPath}`, 'plugin');
|
|
17
17
|
const pluginPath = path.join(pluginDir, 'index.js');
|
|
18
18
|
const pluginTemplate = await fs.readFile(path.resolve(__rspack_import_meta_dirname__, 'crossProjectApiPlugin.js'), 'utf8');
|
|
19
|
-
const updatedPlugin = replaceContent(pluginTemplate, packageJson.name, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework);
|
|
19
|
+
const updatedPlugin = replaceContent(pluginTemplate, packageJson.name, prefix, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts);
|
|
20
20
|
await fs.ensureFile(pluginPath);
|
|
21
21
|
await fs.writeFile(pluginPath, updatedPlugin);
|
|
22
22
|
const typeContent = `import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import type { HttpApi } from 'effect/unstable/httpapi';
|
|
2
2
|
import type { Rpc } from 'effect/unstable/rpc';
|
|
3
|
-
import type {
|
|
4
|
-
export declare function defineEffectBff<TApi extends HttpApi.Constraint, TLayer extends EffectRuntimeLayer, TRpcs extends Rpc.Any = Rpc.Any>(definition:
|
|
5
|
-
api: TApi;
|
|
6
|
-
layer: TLayer;
|
|
7
|
-
rpc?: EffectRpcBffDefinition<TRpcs>;
|
|
8
|
-
dataPlatform?: EffectDataPlatformValidationOptions;
|
|
9
|
-
}): {
|
|
10
|
-
api: TApi;
|
|
11
|
-
layer: TLayer;
|
|
12
|
-
rpc?: EffectRpcBffDefinition<TRpcs>;
|
|
13
|
-
dataPlatform?: EffectDataPlatformValidationOptions;
|
|
14
|
-
} & EffectBffRuntime<TApi, TLayer>;
|
|
3
|
+
import type { EffectBffDefinition, EffectBffRuntime, EffectRpcBffDefinition, EffectRpcBffHandlerFactory, EffectRpcRuntimeLayer, EffectRuntimeLayer } from './types';
|
|
4
|
+
export declare function defineEffectBff<TApi extends HttpApi.Constraint, TLayer extends EffectRuntimeLayer, TRpcs extends Rpc.Any = Rpc.Any>(definition: EffectBffDefinition<TApi, TLayer, TRpcs>): EffectBffDefinition<TApi, TLayer, TRpcs> & EffectBffRuntime<TApi, TLayer>;
|
|
15
5
|
export declare function defineEffectRpcBff<TRpcs extends Rpc.Any = Rpc.Any, TLayer extends EffectRpcRuntimeLayer<TRpcs> = EffectRpcRuntimeLayer<TRpcs>>(definition: EffectRpcBffDefinition<TRpcs, TLayer>): EffectRpcBffDefinition<TRpcs, TLayer> & {
|
|
16
6
|
createHandler: EffectRpcBffHandlerFactory<TRpcs>;
|
|
17
7
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { HttpApi } from 'effect/unstable/httpapi';
|
|
2
2
|
import type { Rpc } from 'effect/unstable/rpc';
|
|
3
|
-
import type { EffectBffOpenApiConfig, EffectDataPlatformValidationOptions, EffectRequestValidator, EffectRpcBffDefinition, EffectRuntimeLayer } from './types';
|
|
3
|
+
import type { EffectBffOpenApiConfig, EffectDataPlatformValidationOptions, EffectRequestInterceptor, EffectRequestValidator, EffectRpcBffDefinition, EffectRuntimeLayer } from './types';
|
|
4
4
|
export declare function createHttpApiHandler<TApi extends HttpApi.Constraint = HttpApi.Top, TRpcs extends Rpc.Any = Rpc.Any>(options: {
|
|
5
5
|
api: TApi;
|
|
6
6
|
layer: EffectRuntimeLayer;
|
|
7
7
|
openapi?: EffectBffOpenApiConfig;
|
|
8
8
|
rpc?: EffectRpcBffDefinition<TRpcs>;
|
|
9
9
|
dataPlatform?: EffectDataPlatformValidationOptions;
|
|
10
|
+
interceptRequest?: EffectRequestInterceptor;
|
|
10
11
|
validateRequest?: EffectRequestValidator;
|
|
11
12
|
}): {
|
|
12
13
|
handler: (request: Request, context?: Parameters<(request: globalThis.Request, context: import("effect/Context").Context<unknown>) => Promise<Response>>[1]) => Promise<Response>;
|
|
@@ -35,6 +35,7 @@ export type EffectBffDefinition<TApi extends HttpApi.Constraint = HttpApi.Top, T
|
|
|
35
35
|
layer: TLayer;
|
|
36
36
|
rpc?: EffectRpcBffDefinition<TRpcs>;
|
|
37
37
|
dataPlatform?: EffectDataPlatformValidationOptions;
|
|
38
|
+
interceptRequest?: EffectRequestInterceptor;
|
|
38
39
|
};
|
|
39
40
|
export type EffectDataPlatformSelectionValidationOptions = {
|
|
40
41
|
maxDepth?: number;
|
|
@@ -131,6 +132,10 @@ export type EffectDataPlatformValidationOptions = {
|
|
|
131
132
|
* cannot bypass it.
|
|
132
133
|
*/
|
|
133
134
|
export type EffectRequestValidator = (request: Request) => Response | null | undefined;
|
|
135
|
+
export type EffectRequestInterceptor = (options: {
|
|
136
|
+
request: Request;
|
|
137
|
+
next: () => Promise<Response>;
|
|
138
|
+
}) => Response | Promise<Response>;
|
|
134
139
|
export type EffectBffHandlerFactory<TApi extends HttpApi.Constraint = HttpApi.Top, TLayer extends EffectRuntimeLayer = EffectRuntimeLayer> = (options?: {
|
|
135
140
|
openapi?: EffectBffOpenApiConfig;
|
|
136
141
|
rpc?: Partial<EffectRpcBffHandlerOptions>;
|
|
@@ -5,5 +5,7 @@ export declare const API_DIR = "{apiDirectory}";
|
|
|
5
5
|
export declare const LAMBDA_DIR = "{lambdaDirectory}";
|
|
6
6
|
export declare const DIST_DIR = "{distDirectory}";
|
|
7
7
|
export declare const RUNTIME_FRAMEWORK: string;
|
|
8
|
+
export declare const EFFECT_ENTRY = "{effectEntry}";
|
|
9
|
+
export declare const OPERATION_CONTRACTS_JSON = "{operationContracts}";
|
|
8
10
|
export declare const crossProjectApiPlugin: () => CliPlugin<AppTools>;
|
|
9
11
|
export default crossProjectApiPlugin;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
declare function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, }: {
|
|
1
|
+
declare function pluginGenerator({ prefix, appDirectory, relativeDistPath, relativeApiPath, relativeLambdaPath, runtimeFramework, relativeEffectEntry, operationContracts, }: {
|
|
2
2
|
prefix: string;
|
|
3
3
|
appDirectory: string;
|
|
4
4
|
relativeDistPath: string;
|
|
5
5
|
relativeApiPath: string;
|
|
6
6
|
relativeLambdaPath: string;
|
|
7
7
|
runtimeFramework: 'hono' | 'effect';
|
|
8
|
+
relativeEffectEntry: string;
|
|
9
|
+
operationContracts: Record<string, unknown>;
|
|
8
10
|
}): Promise<void>;
|
|
9
11
|
export default pluginGenerator;
|