@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.10 → 3.9.0-ultramodern.12

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 (50) hide show
  1. package/README.md +7 -0
  2. package/dist/cjs/backend-federation/identity.js +23 -22
  3. package/dist/cjs/backend-federation-manifest/load.js +28 -5
  4. package/dist/cjs/backend-federation-manifest/validation.js +11 -7
  5. package/dist/cjs/cross-project-policy/node.js +8 -28
  6. package/dist/cjs/effect-adapter/cross-project-policy.js +2 -2
  7. package/dist/cjs/effect-adapter/entry.js +8 -44
  8. package/dist/cjs/effect-adapter/index.js +10 -2
  9. package/dist/cjs/effect-source-loader/built-entry.js +4 -5
  10. package/dist/cjs/effect-source-loader/loader.js +2 -1
  11. package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
  12. package/dist/cjs/effect-source-loader/paths.js +38 -10
  13. package/dist/esm/backend-federation/identity.mjs +18 -23
  14. package/dist/esm/backend-federation-manifest/load.mjs +28 -5
  15. package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
  16. package/dist/esm/cross-project-policy/node.mjs +9 -19
  17. package/dist/esm/effect-adapter/cross-project-policy.mjs +2 -2
  18. package/dist/esm/effect-adapter/entry.mjs +9 -45
  19. package/dist/esm/effect-adapter/index.mjs +10 -2
  20. package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
  21. package/dist/esm/effect-source-loader/loader.mjs +2 -1
  22. package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
  23. package/dist/esm/effect-source-loader/paths.mjs +30 -11
  24. package/dist/esm-node/backend-federation/identity.mjs +18 -23
  25. package/dist/esm-node/backend-federation-manifest/load.mjs +28 -5
  26. package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
  27. package/dist/esm-node/cross-project-policy/node.mjs +9 -19
  28. package/dist/esm-node/effect-adapter/cross-project-policy.mjs +2 -2
  29. package/dist/esm-node/effect-adapter/entry.mjs +9 -45
  30. package/dist/esm-node/effect-adapter/index.mjs +10 -2
  31. package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
  32. package/dist/esm-node/effect-source-loader/loader.mjs +2 -1
  33. package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
  34. package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
  35. package/dist/types/backend-federation/identity.d.ts +3 -6
  36. package/dist/types/cross-project-policy/node.d.ts +2 -2
  37. package/dist/types/effect-adapter/cross-project-policy.d.ts +2 -1
  38. package/dist/types/effect-source-loader/paths.d.ts +20 -2
  39. package/package.json +10 -10
  40. package/src/backend-federation/identity.ts +39 -47
  41. package/src/backend-federation-manifest/load.ts +75 -27
  42. package/src/backend-federation-manifest/validation.ts +22 -29
  43. package/src/cross-project-policy/node.ts +7 -38
  44. package/src/effect-adapter/cross-project-policy.ts +7 -1
  45. package/src/effect-adapter/entry.ts +10 -88
  46. package/src/effect-adapter/index.ts +7 -1
  47. package/src/effect-source-loader/built-entry.ts +11 -9
  48. package/src/effect-source-loader/loader.ts +10 -5
  49. package/src/effect-source-loader/operation-contracts.ts +7 -41
  50. package/src/effect-source-loader/paths.ts +55 -21
@@ -3,7 +3,7 @@ import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSo
3
3
  import { logger } from "@modern-js/utils";
4
4
  import { HttpApi } from "effect/unstable/httpapi";
5
5
  import { resolveAdapterCrossProjectPolicy } from "../cross-project-policy/index.mjs";
