@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.
- package/README.md +7 -0
- package/dist/cjs/backend-federation/identity.js +23 -22
- package/dist/cjs/backend-federation-manifest/load.js +28 -5
- package/dist/cjs/backend-federation-manifest/validation.js +11 -7
- package/dist/cjs/cross-project-policy/node.js +8 -28
- package/dist/cjs/effect-adapter/cross-project-policy.js +2 -2
- package/dist/cjs/effect-adapter/entry.js +8 -44
- package/dist/cjs/effect-adapter/index.js +10 -2
- package/dist/cjs/effect-source-loader/built-entry.js +4 -5
- package/dist/cjs/effect-source-loader/loader.js +2 -1
- package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
- package/dist/cjs/effect-source-loader/paths.js +38 -10
- package/dist/esm/backend-federation/identity.mjs +18 -23
- package/dist/esm/backend-federation-manifest/load.mjs +28 -5
- package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm/cross-project-policy/node.mjs +9 -19
- package/dist/esm/effect-adapter/cross-project-policy.mjs +2 -2
- package/dist/esm/effect-adapter/entry.mjs +9 -45
- package/dist/esm/effect-adapter/index.mjs +10 -2
- package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm/effect-source-loader/loader.mjs +2 -1
- package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm/effect-source-loader/paths.mjs +30 -11
- package/dist/esm-node/backend-federation/identity.mjs +18 -23
- package/dist/esm-node/backend-federation-manifest/load.mjs +28 -5
- package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
- package/dist/esm-node/cross-project-policy/node.mjs +9 -19
- package/dist/esm-node/effect-adapter/cross-project-policy.mjs +2 -2
- package/dist/esm-node/effect-adapter/entry.mjs +9 -45
- package/dist/esm-node/effect-adapter/index.mjs +10 -2
- package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
- package/dist/esm-node/effect-source-loader/loader.mjs +2 -1
- package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
- package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
- package/dist/types/backend-federation/identity.d.ts +3 -6
- package/dist/types/cross-project-policy/node.d.ts +2 -2
- package/dist/types/effect-adapter/cross-project-policy.d.ts +2 -1
- package/dist/types/effect-source-loader/paths.d.ts +20 -2
- package/package.json +10 -10
- package/src/backend-federation/identity.ts +39 -47
- package/src/backend-federation-manifest/load.ts +75 -27
- package/src/backend-federation-manifest/validation.ts +22 -29
- package/src/cross-project-policy/node.ts +7 -38
- package/src/effect-adapter/cross-project-policy.ts +7 -1
- package/src/effect-adapter/entry.ts +10 -88
- package/src/effect-adapter/index.ts +7 -1
- package/src/effect-source-loader/built-entry.ts +11 -9
- package/src/effect-source-loader/loader.ts +10 -5
- package/src/effect-source-loader/operation-contracts.ts +7 -41
- 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,
|
|
5
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
8
|
-
if (
|
|
9
|
-
const builtEntry = upath.resolve(distDirectory, relativeEntry)
|
|
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)
|
|
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
|
-
|
|
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,
|
|
4
|
-
import {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
197
|
-
"@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.
|
|
198
|
-
"@modern-js/bff-effect": "npm:@bleedingdev/modern-js-bff-effect@3.9.0-ultramodern.
|
|
199
|
-
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.
|
|
200
|
-
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.
|
|
201
|
-
"@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.
|
|
202
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.
|
|
203
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|