@bleedingdev/modern-js-plugin-bff 3.5.0-ultramodern.3 → 3.5.0-ultramodern.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/runtime/effect/adapter.js +9 -40
- package/dist/cjs/runtime/effect/backend-federation-manifest.js +287 -0
- package/dist/cjs/runtime/effect/backend-federation.js +189 -0
- package/dist/cjs/runtime/effect/dispatch.js +116 -0
- package/dist/cjs/runtime/effect/edge.js +58 -69
- package/dist/cjs/runtime/effect/endpoint-contracts.js +7 -12
- package/dist/cjs/runtime/effect/entry-shape.js +87 -0
- package/dist/cjs/runtime/effect/handler.js +15 -16
- package/dist/cjs/runtime/effect/index.js +91 -15
- package/dist/cjs/runtime/effect/module.js +23 -80
- package/dist/esm/runtime/effect/adapter.mjs +10 -41
- package/dist/esm/runtime/effect/backend-federation-manifest.mjs +237 -0
- package/dist/esm/runtime/effect/backend-federation.mjs +132 -0
- package/dist/esm/runtime/effect/dispatch.mjs +63 -0
- package/dist/esm/runtime/effect/edge.mjs +17 -63
- package/dist/esm/runtime/effect/endpoint-contracts.mjs +7 -12
- package/dist/esm/runtime/effect/entry-shape.mjs +36 -0
- package/dist/esm/runtime/effect/handler.mjs +1 -4
- package/dist/esm/runtime/effect/index.mjs +3 -0
- package/dist/esm/runtime/effect/module.mjs +23 -80
- package/dist/esm-node/runtime/effect/adapter.mjs +10 -41
- package/dist/esm-node/runtime/effect/backend-federation-manifest.mjs +238 -0
- package/dist/esm-node/runtime/effect/backend-federation.mjs +133 -0
- package/dist/esm-node/runtime/effect/dispatch.mjs +64 -0
- package/dist/esm-node/runtime/effect/edge.mjs +17 -63
- package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +7 -12
- package/dist/esm-node/runtime/effect/entry-shape.mjs +37 -0
- package/dist/esm-node/runtime/effect/handler.mjs +1 -4
- package/dist/esm-node/runtime/effect/index.mjs +3 -0
- package/dist/esm-node/runtime/effect/module.mjs +23 -80
- package/dist/types/runtime/effect/backend-federation-manifest.d.ts +60 -0
- package/dist/types/runtime/effect/backend-federation.d.ts +60 -0
- package/dist/types/runtime/effect/dispatch.d.ts +17 -0
- package/dist/types/runtime/effect/edge.d.ts +9 -8
- package/dist/types/runtime/effect/endpoint-contracts.d.ts +2 -2
- package/dist/types/runtime/effect/entry-shape.d.ts +31 -0
- package/dist/types/runtime/effect/handler.d.ts +4 -20
- package/dist/types/runtime/effect/index.d.ts +3 -0
- package/dist/types/runtime/effect-client/index.d.ts +1 -1
- package/package.json +26 -14
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const BACKEND_FEDERATION_EFFECT_EXPOSE = './effect-api';
|
|
3
|
+
const BACKEND_FEDERATION_MANIFEST_FILE = 'backend-mf-manifest.json';
|
|
4
|
+
const BACKEND_FEDERATION_CONTRACT_VERSION = 'microvertical-server-effect-v1';
|
|
5
|
+
const BACKEND_FEDERATION_NODE_ADAPTER_VERSION = 'backend-mf-effect-v1';
|
|
6
|
+
function normalizeExpose(expose) {
|
|
7
|
+
return expose.replace(/^\.\//u, '');
|
|
8
|
+
}
|
|
9
|
+
function exposeForRemoteRequest(expose) {
|
|
10
|
+
return `./${normalizeExpose(expose)}`;
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return 'object' == typeof value && null !== value;
|
|
14
|
+
}
|
|
15
|
+
function collectRemotes(options) {
|
|
16
|
+
const remotes = [
|
|
17
|
+
...options.remotes ?? []
|
|
18
|
+
];
|
|
19
|
+
if (options.remote) {
|
|
20
|
+
const existingIndex = remotes.findIndex((remote)=>remote.name === options.remote?.name);
|
|
21
|
+
if (existingIndex >= 0) remotes[existingIndex] = options.remote;
|
|
22
|
+
else remotes.push(options.remote);
|
|
23
|
+
}
|
|
24
|
+
return remotes;
|
|
25
|
+
}
|
|
26
|
+
function parseRemoteRequest(request) {
|
|
27
|
+
const [remoteName, ...exposeParts] = request.split('/');
|
|
28
|
+
if (!remoteName || 0 === exposeParts.length) throw new Error(`[BFF][Effect] Invalid backend federation request ${request}.`);
|
|
29
|
+
return {
|
|
30
|
+
remoteName,
|
|
31
|
+
expose: exposeForRemoteRequest(exposeParts.join('/'))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function decodeDataUrl(url) {
|
|
35
|
+
const commaIndex = url.indexOf(',');
|
|
36
|
+
if (commaIndex < 0) throw new Error('[BFF][Effect] Invalid backend federation data URL entry.');
|
|
37
|
+
return decodeURIComponent(url.slice(commaIndex + 1));
|
|
38
|
+
}
|
|
39
|
+
async function readCommonJsEntrySource(remote) {
|
|
40
|
+
if (remote.entry.startsWith('data:')) return decodeDataUrl(remote.entry);
|
|
41
|
+
if (remote.entry.startsWith('http://') || remote.entry.startsWith('https://')) {
|
|
42
|
+
const response = await fetch(remote.entry);
|
|
43
|
+
if (!response.ok) throw new Error(`[BFF][Effect] Failed to load backend federation remote ${remote.name}: ${response.status}`);
|
|
44
|
+
return response.text();
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`[BFF][Effect] Backend federation remote ${remote.name} cannot load CommonJS entry ${remote.entry}.`);
|
|
47
|
+
}
|
|
48
|
+
function evaluateCommonJsEntry(remote, source) {
|
|
49
|
+
const module = {
|
|
50
|
+
exports: {}
|
|
51
|
+
};
|
|
52
|
+
const exports = module.exports;
|
|
53
|
+
const evaluate = new Function('module', 'exports', 'globalThis', source);
|
|
54
|
+
evaluate(module, exports, globalThis);
|
|
55
|
+
const entry = module.exports.default ?? module.exports;
|
|
56
|
+
if (!isRecord(entry)) throw new Error(`[BFF][Effect] Backend federation remote ${remote.name} entry must load object module.`);
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
async function importModuleEntry(remote) {
|
|
60
|
+
const entry = await import(remote.entry);
|
|
61
|
+
return entry.default ?? entry;
|
|
62
|
+
}
|
|
63
|
+
async function resolvePluginEntry(remote, plugins) {
|
|
64
|
+
for (const plugin of plugins ?? []){
|
|
65
|
+
const loadEntry = plugin.loadEntry;
|
|
66
|
+
if ('function' != typeof loadEntry) continue;
|
|
67
|
+
const entry = await loadEntry({
|
|
68
|
+
remoteInfo: remote
|
|
69
|
+
});
|
|
70
|
+
if (entry) return entry;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function loadBackendFederationEntry(remote, plugins) {
|
|
74
|
+
const pluginEntry = await resolvePluginEntry(remote, plugins);
|
|
75
|
+
if (pluginEntry) return pluginEntry;
|
|
76
|
+
if (remote.entryGlobalName) {
|
|
77
|
+
const globalEntry = globalThis[remote.entryGlobalName];
|
|
78
|
+
if (isRecord(globalEntry)) return globalEntry;
|
|
79
|
+
}
|
|
80
|
+
if ('module' === remote.type || remote.entry.startsWith('file:')) return importModuleEntry(remote);
|
|
81
|
+
return evaluateCommonJsEntry(remote, await readCommonJsEntrySource(remote));
|
|
82
|
+
}
|
|
83
|
+
async function loadBackendFederationExpose(remote, expose, options) {
|
|
84
|
+
const entry = await loadBackendFederationEntry(remote, options.plugins);
|
|
85
|
+
if ('function' == typeof entry.init) await entry.init({
|
|
86
|
+
hostName: options.hostName
|
|
87
|
+
});
|
|
88
|
+
if ('function' != typeof entry.get) throw new Error(`[BFF][Effect] Backend federation remote ${remote.name} entry must expose get().`);
|
|
89
|
+
const factory = await entry.get(expose);
|
|
90
|
+
if ('function' != typeof factory) throw new Error(`[BFF][Effect] Backend federation expose ${remote.name}/${normalizeExpose(expose)} must load factory function.`);
|
|
91
|
+
return factory();
|
|
92
|
+
}
|
|
93
|
+
function createBackendFederationRuntime(options) {
|
|
94
|
+
const remotes = collectRemotes(options);
|
|
95
|
+
return {
|
|
96
|
+
async loadRemote (request) {
|
|
97
|
+
const { remoteName, expose } = parseRemoteRequest(request);
|
|
98
|
+
const remote = remotes.find((candidate)=>candidate.name === remoteName);
|
|
99
|
+
if (!remote) throw new Error(`[BFF][Effect] Missing backend federation remote ${remoteName}.`);
|
|
100
|
+
return loadBackendFederationExpose(remote, expose, options);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function createBackendFederationLoadEntryPlugin(options) {
|
|
105
|
+
return {
|
|
106
|
+
name: 'modernjs-backend-federation-load-entry',
|
|
107
|
+
async loadEntry ({ remoteInfo }) {
|
|
108
|
+
return options.resolveEntry({
|
|
109
|
+
name: remoteInfo.name,
|
|
110
|
+
entry: remoteInfo.entry,
|
|
111
|
+
type: remoteInfo.type,
|
|
112
|
+
entryGlobalName: remoteInfo.entryGlobalName,
|
|
113
|
+
shareScope: remoteInfo.shareScope
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function loadBackendFederatedEffectApi(options) {
|
|
119
|
+
const remoteName = options.remote?.name ?? options.remoteName;
|
|
120
|
+
if (!remoteName) throw new Error('[BFF][Effect] Missing backend federation remote name.');
|
|
121
|
+
const runtime = options.runtime ?? createBackendFederationRuntime(options);
|
|
122
|
+
const expose = options.expose ?? options.remote?.expose ?? BACKEND_FEDERATION_EFFECT_EXPOSE;
|
|
123
|
+
const remoteRequest = `${remoteName}/${normalizeExpose(expose)}`;
|
|
124
|
+
const loaded = await runtime.loadRemote(remoteRequest);
|
|
125
|
+
if (!isRecord(loaded)) throw new Error(`[BFF][Effect] Backend federation expose ${remoteRequest} must load an object module.`);
|
|
126
|
+
const backendContract = loaded.backendFederationContract;
|
|
127
|
+
if (!isRecord(backendContract) || true !== backendContract.strictEffectApproach) throw new Error(`[BFF][Effect] Backend federation expose ${remoteRequest} must expose strict Effect metadata (strictEffectApproach: true).`);
|
|
128
|
+
if ('effect' !== backendContract.runtimeFramework) throw new Error(`[BFF][Effect] Backend federation expose ${remoteRequest} must expose strict Effect metadata (runtimeFramework: "effect").`);
|
|
129
|
+
if ('string' == typeof backendContract.name && backendContract.name !== remoteName) throw new Error(`[BFF][Effect] Backend federation expose ${remoteRequest} metadata name mismatch: expected ${remoteName}, received ${backendContract.name}.`);
|
|
130
|
+
if (!('runtime' in loaded)) throw new Error(`[BFF][Effect] Backend federation expose ${remoteRequest} must expose api and runtime.`);
|
|
131
|
+
return loaded;
|
|
132
|
+
}
|
|
133
|
+
export { BACKEND_FEDERATION_CONTRACT_VERSION, BACKEND_FEDERATION_EFFECT_EXPOSE, BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_NODE_ADAPTER_VERSION, createBackendFederationLoadEntryPlugin, createBackendFederationRuntime, loadBackendFederatedEffectApi };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
3
|
+
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
4
|
+
function normalizeEffectBffPrefix(prefix) {
|
|
5
|
+
if (!prefix || '/' === prefix) return '';
|
|
6
|
+
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
7
|
+
}
|
|
8
|
+
function removeEffectBffPrefixFromPath(pathname, prefix) {
|
|
9
|
+
const normalized = normalizeEffectBffPrefix(prefix);
|
|
10
|
+
if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
|
|
11
|
+
const sliced = pathname.slice(normalized.length);
|
|
12
|
+
return sliced.startsWith('/') ? sliced : `/${sliced}`;
|
|
13
|
+
}
|
|
14
|
+
function matchesEffectBffPrefix(pathname, prefix) {
|
|
15
|
+
const normalized = normalizeEffectBffPrefix(prefix);
|
|
16
|
+
return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
17
|
+
}
|
|
18
|
+
function createEffectBffMountedRequest(req, prefix) {
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
const nextPath = removeEffectBffPrefixFromPath(url.pathname, prefix);
|
|
21
|
+
if (nextPath !== url.pathname) {
|
|
22
|
+
url.pathname = nextPath;
|
|
23
|
+
return new Request(url, req);
|
|
24
|
+
}
|
|
25
|
+
return req;
|
|
26
|
+
}
|
|
27
|
+
function createEffectBffDispatchContext(originalRequest, mountedRequest, options) {
|
|
28
|
+
const path = options.path ?? new URL(originalRequest.url).pathname;
|
|
29
|
+
const method = options.method ?? originalRequest.method;
|
|
30
|
+
const env = options.env ?? {};
|
|
31
|
+
return {
|
|
32
|
+
request: mountedRequest,
|
|
33
|
+
env,
|
|
34
|
+
path,
|
|
35
|
+
method,
|
|
36
|
+
operationContext: createEffectOperationContext({
|
|
37
|
+
request: mountedRequest,
|
|
38
|
+
env,
|
|
39
|
+
path,
|
|
40
|
+
method
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function dispatchEffectBffRequestWithContext(handler, request, options) {
|
|
45
|
+
const requestPath = new URL(request.url).pathname;
|
|
46
|
+
if (!matchesEffectBffPrefix(requestPath, options.prefix)) return new Response('', {
|
|
47
|
+
status: 404
|
|
48
|
+
});
|
|
49
|
+
const mountedRequest = createEffectBffMountedRequest(request, options.prefix);
|
|
50
|
+
const effectContext = createEffectBffDispatchContext(request, mountedRequest, options);
|
|
51
|
+
try {
|
|
52
|
+
const response = await options.runWithEffectContext(effectContext, ()=>handler(mountedRequest, effectContext));
|
|
53
|
+
if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return Response instance.');
|
|
54
|
+
return new Response(response.body, response);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof Response) return new Response(error.body, error);
|
|
57
|
+
if (options.onError) {
|
|
58
|
+
const errorResponse = await options.onError(error, effectContext);
|
|
59
|
+
if (errorResponse instanceof Response) return errorResponse;
|
|
60
|
+
}
|
|
61
|
+
return createSafeFailureResponse(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export { createEffectBffDispatchContext, createEffectBffMountedRequest, dispatchEffectBffRequestWithContext, matchesEffectBffPrefix, normalizeEffectBffPrefix, removeEffectBffPrefixFromPath };
|
|
@@ -1,71 +1,15 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { dispatchEffectBffRequestWithContext } from "./dispatch.mjs";
|
|
3
3
|
import { runWithEffectContext, useEffectContext, useOperationContext } from "./edge-context.mjs";
|
|
4
4
|
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
5
|
-
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
6
5
|
export * from "./handler.mjs";
|
|
7
|
-
function normalizePrefix(prefix) {
|
|
8
|
-
if (!prefix || '/' === prefix) return '';
|
|
9
|
-
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
10
|
-
}
|
|
11
|
-
function removePrefixFromPath(pathname, prefix) {
|
|
12
|
-
const normalized = normalizePrefix(prefix);
|
|
13
|
-
if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
|
|
14
|
-
const sliced = pathname.slice(normalized.length);
|
|
15
|
-
return sliced.startsWith('/') ? sliced : `/${sliced}`;
|
|
16
|
-
}
|
|
17
|
-
function matchesPrefix(pathname, prefix) {
|
|
18
|
-
const normalized = normalizePrefix(prefix);
|
|
19
|
-
return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
20
|
-
}
|
|
21
|
-
function createRequestForMountedPrefix(req, prefix) {
|
|
22
|
-
const url = new URL(req.url);
|
|
23
|
-
const nextPath = removePrefixFromPath(url.pathname, prefix);
|
|
24
|
-
if (nextPath === url.pathname) return req;
|
|
25
|
-
url.pathname = nextPath;
|
|
26
|
-
return new Request(url, req);
|
|
27
|
-
}
|
|
28
|
-
function createEdgeEffectContext(originalRequest, effectRequest, options) {
|
|
29
|
-
const originalPath = options.path || new URL(originalRequest.url).pathname;
|
|
30
|
-
const method = options.method || originalRequest.method;
|
|
31
|
-
return {
|
|
32
|
-
request: effectRequest,
|
|
33
|
-
env: options.env || {},
|
|
34
|
-
path: originalPath,
|
|
35
|
-
method,
|
|
36
|
-
operationContext: createEffectOperationContext({
|
|
37
|
-
request: effectRequest,
|
|
38
|
-
env: options.env || {},
|
|
39
|
-
path: originalPath,
|
|
40
|
-
method
|
|
41
|
-
})
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function createRuntimeErrorResponse(error) {
|
|
45
|
-
return createSafeFailureResponse(error);
|
|
46
|
-
}
|
|
47
6
|
async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
7
|
+
return dispatchEffectBffRequestWithContext(handler, request, {
|
|
8
|
+
...options,
|
|
9
|
+
runWithEffectContext: runWithEffectContext
|
|
51
10
|
});
|
|
52
|
-
const effectRequest = createRequestForMountedPrefix(request, options.prefix);
|
|
53
|
-
const effectContext = createEdgeEffectContext(request, effectRequest, options);
|
|
54
|
-
try {
|
|
55
|
-
const response = await runWithEffectContext(effectContext, ()=>handler.length > 1 ? handler(effectRequest, effectContext) : handler(effectRequest));
|
|
56
|
-
if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
|
|
57
|
-
return new Response(response.body, response);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error instanceof Response) return new Response(error.body, error);
|
|
60
|
-
if (options.onError) {
|
|
61
|
-
const errorResponse = await options.onError(error, effectContext);
|
|
62
|
-
if (errorResponse instanceof Response) return errorResponse;
|
|
63
|
-
}
|
|
64
|
-
return createRuntimeErrorResponse(error);
|
|
65
|
-
}
|
|
66
11
|
}
|
|
67
|
-
|
|
68
|
-
async function createEffectBffEdgeHandler(options) {
|
|
12
|
+
async function createEffectBffEdgeDispatcher(options) {
|
|
69
13
|
const loaded = await resolveEffectBffModuleHandler(options.module, {
|
|
70
14
|
openapi: options.openapi,
|
|
71
15
|
dataPlatform: options.dataPlatform,
|
|
@@ -73,7 +17,7 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
73
17
|
});
|
|
74
18
|
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
75
19
|
return {
|
|
76
|
-
|
|
20
|
+
dispatch: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
77
21
|
...dispatchOptions,
|
|
78
22
|
prefix: options.prefix,
|
|
79
23
|
onError: options.onError
|
|
@@ -83,4 +27,14 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
83
27
|
}
|
|
84
28
|
};
|
|
85
29
|
}
|
|
86
|
-
|
|
30
|
+
const createEffectBffTestHandler = createEffectBffEdgeHandler;
|
|
31
|
+
async function createEffectBffEdgeHandler(options) {
|
|
32
|
+
const dispatcher = await createEffectBffEdgeDispatcher(options);
|
|
33
|
+
return {
|
|
34
|
+
handler: dispatcher.dispatch,
|
|
35
|
+
dispose: dispatcher.dispose
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export { BACKEND_FEDERATION_CONTRACT_VERSION, BACKEND_FEDERATION_EFFECT_EXPOSE, BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_NODE_ADAPTER_VERSION, createBackendFederationLoadEntryPlugin, createBackendFederationRuntime, loadBackendFederatedEffectApi } from "./backend-federation.mjs";
|
|
39
|
+
export { createEffectOperationContext } from "./operation-context.mjs";
|
|
40
|
+
export { createEffectBffEdgeDispatcher, createEffectBffEdgeHandler, createEffectBffTestHandler, dispatchEffectBffRequest, runWithEffectContext, useEffectContext, useOperationContext };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { createOperationContractHash } from "@modern-js/bff-core";
|
|
3
|
+
import { classifyEffectBffEntryModule, isValidatorAwareHandlerFactory } from "./entry-shape.mjs";
|
|
3
4
|
function ensureLeadingSlash(pathname) {
|
|
4
5
|
return pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
5
6
|
}
|
|
@@ -49,19 +50,13 @@ function createEffectOperationContractSource(endpoint) {
|
|
|
49
50
|
routePath: endpoint.routePath
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
|
-
function isRecord(value) {
|
|
53
|
-
return 'object' == typeof value && null !== value;
|
|
54
|
-
}
|
|
55
53
|
async function extractHttpApiFromModule(mod, isHttpApi) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (isRecord(output) && isHttpApi(output.api)) return output.api;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
54
|
+
const facts = classifyEffectBffEntryModule(mod, {
|
|
55
|
+
isRequestHandler: (value)=>'function' == typeof value,
|
|
56
|
+
isValidatorAwareHandlerFactory: isValidatorAwareHandlerFactory,
|
|
57
|
+
isHttpApi
|
|
58
|
+
});
|
|
59
|
+
return !facts?.legacyShape && facts?.api !== void 0 && facts.hasRuntimeLayer && (void 0 === facts.createHandler || facts.createHandlerValidatorAware) ? facts.api : null;
|
|
65
60
|
}
|
|
66
61
|
function createEffectEndpointContractHash(endpoint, requestId) {
|
|
67
62
|
return createOperationContractHash(createEffectOperationContractSource(endpoint), requestId);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
|
|
3
|
+
function isValidatorAwareHandlerFactory(factory) {
|
|
4
|
+
return Boolean('function' == typeof factory && factory[EFFECT_VALIDATOR_AWARE_FACTORY]);
|
|
5
|
+
}
|
|
6
|
+
const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries export defineEffectBff(...) or { api, layer } HttpApi module. Raw handler exports, default request handlers, unbranded custom createHandler factories not valid Effect API entries.';
|
|
7
|
+
function isEffectEntryRecord(value) {
|
|
8
|
+
return 'object' == typeof value && null !== value;
|
|
9
|
+
}
|
|
10
|
+
function classifyEffectBffEntryModule(value, predicates) {
|
|
11
|
+
if (!isEffectEntryRecord(value)) return null;
|
|
12
|
+
const rootModule = value;
|
|
13
|
+
if (predicates.isRequestHandler(rootModule.handler)) return createEntryShapeFacts(rootModule, predicates, '`handler` export');
|
|
14
|
+
const defaultEntry = rootModule.default;
|
|
15
|
+
if (predicates.isRequestHandler(defaultEntry)) return createEntryShapeFacts(rootModule, predicates, 'default request handler');
|
|
16
|
+
const module = isEffectEntryRecord(defaultEntry) ? {
|
|
17
|
+
...rootModule,
|
|
18
|
+
...defaultEntry
|
|
19
|
+
} : rootModule;
|
|
20
|
+
if (predicates.isRequestHandler(module.handler)) return createEntryShapeFacts(module, predicates, '`handler` export');
|
|
21
|
+
return createEntryShapeFacts(module, predicates);
|
|
22
|
+
}
|
|
23
|
+
function createEntryShapeFacts(module, predicates, legacyShape) {
|
|
24
|
+
const createHandler = 'function' == typeof module.createHandler ? module.createHandler : void 0;
|
|
25
|
+
const api = predicates.isHttpApi(module.api) ? module.api : void 0;
|
|
26
|
+
const hasRuntimeLayer = void 0 !== module.layer;
|
|
27
|
+
return {
|
|
28
|
+
module,
|
|
29
|
+
legacyShape,
|
|
30
|
+
createHandler,
|
|
31
|
+
createHandlerValidatorAware: void 0 !== createHandler && predicates.isValidatorAwareHandlerFactory(createHandler),
|
|
32
|
+
api,
|
|
33
|
+
layer: module.layer,
|
|
34
|
+
hasRuntimeLayer
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export { EFFECT_VALIDATOR_AWARE_FACTORY, classifyEffectBffEntryModule, isEffectEntryRecord, isValidatorAwareHandlerFactory, strictEffectApproachMessage };
|
|
@@ -4,6 +4,7 @@ import { HttpRouter, HttpServer, HttpServerResponse, HttpTraceContext } from "ef
|
|
|
4
4
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
5
5
|
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
6
6
|
import { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, decodeRequestEnvelopeHeader, validateRequestEnvelope, validateSelectionPlan } from "../data-platform/index.mjs";
|
|
7
|
+
import { EFFECT_VALIDATOR_AWARE_FACTORY, isValidatorAwareHandlerFactory } from "./entry-shape.mjs";
|
|
7
8
|
import * as __rspack_external_effect_Config_29be8a92 from "effect/Config";
|
|
8
9
|
import * as __rspack_external_effect_Effect_194ac36c from "effect/Effect";
|
|
9
10
|
import * as __rspack_external_effect_Option_4d691636 from "effect/Option";
|
|
@@ -15,10 +16,6 @@ import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
|
|
|
15
16
|
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
16
17
|
const isEffectServiceContext = (context)=>'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
17
18
|
const toEffectServiceContext = (context)=>isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
18
|
-
const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
|
|
19
|
-
function isValidatorAwareHandlerFactory(factory) {
|
|
20
|
-
return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
|
|
21
|
-
}
|
|
22
19
|
function normalizeOpenApiPath(pathname) {
|
|
23
20
|
if (!pathname.startsWith('/')) return `/${pathname}`;
|
|
24
21
|
return pathname;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
|
|
3
|
+
export * from "./backend-federation.mjs";
|
|
3
4
|
export * from "./handler.mjs";
|
|
5
|
+
export { BACKEND_FEDERATION_CONTRACT_VERSION, BACKEND_FEDERATION_EFFECT_EXPOSE, BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_NODE_ADAPTER_VERSION, createBackendFederationLoadEntryPlugin, createBackendFederationRuntime, loadBackendFederatedEffectApi } from "./backend-federation.mjs";
|
|
6
|
+
export { BackendFederationManifestAdapterError, loadBackendFederatedEffectApiFromManifest, loadBackendFederationManifest, resolveBackendFederationRemoteFromManifest } from "./backend-federation-manifest.mjs";
|
|
4
7
|
export { createEffectOperationContext, runWithEffectContext, useEffectContext, useOperationContext } from "./context.mjs";
|
|
5
8
|
export { createEffectBffEdgeHandler, createEffectBffTestHandler, dispatchEffectBffRequest } from "./edge.mjs";
|
|
6
9
|
export { __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry };
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
3
|
-
import {
|
|
3
|
+
import { classifyEffectBffEntryModule, isValidatorAwareHandlerFactory, strictEffectApproachMessage } from "./entry-shape.mjs";
|
|
4
|
+
import { createHttpApiHandler } from "./handler.mjs";
|
|
4
5
|
import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return 'object' == typeof value && null !== value;
|
|
7
|
-
}
|
|
8
|
-
function includesRuntimeExports(value) {
|
|
9
|
-
return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
|
|
10
|
-
}
|
|
11
6
|
function isRequestHandler(value) {
|
|
12
7
|
return 'function' == typeof value;
|
|
13
8
|
}
|
|
14
|
-
function isEffectApiDefinition(module) {
|
|
15
|
-
return HttpApi.isHttpApi(module.api) && void 0 !== module.layer;
|
|
16
|
-
}
|
|
17
9
|
function isEffectServiceContext(context) {
|
|
18
10
|
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
19
11
|
}
|
|
20
12
|
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
21
|
-
const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries must export defineEffectBff(...) or a { api, layer } HttpApi module. Raw handler exports, default request handlers, and unbranded custom createHandler factories are not valid Effect API entries.';
|
|
22
13
|
function rejectLegacyEffectModuleShape(options, shape) {
|
|
23
14
|
options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
|
|
24
15
|
return true;
|
|
@@ -41,41 +32,21 @@ function createLoadedHttpApiHandler(webHandler) {
|
|
|
41
32
|
dispose: webHandler.dispose
|
|
42
33
|
};
|
|
43
34
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
handler: entry
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (isRecord(entry)) normalizedModule = {
|
|
59
|
-
...normalizedModule,
|
|
60
|
-
...entry
|
|
61
|
-
};
|
|
62
|
-
if (isRecord(entry) && 'handler' in entry) {
|
|
63
|
-
const maybeHandler = entry.handler;
|
|
64
|
-
if (isRequestHandler(maybeHandler)) normalizedModule = {
|
|
65
|
-
...normalizedModule,
|
|
66
|
-
handler: maybeHandler
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (isRequestHandler(normalizedModule.handler)) {
|
|
70
|
-
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
71
|
-
return {
|
|
72
|
-
handler: normalizedModule.handler
|
|
73
|
-
};
|
|
35
|
+
function classifyEffectBffRuntimeEntryShape(module) {
|
|
36
|
+
return classifyEffectBffEntryModule(module, {
|
|
37
|
+
isRequestHandler: isRequestHandler,
|
|
38
|
+
isValidatorAwareHandlerFactory: isValidatorAwareHandlerFactory,
|
|
39
|
+
isHttpApi: HttpApi.isHttpApi
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function resolveClassifiedEffectBffModuleHandler(facts, options = {}) {
|
|
43
|
+
if (!facts) return null;
|
|
44
|
+
if (facts.legacyShape) {
|
|
45
|
+
if (rejectLegacyEffectModuleShape(options, facts.legacyShape)) return null;
|
|
74
46
|
}
|
|
75
|
-
if ('function' == typeof
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
47
|
+
if ('function' == typeof facts.createHandler) {
|
|
48
|
+
if (!facts.createHandlerValidatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
49
|
+
const factory = facts.createHandler;
|
|
79
50
|
const webHandler = factory({
|
|
80
51
|
openapi: options.openapi,
|
|
81
52
|
dataPlatform: options.dataPlatform,
|
|
@@ -83,11 +54,11 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
83
54
|
});
|
|
84
55
|
return createLoadedHandler(webHandler);
|
|
85
56
|
}
|
|
86
|
-
if (
|
|
57
|
+
if (void 0 !== facts.api && facts.hasRuntimeLayer) {
|
|
87
58
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
88
59
|
const webHandler = createHttpApiHandler({
|
|
89
|
-
api:
|
|
90
|
-
layer:
|
|
60
|
+
api: facts.api,
|
|
61
|
+
layer: facts.layer,
|
|
91
62
|
openapi: options.openapi,
|
|
92
63
|
dataPlatform: options.dataPlatform,
|
|
93
64
|
validateRequest: options.validateRequest
|
|
@@ -96,38 +67,10 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
96
67
|
}
|
|
97
68
|
return null;
|
|
98
69
|
}
|
|
70
|
+
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
71
|
+
return resolveClassifiedEffectBffModuleHandler(classifyEffectBffRuntimeEntryShape(normalizedModule), options);
|
|
72
|
+
}
|
|
99
73
|
function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
100
|
-
|
|
101
|
-
const mergeRuntimeExports = (value)=>{
|
|
102
|
-
if (!isRecord(value) || !includesRuntimeExports(value)) return;
|
|
103
|
-
normalizedModule = {
|
|
104
|
-
...normalizedModule,
|
|
105
|
-
...value
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
if (isRequestHandler(normalizedModule.handler)) {
|
|
109
|
-
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
|
|
110
|
-
return Promise.resolve({
|
|
111
|
-
handler: normalizedModule.handler
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
const entry = normalizedModule.default;
|
|
115
|
-
if (isRequestHandler(entry)) {
|
|
116
|
-
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
|
|
117
|
-
return Promise.resolve({
|
|
118
|
-
handler: entry
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
122
|
-
if (isRequestHandler(out)) {
|
|
123
|
-
if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
|
|
124
|
-
return {
|
|
125
|
-
handler: out
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
mergeRuntimeExports(out);
|
|
129
|
-
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
130
|
-
});
|
|
131
|
-
return Promise.resolve(resolveNormalizedEffectBffModuleHandler(normalizedModule, options));
|
|
74
|
+
return Promise.resolve(resolveNormalizedEffectBffModuleHandler(mod, options));
|
|
132
75
|
}
|
|
133
76
|
export { resolveEffectBffModuleHandler };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ModuleFederation, ModuleFederationRuntimePlugin } from '@module-federation/runtime';
|
|
2
|
+
import { type BackendFederatedEffectApiModule, type BackendFederationRemote } from './backend-federation';
|
|
3
|
+
export type BackendFederationManifestAdapterErrorCode = 'manifest_unavailable' | 'manifest_invalid' | 'remote_unavailable' | 'strict_effect_required' | 'timeout' | 'version_mismatch';
|
|
4
|
+
export declare const BACKEND_FEDERATION_MANIFEST_ADAPTER_FAILURE_EVENT: 'modernjs:microvertical-server-fallback';
|
|
5
|
+
export declare class BackendFederationManifestAdapterError extends Error {
|
|
6
|
+
code: BackendFederationManifestAdapterErrorCode;
|
|
7
|
+
readonly failureEvent: "modernjs:microvertical-server-fallback";
|
|
8
|
+
details?: Record<string, unknown>;
|
|
9
|
+
constructor(code: BackendFederationManifestAdapterErrorCode, message: string, cause?: unknown, details?: Record<string, unknown>);
|
|
10
|
+
}
|
|
11
|
+
export type BackendFederationManifest = Record<string, unknown>;
|
|
12
|
+
export type BackendFederationVersionBoundaryExpectation = {
|
|
13
|
+
buildVersion?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Delivery-unit build marker (ADR-0019). Semantically the same value as
|
|
16
|
+
* `buildVersion`; treated as an alias. If both are set and disagree the
|
|
17
|
+
* expectation itself is invalid and fails closed before any manifest
|
|
18
|
+
* comparison runs.
|
|
19
|
+
*/
|
|
20
|
+
buildMarker?: string;
|
|
21
|
+
contractVersion?: string;
|
|
22
|
+
nodeAdapterVersion?: string;
|
|
23
|
+
packageName?: string;
|
|
24
|
+
remoteName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Delivery-unit identity root (ADR-0019 §3). When set, the manifest's
|
|
27
|
+
* `backendFederation.versionBoundary.deliveryUnit.unitId` (or the
|
|
28
|
+
* top-level `backendFederation.deliveryUnit.unitId` fallback) must match.
|
|
29
|
+
*/
|
|
30
|
+
unitId?: string;
|
|
31
|
+
version?: string;
|
|
32
|
+
};
|
|
33
|
+
export type BackendFederationManifestFetchResponse = {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
status: number;
|
|
36
|
+
statusText?: string;
|
|
37
|
+
text: () => Promise<string>;
|
|
38
|
+
};
|
|
39
|
+
export type BackendFederationManifestAdapterFallback = (error: BackendFederationManifestAdapterError, context: {
|
|
40
|
+
manifest?: BackendFederationManifest;
|
|
41
|
+
remote?: BackendFederationRemote;
|
|
42
|
+
}) => BackendFederatedEffectApiModule | Promise<BackendFederatedEffectApiModule>;
|
|
43
|
+
export type BackendFederationManifestAdapterOptions = {
|
|
44
|
+
env?: Record<string, string | undefined>;
|
|
45
|
+
expected?: BackendFederationVersionBoundaryExpectation;
|
|
46
|
+
fallback?: BackendFederationManifestAdapterFallback;
|
|
47
|
+
fetch?: (url: string) => Promise<BackendFederationManifestFetchResponse>;
|
|
48
|
+
hostName: string;
|
|
49
|
+
manifest?: BackendFederationManifest;
|
|
50
|
+
manifestEnv?: string;
|
|
51
|
+
manifestPath?: string;
|
|
52
|
+
manifestUrl?: string;
|
|
53
|
+
plugins?: ModuleFederationRuntimePlugin[];
|
|
54
|
+
remote?: Partial<BackendFederationRemote>;
|
|
55
|
+
runtime?: ModuleFederation;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
};
|
|
58
|
+
export declare function loadBackendFederationManifest(options: Pick<BackendFederationManifestAdapterOptions, 'env' | 'fetch' | 'manifest' | 'manifestEnv' | 'manifestPath' | 'manifestUrl' | 'timeoutMs'>): Promise<BackendFederationManifest>;
|
|
59
|
+
export declare function resolveBackendFederationRemoteFromManifest(manifest: BackendFederationManifest, remoteOverride?: Partial<BackendFederationRemote>): BackendFederationRemote;
|
|
60
|
+
export declare function loadBackendFederatedEffectApiFromManifest(options: BackendFederationManifestAdapterOptions): Promise<BackendFederatedEffectApiModule>;
|