6
- function resolveEffectAdapterCrossProjectPolicy(api, prefix, mod) {
6
+ function resolveEffectAdapterCrossProjectPolicy(api, prefix, mod, producer) {
7
7
  const contractSourcesPromise = null === mod ? Promise.resolve([]) : Promise.resolve().then(()=>extractHttpApiFromModule(mod, HttpApi.isHttpApi)).then((effectApi)=>{
8
8
  if (null === effectApi) return [];
9
9
  const reflect = (apiValue, handlers)=>{
@@ -19,7 +19,7 @@ function resolveEffectAdapterCrossProjectPolicy(api, prefix, mod) {
19
19
  return [];
20
20
  });
21
21
  return contractSourcesPromise.then((contractSources)=>{
22
- const policy = resolveAdapterCrossProjectPolicy(api, contractSources);
22
+ const policy = resolveAdapterCrossProjectPolicy(api, contractSources, producer);
23
23
  if (policy?.enabled === true && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy enabled but no HttpApi endpoints could be reflected; requests fail operation-contract matching unless allowUnknownOperations is enabled.');
24
24
  return policy;
25
25
  });
@@ -1,52 +1,16 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
3
  import "@modern-js/server-runtime-extensions/server-config";
4
- import { API_DIR, findExists, fs, isProd } from "@modern-js/utils";
5
- const JS_OR_TS_EXTS = [
6
- '.js',
7
- '.jsx',
8
- '.ts',
9
- '.tsx',
10
- '.mjs',
11
- '.mts',
12
- '.cjs',
13
- '.cts'
14
- ];
15
- function resolveJsOrTsEntry(entryWithoutOrWithExt) {
16
- const extension = node_path.extname(entryWithoutOrWithExt);
17
- if (JS_OR_TS_EXTS.includes(extension)) return fs.existsSync(entryWithoutOrWithExt) ? entryWithoutOrWithExt : void 0;
18
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutOrWithExt}${ext}`)) || void 0;
19
- }
20
- function builtEntryPath(entryPath) {
21
- return entryPath.replace(/\.(?:[cm]?ts|tsx|jsx)$/u, '.js');
22
- }
23
- function relativeAppPath(appDirectory, entryPath) {
24
- const relativePath = node_path.relative(appDirectory, entryPath);
25
- return '..' === relativePath || relativePath.startsWith(`..${node_path.sep}`) ? void 0 : relativePath;
26
- }
27
- function isDependencyPath(relativePath) {
28
- return relativePath?.split(/[\\/]/u).includes('node_modules') ?? false;
29
- }
4
+ import { API_DIR, isProd } from "@modern-js/utils";
5
+ import { resolveEffectEntry } from "../effect-source-loader/paths.mjs";
30
6
  function resolveEffectAdapterEntryFile(api) {
31
7
  const { appDirectory, apiDirectory, distDirectory } = api.getServerContext();
32
- const appRoot = node_path.resolve(appDirectory || process.cwd());
33
- const productionRoot = isProd() && distDirectory ? node_path.resolve(distDirectory) : void 0;
34
- const configuredEntry = api.getServerConfig()?.bff?.effect?.entry;
35
- if (configuredEntry) {
36
- const sourceEntry = node_path.isAbsolute(configuredEntry) ? configuredEntry : node_path.resolve(appRoot, configuredEntry);
37
- if (productionRoot) {
38
- const relativeEntry = relativeAppPath(appRoot, sourceEntry);
39
- if (void 0 === relativeEntry || isDependencyPath(relativeEntry)) return resolveJsOrTsEntry(sourceEntry);
40
- return relativeEntry ? resolveJsOrTsEntry(builtEntryPath(node_path.resolve(productionRoot, relativeEntry))) : void 0;
41
- }
42
- return resolveJsOrTsEntry(sourceEntry);
43
- }
44
- const resolvedApiDirectory = node_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 ? node_path.resolve(productionRoot, relativeApiDirectory) : void 0;
49
- })() : resolvedApiDirectory;
50
- return productionApiDirectory ? resolveJsOrTsEntry(node_path.resolve(productionApiDirectory, 'index')) : void 0;
8
+ const appDir = node_path.resolve(appDirectory || process.cwd());
9
+ return resolveEffectEntry({
10
+ appDir,
11
+ apiDir: node_path.resolve(appDir, apiDirectory || API_DIR),
12
+ effectEntry: api.getServerConfig()?.bff?.effect?.entry,
13
+ distDir: isProd() && distDirectory ? node_path.resolve(distDirectory) : void 0
14
+ }).path;
51
15
  }
52
16
  export { resolveEffectAdapterEntryFile };
@@ -2,6 +2,7 @@ import "node:module";
2
2
  import node_path from "node:path";
3
3
  import "@modern-js/server-runtime-extensions/server-config";
4
4
  import { dispatchEffectBffRequest, resolveEffectBffModuleHandler, useEffectContext } from "@modern-js/bff-effect/effect";
5
+ import { resolveOperationProducer } from "@modern-js/server-runtime-extensions/bff-policy/node";
5
6
  import { createDisposableServerRuntimeHandle, registerServerRuntimeDisposer } from "@modern-js/server-runtime-extensions/runtime-lifecycle";
6
7
  import { fs, isProd, logger } from "@modern-js/utils";
7
8
  import { checkCrossProjectPolicyForRequest } from "../cross-project-policy/evaluation.mjs";
@@ -21,9 +22,9 @@ class EffectAdapter {
21
22
  this.handler = null;
22
23
  return;
23
24
  }
25
+ const { appDirectory } = this.api.getServerContext();
24
26
  let mod;
25
27
  try {
26
- const { appDirectory } = this.api.getServerContext();
27
28
  mod = await (isProd() ? loadEffectBuiltModule(entryFile) : loadEffectSourceModule({
28
29
  resourcePath: entryFile,
29
30
  appDir: appDirectory || node_path.dirname(entryFile)
@@ -32,9 +33,16 @@ class EffectAdapter {
32
33
  logger.error(`[BFF][Effect] Failed to load Effect entry: ${entryFile}\n${String(error)}`);
33
34
  throw error;
34
35
  }
36
+ const producer = resolveOperationProducer({
37
+ directories: [
38
+ node_path.dirname(entryFile),
39
+ appDirectory
40
+ ],
41
+ requestId: this.api.getServerConfig()?.bff?.requestId
42
+ });
35
43
  const crossProjectPolicies = new Map(await Promise.all(this.prefixes.map(async (prefix)=>[
36
44
  prefix,
37
- await resolveEffectAdapterCrossProjectPolicy(this.api, prefix, mod)
45
+ await resolveEffectAdapterCrossProjectPolicy(this.api, prefix, mod, producer)
38
46
  ])));
39
47
  const effectConfig = this.api.getServerConfig()?.bff?.effect;
40
48
  const loaded = await resolveEffectBffModuleHandler(mod, {
@@ -1,12 +1,11 @@
1
1
  import "node:module";
2
2
  import { fs, upath } from "@modern-js/utils";
3
- import { resolveEffectEntryFile } from "./paths.mjs";
4
- const BUILT_ENTRY_EXT = /\.(?:[cm]?ts|tsx|jsx)$/u;
3
+ import { emittedEffectEntry, relativeEffectAppPath, resolveEffectEntryFile } from "./paths.mjs";
5
4
  function resolveBuiltEffectEntry(appDirectory, distDirectory, sourceEntry) {
6
5
  if (void 0 === sourceEntry || 0 === sourceEntry.length) return;
7
- const relativeEntry = upath.relative(appDirectory, sourceEntry);
8
- if ('..' === relativeEntry || relativeEntry.startsWith(`..${upath.sep}`)) throw new Error(`Effect BFF entry must be inside the application directory: ${sourceEntry}`);
9
- const builtEntry = upath.resolve(distDirectory, relativeEntry).replace(BUILT_ENTRY_EXT, '.js');
6
+ const relativeEntry = relativeEffectAppPath(appDirectory, sourceEntry);
7
+ if (void 0 === relativeEntry) throw new Error(`Effect BFF entry must be inside the application directory: ${sourceEntry}`);
8
+ const builtEntry = emittedEffectEntry(upath.resolve(distDirectory, relativeEntry));
10
9
  return fs.existsSync(builtEntry) ? builtEntry : void 0;
11
10
  }
12
11
  async function bundleBuiltEffectEntryForNode(options) {
@@ -32,7 +31,7 @@ function resolveEffectEntryPaths(options) {
32
31
  apiDir,
33
32
  effectEntry
34
33
  });
35
- const relativeEffectEntry = void 0 !== sourceEffectEntry && sourceEffectEntry.length > 0 ? upath.relative(appDir, sourceEffectEntry).replace(BUILT_ENTRY_EXT, '.js') : '';
34
+ const relativeEffectEntry = void 0 !== sourceEffectEntry && sourceEffectEntry.length > 0 ? emittedEffectEntry(upath.relative(appDir, sourceEffectEntry)) : '';
36
35
  return {
37
36
  sourceEffectEntry,
38
37
  relativeEffectEntry
@@ -131,7 +131,8 @@ function externalizeInstalledDependencies(options) {
131
131
  path: args.path
132
132
  };
133
133
  const realPath = await node_fs.promises.realpath(resolution.path).catch(()=>resolution.path);
134
- if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`)) {
134
+ const loader = SOURCE_LOADERS.get(node_path.extname(realPath).toLowerCase());
135
+ if (realPath.includes(`${node_path.sep}node_modules${node_path.sep}`) && 'ts' !== loader && 'tsx' !== loader && 'jsx' !== loader) {
135
136
  const runtimeResolution = await buildApi.resolve(args.path, {
136
137
  importer: '',
137
138
  kind: args.kind,
@@ -1,46 +1,23 @@
1
1
  import "node:module";
2
2
  import { collectEffectEndpoints, toOperationContractSources } from "@modern-js/bff-effect/effect";
3
- import { buildOperationContractMap, deriveOperationVersion } from "@modern-js/server-runtime-extensions/bff-policy/node";
4
- import { fs, upath } from "@modern-js/utils";
3
+ import { buildOperationContractMap, resolveOperationProducer } from "@modern-js/server-runtime-extensions/bff-policy/node";
4
+ import { upath } from "@modern-js/utils";
5
5
  import { getHttpApiRuntime, loadEffectApi } from "./http-api-runtime.mjs";
6
- function getPackageInfo(resourcePath, appDir, onDependency) {
7
- for (const startDir of [
8
- upath.dirname(resourcePath),
9
- appDir
10
- ]){
11
- let current = upath.resolve(startDir);
12
- for(let depth = 0; depth < 32; depth += 1){
13
- const packageJsonPath = upath.join(current, 'package.json');
14
- if (fs.existsSync(packageJsonPath)) {
15
- onDependency?.(packageJsonPath);
16
- try {
17
- const packageJson = fs.readJSONSync(packageJsonPath);
18
- return {
19
- name: packageJson.name,
20
- version: packageJson.version
21
- };
22
- } catch {
23
- return {};
24
- }
25
- }
26
- const parent = upath.dirname(current);
27
- if (parent === current) break;
28
- current = parent;
29
- }
30
- }
31
- return {};
32
- }
33
6
  async function resolveEffectOperationContracts(options) {
34
7
  const api = await loadEffectApi(options);
35
8
  if (null === api) return null;
36
9
  const runtime = await getHttpApiRuntime();
37
10
  const endpoints = collectEffectEndpoints(runtime.reflect, api, options.prefix);
38
- const packageInfo = getPackageInfo(options.resourcePath, options.appDir, options.onDependency);
39
- const requestId = options.requestId?.trim() || packageInfo.name || 'default';
40
11
  return buildOperationContractMap({
41
12
  handlers: toOperationContractSources(endpoints),
42
- requestId,
43
- operationVersion: deriveOperationVersion(packageInfo.version)
13
+ ...resolveOperationProducer({
14
+ directories: [
15
+ upath.dirname(options.resourcePath),
16
+ options.appDir
17
+ ],
18
+ requestId: options.requestId,
19
+ onDependency: options.onDependency
20
+ })
44
21
  });
45
22
  }
46
23
  export { resolveEffectOperationContracts };
@@ -10,16 +10,35 @@ const JS_OR_TS_EXTS = [
10
10
  '.cjs',
11
11
  '.cts'
12
12
  ];
13
- function resolveEffectEntryFile(options) {
14
- const { appDir, apiDir, effectEntry } = options;
15
- const resolveEntry = (entryWithoutExt)=>{
16
- if ('' !== upath.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
17
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`)) || void 0;
13
+ const emittedEffectEntry = (entry)=>entry.replace(/\.(?:[cm]?ts|tsx|jsx)$/u, '.js');
14
+ function relativeEffectAppPath(appDir, entry) {
15
+ const relative = upath.relative(appDir, entry);
16
+ return '..' === relative || relative.startsWith('../') || relative.split('/').includes('node_modules') ? void 0 : relative;
17
+ }
18
+ function existingEntry(entry) {
19
+ return upath.extname(entry) ? fs.existsSync(entry) ? entry : void 0 : findExists(JS_OR_TS_EXTS.map((ext)=>`${entry}${ext}`)) || void 0;
20
+ }
21
+ function resolveEffectEntry(options) {
22
+ const { appDir, apiDir, effectEntry, distDir } = options;
23
+ const entry = upath.resolve(appDir, effectEntry || upath.join(apiDir, 'index'));
24
+ if (distDir && void 0 !== relativeEffectAppPath(distDir, entry)) return {
25
+ kind: 'built-output',
26
+ path: existingEntry(emittedEffectEntry(entry))
27
+ };
28
+ const relative = relativeEffectAppPath(appDir, entry);
29
+ if (void 0 === relative) return {
30
+ kind: 'external-sdk',
31
+ path: existingEntry(entry)
18
32
  };
19
- if (void 0 !== effectEntry && '' !== effectEntry) {
20
- const entryWithoutExt = upath.isAbsolute(effectEntry) ? effectEntry : upath.resolve(appDir, effectEntry);
21
- return resolveEntry(entryWithoutExt);
22
- }
23
- return resolveEntry(upath.resolve(apiDir, 'index'));
33
+ return distDir ? {
34
+ kind: 'built-output',
35
+ path: existingEntry(emittedEffectEntry(upath.resolve(distDir, relative)))
36
+ } : {
37
+ kind: 'app-source',
38
+ path: existingEntry(entry)
39
+ };
40
+ }
41
+ function resolveEffectEntryFile(options) {
42
+ return resolveEffectEntry(options).path;
24
43
  }
25
- export { resolveEffectEntryFile };
44
+ export { emittedEffectEntry, relativeEffectAppPath, resolveEffectEntry, resolveEffectEntryFile };
@@ -11,11 +11,8 @@ export type BackendFederationIdentityIssue = {
11
11
  path: string;
12
12
  message: string;
13
13
  };
14
- /**
15
- * Shared expected-identity validation for loaded backend federation modules.
16
- * Compares the expose's `backendFederationContract.compatibility` identity
17
- * (`unitId`, `build`) against the consumer's expected delivery-unit identity.
18
- * Missing identity metadata is an error.
19
- */
14
+ /** Decode independently at each received-manifest / executed-expose boundary. */
15
+ export declare function backendFederationCompatibility(loaded: unknown): Record<string, unknown> | undefined;
16
+ export declare function validateBackendFederationCompatibility(compatibility: Record<string, unknown>, expected: Record<string, string | undefined>): BackendFederationIdentityIssue[];
20
17
  export declare function validateExpectedBackendFederationIdentity(loaded: unknown, expected: BackendFederationExpectedIdentity): BackendFederationIdentityIssue[];
21
18
  export declare function formatBackendFederationIdentityIssues(issues: BackendFederationIdentityIssue[]): string;
@@ -1,5 +1,5 @@
1
1
  import '@modern-js/server-runtime-extensions/server-config';
2
2
  import type { ServerPluginAPI } from '@modern-js/server-core';
3
- import { type OperationContractSource } from '@modern-js/server-runtime-extensions/bff-policy/node';
3
+ import { type OperationContractSource, resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
4
4
  import type { ResolvedCrossProjectPolicy } from './evaluation.js';
5
- export declare const resolveAdapterCrossProjectPolicy: (api: ServerPluginAPI, handlers: OperationContractSource[]) => ResolvedCrossProjectPolicy | undefined;
5
+ export declare const resolveAdapterCrossProjectPolicy: (api: ServerPluginAPI, handlers: OperationContractSource[], producer?: ReturnType<typeof resolveOperationProducer>) => ResolvedCrossProjectPolicy | undefined;
@@ -1,4 +1,5 @@
1
1
  import { type EffectApiModule } from '@modern-js/bff-effect/effect';
2
2
  import type { ServerPluginAPI } from '@modern-js/server-core';
3
+ import type { resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
3
4
  import type { ResolvedCrossProjectPolicy } from '../cross-project-policy/evaluation.js';
4
- export declare function resolveEffectAdapterCrossProjectPolicy(api: ServerPluginAPI, prefix: string, mod: EffectApiModule | null): Promise<ResolvedCrossProjectPolicy | undefined>;
5
+ export declare function resolveEffectAdapterCrossProjectPolicy(api: ServerPluginAPI, prefix: string, mod: EffectApiModule | null, producer?: ReturnType<typeof resolveOperationProducer>): Promise<ResolvedCrossProjectPolicy | undefined>;
@@ -1,5 +1,23 @@
1
- export declare function resolveEffectEntryFile(options: {
1
+ export declare const emittedEffectEntry: (entry: string) => string;
2
+ export declare function relativeEffectAppPath(appDir: string, entry: string): string | undefined;
3
+ type EffectEntryOptions = {
2
4
  appDir: string;
3
5
  apiDir: string;
4
6
  effectEntry?: string;
5
- }): string | undefined;
7
+ };
8
+ export type EffectEntry = {
9
+ kind: 'app-source';
10
+ path: string | undefined;
11
+ } | {
12
+ kind: 'built-output';
13
+ path: string | undefined;
14
+ } | {
15
+ kind: 'external-sdk';
16
+ path: string | undefined;
17
+ };
18
+ /** Classify ownership before resolving files: production works without source. */
19
+ export declare function resolveEffectEntry(options: EffectEntryOptions & {
20
+ distDir?: string;
21
+ }): EffectEntry;
22
+ export declare function resolveEffectEntryFile(options: EffectEntryOptions): string | undefined;
23
+ export {};
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "modern.js",
19
19
  "ultramodern.js"
20
20
  ],
21
- "version": "3.9.0-ultramodern.10",
21
+ "version": "3.9.0-ultramodern.12",
22
22
  "files": [
23
23
  "dist",
24
24
  "src"
@@ -193,14 +193,14 @@
193
193
  "node": ">=26.7.0"
194
194
  },
195
195
  "dependencies": {
196
- "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.10",
197
- "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.10",
198
- "@modern-js/bff-effect": "npm:@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.10",
199
- "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.10",
200
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.10",
201
- "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.10",
202
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.10",
203
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.10",
196
+ "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.12",
197
+ "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.12",
198
+ "@modern-js/bff-effect": "npm:@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.12",
199
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.12",
200
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.12",
201
+ "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.12",
202
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.12",
203
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.12",
204
204
  "@module-federation/runtime": "2.9.0",
205
205
  "@swc/core": "1.16.2",
206
206
  "esbuild": "^0.28.2"
@@ -219,7 +219,7 @@
219
219
  },
220
220
  "devDependencies": {
221
221
  "@effect/opentelemetry": "4.0.0-rc.112",
222
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.10",
222
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.12",
223
223
  "@rsbuild/core": "2.2.3",
224
224
  "@rslib/core": "1.0.0",
225
225
  "@scripts/rstest-config": "2.66.0",
@@ -17,59 +17,51 @@ export type BackendFederationIdentityIssue = {
17
17
  message: string;
18
18
  };
19
19
 
20
- /**
21
- * Shared expected-identity validation for loaded backend federation modules.
22
- * Compares the expose's `backendFederationContract.compatibility` identity
23
- * (`unitId`, `build`) against the consumer's expected delivery-unit identity.
24
- * Missing identity metadata is an error.
25
- */
26
- export function validateExpectedBackendFederationIdentity(
27
- loaded: unknown,
28
- expected: BackendFederationExpectedIdentity,
29
- ): BackendFederationIdentityIssue[] {
30
- const issues: BackendFederationIdentityIssue[] = [];
20
+ /** Decode independently at each received-manifest / executed-expose boundary. */
21
+ export function backendFederationCompatibility(loaded: unknown) {
31
22
  const contract = isRecord(loaded)
32
23
  ? loaded.backendFederationContract
33
24
  : undefined;
34
25
  const compatibility = isRecord(contract) ? contract.compatibility : undefined;
26
+ return isRecord(compatibility) ? compatibility : undefined;
27
+ }
35
28
 
36
- if (!isRecord(compatibility)) {
37
- return [
38
- {
39
- path: 'backendFederationContract.compatibility',
40
- message:
41
- 'loaded module declares no compatibility metadata; delivery-unit identity cannot be validated',
42
- },
43
- ];
44
- }
45
-
46
- const unitId = compatibility.unitId;
47
- if (typeof unitId !== 'string' || unitId.length === 0) {
48
- issues.push({
49
- path: 'backendFederationContract.compatibility.unitId',
50
- message: `missing delivery-unit id; expected ${expected.unitId}`,
51
- });
52
- } else if (unitId !== expected.unitId) {
53
- issues.push({
54
- path: 'backendFederationContract.compatibility.unitId',
55
- message: `expected ${expected.unitId}, received ${unitId}`,
56
- });
57
- }
58
-
59
- const build = compatibility.build;
60
- if (typeof build !== 'string' || build.length === 0) {
61
- issues.push({
62
- path: 'backendFederationContract.compatibility.build',
63
- message: `missing build marker; expected ${expected.buildMarker}`,
64
- });
65
- } else if (build !== expected.buildMarker) {
66
- issues.push({
67
- path: 'backendFederationContract.compatibility.build',
68
- message: `expected ${expected.buildMarker}, received ${build}`,
69
- });
70
- }
29
+ export function validateBackendFederationCompatibility(
30
+ compatibility: Record<string, unknown>,
31
+ expected: Record<string, string | undefined>,
32
+ ): BackendFederationIdentityIssue[] {
33
+ return Object.entries(expected).flatMap(([field, value]) =>
34
+ value === undefined ||
35
+ (typeof compatibility[field] === 'string' &&
36
+ compatibility[field].length > 0 &&
37
+ compatibility[field] === value)
38
+ ? []
39
+ : [
40
+ {
41
+ path: `backendFederationContract.compatibility.${field}`,
42
+ message: `expected ${value}, received ${typeof compatibility[field] === 'string' ? compatibility[field] : typeof compatibility[field]}`,
43
+ },
44
+ ],
45
+ );
46
+ }
71
47
 
72
- return issues;
48
+ export function validateExpectedBackendFederationIdentity(
49
+ loaded: unknown,
50
+ expected: BackendFederationExpectedIdentity,
51
+ ): BackendFederationIdentityIssue[] {
52
+ const compatibility = backendFederationCompatibility(loaded);
53
+ return compatibility
54
+ ? validateBackendFederationCompatibility(compatibility, {
55
+ unitId: expected.unitId,
56
+ build: expected.buildMarker,
57
+ })
58
+ : [
59
+ {
60
+ path: 'backendFederationContract.compatibility',
61
+ message:
62
+ 'loaded module declares no compatibility metadata; delivery-unit identity cannot be validated',
63
+ },
64
+ ];
73
65
  }
74
66
 
75
67
  export function formatBackendFederationIdentityIssues(
@@ -25,6 +25,10 @@ import {
25
25
  validateLoadedBackendFederationContract,
26
26
  } from './validation';
27
27
 
28
+ // One manifest+entry budget. Explicit outer 0 opts out; nested 0 cannot
29
+ // disable a positive nested deadline.
30
+ const DEFAULT_SHARED_MANIFEST_LOAD_TIMEOUT_MS = 10_000;
31
+
28
32
  function createManifestLoadScope(
29
33
  options: BackendFederationManifestAdapterOptions,
30
34
  ) {
@@ -44,9 +48,14 @@ function createManifestLoadScope(
44
48
  );
45
49
  }
46
50
  }
51
+ const positivePolicyTimeouts = policyTimeouts.filter(timeout => timeout > 0);
47
52
  const timeoutMs =
48
53
  options.timeoutMs ??
49
- (policyTimeouts.length > 0 ? Math.min(...policyTimeouts) : 0);
54
+ (policyTimeouts.length === 0
55
+ ? DEFAULT_SHARED_MANIFEST_LOAD_TIMEOUT_MS
56
+ : positivePolicyTimeouts.length > 0
57
+ ? Math.min(...positivePolicyTimeouts)
58
+ : 0);
50
59
  const inputSignals = [
51
60
  options.signal,
52
61
  options.manifestPolicy?.signal,
@@ -140,6 +149,39 @@ function throwIfManifestLoadAborted(signal: AbortSignal) {
140
149
  }
141
150
  }
142
151
 
152
+ // Stop waiting even when trusted plugin work ignores cancellation. The race
153
+ // observes late rejections; it cannot stop that work from running.
154
+ function settleOnSharedAbort<T>(
155
+ promise: Promise<T>,
156
+ signal: AbortSignal,
157
+ ): Promise<T> {
158
+ if (signal.aborted) {
159
+ // Observe work even when the pre-aborted path never reaches Promise.race.
160
+ promise.catch(() => {});
161
+ throwIfManifestLoadAborted(signal);
162
+ }
163
+
164
+ let onAbort: (() => void) | undefined;
165
+ const aborted = new Promise<never>((_resolve, reject) => {
166
+ onAbort = () => {
167
+ reject(
168
+ new BackendFederationManifestAdapterError(
169
+ 'remote_unavailable',
170
+ '[BFF][Effect] Backend federation load was aborted before remote execution.',
171
+ signal.reason,
172
+ ),
173
+ );
174
+ };
175
+ signal.addEventListener('abort', onAbort, { once: true });
176
+ });
177
+
178
+ return Promise.race([promise, aborted]).finally(() => {
179
+ if (onAbort) {
180
+ signal.removeEventListener('abort', onAbort);
181
+ }
182
+ });
183
+ }
184
+
143
185
  export async function loadBackendFederatedEffectApiFromManifest(
144
186
  options: BackendFederationManifestAdapterOptions,
145
187
  ): Promise<BackendFederatedEffectApiModule> {
@@ -150,16 +192,19 @@ export async function loadBackendFederatedEffectApiFromManifest(
150
192
  try {
151
193
  scope = createManifestLoadScope(options);
152
194
  throwIfManifestLoadAborted(scope.signal);
153
- manifest = await loadBackendFederationManifest({
154
- ...options,
155
- manifestPolicy: {
156
- ...options.manifestPolicy,
195
+ manifest = await settleOnSharedAbort(
196
+ loadBackendFederationManifest({
197
+ ...options,
198
+ manifestPolicy: {
199
+ ...options.manifestPolicy,
200
+ signal: scope.signal,
201
+ timeoutMs: 0,
202
+ },
157
203
  signal: scope.signal,
158
204
  timeoutMs: 0,
159
- },
160
- signal: scope.signal,
161
- timeoutMs: 0,
162
- });
205
+ }),
206
+ scope.signal,
207
+ );
163
208
  validateBackendFederationManifest(manifest, options.expected);
164
209
  remote = resolveBackendFederationRemoteFromManifest(
165
210
  manifest,
@@ -223,24 +268,27 @@ export async function loadBackendFederatedEffectApiFromManifest(
223
268
  'manifest_invalid',
224
269
  'Backend federation manifest requires delivery-unit identity.',
225
270
  );
226
- const loaded = await loadBackendFederatedEffectApi({
227
- hostName: options.hostName,
228
- remote,
229
- ...(options.plugins ? { plugins: options.plugins } : {}),
230
- entryPolicy: {
231
- ...options.entryPolicy,
232
- signal: scope.signal,
233
- timeoutMs: 0,
234
- },
235
- ...(options.runtime ? { runtime: options.runtime } : {}),
236
- expected: { unitId, buildMarker },
237
- }).catch((error: unknown) => {
238
- throw new BackendFederationManifestAdapterError(
239
- scope?.timedOut() === true ? 'timeout' : classifyLoadError(error),
240
- `[BFF][Effect] Backend federation remote ${remote?.name ?? 'unknown'} could not load ${remote?.expose ?? BACKEND_FEDERATION_EFFECT_EXPOSE}.`,
241
- error,
242
- );
243
- });
271
+ const loaded = await settleOnSharedAbort(
272
+ loadBackendFederatedEffectApi({
273
+ hostName: options.hostName,
274
+ remote,
275
+ ...(options.plugins ? { plugins: options.plugins } : {}),
276
+ entryPolicy: {
277
+ ...options.entryPolicy,
278
+ signal: scope.signal,
279
+ timeoutMs: 0,
280
+ },
281
+ ...(options.runtime ? { runtime: options.runtime } : {}),
282
+ expected: { unitId, buildMarker },
283
+ }).catch((error: unknown) => {
284
+ throw new BackendFederationManifestAdapterError(
285
+ scope?.timedOut() === true ? 'timeout' : classifyLoadError(error),
286
+ `[BFF][Effect] Backend federation remote ${remote?.name ?? 'unknown'} could not load ${remote?.expose ?? BACKEND_FEDERATION_EFFECT_EXPOSE}.`,
287
+ error,
288
+ );
289
+ }),
290
+ scope.signal,
291
+ );
244
292
 
245
293
  validateLoadedBackendFederationContract(loaded, manifest, remote);
246
294