@bleedingdev/modern-js-plugin-bff 3.5.0-ultramodern.101 → 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.
Files changed (34) hide show
  1. package/dist/cjs/cli/generator.js +12 -1
  2. package/dist/cjs/cli.js +6 -2
  3. package/dist/cjs/loader.js +5 -4
  4. package/dist/cjs/runtime/effect/adapter/entry.js +13 -5
  5. package/dist/cjs/runtime/effect/handler/definition.js +1 -0
  6. package/dist/cjs/runtime/effect/handler/http.js +17 -6
  7. package/dist/cjs/utils/crossProjectApiPlugin.js +21 -2
  8. package/dist/cjs/utils/effectSourceLoader.js +63 -7
  9. package/dist/cjs/utils/pluginGenerator.js +4 -4
  10. package/dist/esm/cli/generator.mjs +13 -2
  11. package/dist/esm/cli.mjs +2 -0
  12. package/dist/esm/loader.mjs +5 -4
  13. package/dist/esm/runtime/effect/adapter/entry.mjs +13 -5
  14. package/dist/esm/runtime/effect/handler/definition.mjs +1 -0
  15. package/dist/esm/runtime/effect/handler/http.mjs +17 -6
  16. package/dist/esm/utils/crossProjectApiPlugin.mjs +16 -3
  17. package/dist/esm/utils/effectSourceLoader.mjs +64 -8
  18. package/dist/esm/utils/pluginGenerator.mjs +5 -5
  19. package/dist/esm-node/cli/generator.mjs +13 -2
  20. package/dist/esm-node/cli.mjs +2 -0
  21. package/dist/esm-node/loader.mjs +5 -4
  22. package/dist/esm-node/runtime/effect/adapter/entry.mjs +13 -5
  23. package/dist/esm-node/runtime/effect/handler/definition.mjs +1 -0
  24. package/dist/esm-node/runtime/effect/handler/http.mjs +17 -6
  25. package/dist/esm-node/utils/crossProjectApiPlugin.mjs +16 -3
  26. package/dist/esm-node/utils/effectSourceLoader.mjs +64 -8
  27. package/dist/esm-node/utils/pluginGenerator.mjs +5 -5
  28. package/dist/types/cli.d.ts +1 -0
  29. package/dist/types/runtime/effect/handler/definition.d.ts +2 -12
  30. package/dist/types/runtime/effect/handler/http.d.ts +2 -1
  31. package/dist/types/runtime/effect/handler/types.d.ts +5 -0
  32. package/dist/types/utils/crossProjectApiPlugin.d.ts +2 -0
  33. package/dist/types/utils/pluginGenerator.d.ts +3 -1
  34. 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}`)) return {
112
- external: true,
113
- path: args.path
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,
@@ -132,4 +132,6 @@ const bffPlugin = ()=>({
132
132
  });
133
133
  }
134
134
  });
135
+ const cli = bffPlugin;
136
+ export default cli;
135
137
  export { bffPlugin };
@@ -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, `throw new Error('Failed to generate Effect client for ${resourcePath}')`);
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, `throw new Error('${warning}')`);
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, `throw new Error('${warning}')`);
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 : `throw new Error('${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 relativeApiDirectory = apiDirectory ? path.isAbsolute(apiDirectory) ? relativeAppPath(appRoot, apiDirectory) : apiDirectory : API_DIR;
41
- if (!relativeApiDirectory) return;
42
- const apiRoot = path.resolve(productionRoot || appRoot, relativeApiDirectory);
43
- return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
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 withDataPlatformValidation = async (request, context)=>{
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: withDataPlatformValidation
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 withDataPlatformValidation(request, context);
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
- return rpcHandler.handler(request, toEffectServiceContext(context));
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 apiDirectory = path.join(sdkDistPath, API_DIR);
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}`)) return {
113
- external: true,
114
- path: args.path
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';
@@ -1,2 +1,3 @@
1
1
  import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
2
  export declare const bffPlugin: () => CliPlugin<AppTools>;
3
+ export default bffPlugin;
@@ -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 { EffectBffRuntime, EffectDataPlatformValidationOptions, 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: {
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;