@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.0
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/LICENSE +21 -0
- package/README.md +26 -0
- package/cli.js +1 -0
- package/dist/cjs/cli.js +294 -0
- package/dist/cjs/constants.js +48 -0
- package/dist/cjs/index.js +58 -0
- package/dist/cjs/loader.js +106 -0
- package/dist/cjs/runtime/create-request/index.js +48 -0
- package/dist/cjs/runtime/data-platform/index.js +693 -0
- package/dist/cjs/runtime/effect/adapter.js +311 -0
- package/dist/cjs/runtime/effect/context.js +48 -0
- package/dist/cjs/runtime/effect/index.js +608 -0
- package/dist/cjs/runtime/effect-client/index.js +178 -0
- package/dist/cjs/runtime/hono/adapter.js +168 -0
- package/dist/cjs/runtime/hono/index.js +65 -0
- package/dist/cjs/runtime/hono/operators.js +68 -0
- package/dist/cjs/server.js +179 -0
- package/dist/cjs/utils/clientGenerator.js +342 -0
- package/dist/cjs/utils/createHonoRoutes.js +138 -0
- package/dist/cjs/utils/crossProjectApiPlugin.js +118 -0
- package/dist/cjs/utils/effectClientGenerator.js +673 -0
- package/dist/cjs/utils/pluginGenerator.js +73 -0
- package/dist/cjs/utils/runtimeGenerator.js +133 -0
- package/dist/esm/cli.mjs +245 -0
- package/dist/esm/constants.mjs +11 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/loader.mjs +62 -0
- package/dist/esm/runtime/create-request/index.mjs +1 -0
- package/dist/esm/runtime/data-platform/index.mjs +599 -0
- package/dist/esm/runtime/effect/adapter.mjs +267 -0
- package/dist/esm/runtime/effect/context.mjs +11 -0
- package/dist/esm/runtime/effect/index.mjs +438 -0
- package/dist/esm/runtime/effect-client/index.mjs +90 -0
- package/dist/esm/runtime/hono/adapter.mjs +124 -0
- package/dist/esm/runtime/hono/index.mjs +2 -0
- package/dist/esm/runtime/hono/operators.mjs +31 -0
- package/dist/esm/server.mjs +135 -0
- package/dist/esm/utils/clientGenerator.mjs +293 -0
- package/dist/esm/utils/createHonoRoutes.mjs +92 -0
- package/dist/esm/utils/crossProjectApiPlugin.mjs +54 -0
- package/dist/esm/utils/effectClientGenerator.mjs +623 -0
- package/dist/esm/utils/pluginGenerator.mjs +29 -0
- package/dist/esm/utils/runtimeGenerator.mjs +89 -0
- package/dist/esm-node/cli.mjs +249 -0
- package/dist/esm-node/constants.mjs +12 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/loader.mjs +64 -0
- package/dist/esm-node/runtime/create-request/index.mjs +2 -0
- package/dist/esm-node/runtime/data-platform/index.mjs +600 -0
- package/dist/esm-node/runtime/effect/adapter.mjs +269 -0
- package/dist/esm-node/runtime/effect/context.mjs +12 -0
- package/dist/esm-node/runtime/effect/index.mjs +439 -0
- package/dist/esm-node/runtime/effect-client/index.mjs +91 -0
- package/dist/esm-node/runtime/hono/adapter.mjs +125 -0
- package/dist/esm-node/runtime/hono/index.mjs +3 -0
- package/dist/esm-node/runtime/hono/operators.mjs +32 -0
- package/dist/esm-node/server.mjs +136 -0
- package/dist/esm-node/utils/clientGenerator.mjs +294 -0
- package/dist/esm-node/utils/createHonoRoutes.mjs +93 -0
- package/dist/esm-node/utils/crossProjectApiPlugin.mjs +55 -0
- package/dist/esm-node/utils/effectClientGenerator.mjs +625 -0
- package/dist/esm-node/utils/pluginGenerator.mjs +33 -0
- package/dist/esm-node/utils/runtimeGenerator.mjs +91 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/loader.d.ts +27 -0
- package/dist/types/runtime/create-request/index.d.ts +2 -0
- package/dist/types/runtime/data-platform/index.d.ts +187 -0
- package/dist/types/runtime/effect/adapter.d.ts +22 -0
- package/dist/types/runtime/effect/context.d.ts +8 -0
- package/dist/types/runtime/effect/index.d.ts +171 -0
- package/dist/types/runtime/effect-client/index.d.ts +47 -0
- package/dist/types/runtime/hono/adapter.d.ts +19 -0
- package/dist/types/runtime/hono/index.d.ts +2 -0
- package/dist/types/runtime/hono/operators.d.ts +10 -0
- package/dist/types/server.d.ts +3 -0
- package/dist/types/utils/clientGenerator.d.ts +37 -0
- package/dist/types/utils/createHonoRoutes.d.ts +10 -0
- package/dist/types/utils/crossProjectApiPlugin.d.ts +9 -0
- package/dist/types/utils/effectClientGenerator.d.ts +27 -0
- package/dist/types/utils/pluginGenerator.d.ts +9 -0
- package/dist/types/utils/runtimeGenerator.d.ts +7 -0
- package/docs/data-platform-architecture.md +61 -0
- package/package.json +172 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +10 -0
- package/server.js +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useHonoContext } from "@modern-js/server-core";
|
|
2
|
+
const Pipe = (func)=>({
|
|
3
|
+
name: 'pipe',
|
|
4
|
+
async execute (executeHelper, next) {
|
|
5
|
+
const { inputs } = executeHelper;
|
|
6
|
+
const ctx = useHonoContext();
|
|
7
|
+
const { res } = ctx;
|
|
8
|
+
if ('function' == typeof func) {
|
|
9
|
+
let isPiped = true;
|
|
10
|
+
const end = (value)=>{
|
|
11
|
+
isPiped = false;
|
|
12
|
+
if ('function' == typeof value) return void value(res);
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
const output = await func(inputs, end);
|
|
16
|
+
if (!isPiped) if (output) return executeHelper.result = output;
|
|
17
|
+
else return;
|
|
18
|
+
executeHelper.inputs = output;
|
|
19
|
+
await next();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const Middleware = (middleware)=>({
|
|
24
|
+
name: 'middleware',
|
|
25
|
+
metadata (helper) {
|
|
26
|
+
const middlewares = helper.getMetadata('pipe') || [];
|
|
27
|
+
middlewares.push(middleware);
|
|
28
|
+
helper.setMetadata('middleware', middlewares);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
export { Middleware, Pipe };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ApiRouter } from "@modern-js/bff-core";
|
|
2
|
+
import { API_DIR, isFunction, isWebOnly } from "@modern-js/utils";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { EffectAdapter } from "./runtime/effect/adapter.mjs";
|
|
5
|
+
import { HonoAdapter } from "./runtime/hono/adapter.mjs";
|
|
6
|
+
const RUNTIME_ADAPTER_FACTORIES = {
|
|
7
|
+
hono: [
|
|
8
|
+
(api)=>new HonoAdapter(api)
|
|
9
|
+
],
|
|
10
|
+
effect: [
|
|
11
|
+
(api)=>new EffectAdapter(api)
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
const normalizePrefixList = (prefix)=>{
|
|
15
|
+
if (Array.isArray(prefix)) return prefix.filter(Boolean);
|
|
16
|
+
return [
|
|
17
|
+
prefix || '/api'
|
|
18
|
+
];
|
|
19
|
+
};
|
|
20
|
+
const getPrimaryPrefix = (prefix)=>normalizePrefixList(prefix)[0] || '/api';
|
|
21
|
+
function resolveRuntimeFramework(runtimeFramework) {
|
|
22
|
+
return 'hono' === runtimeFramework ? 'hono' : 'effect';
|
|
23
|
+
}
|
|
24
|
+
class Storage {
|
|
25
|
+
reset() {
|
|
26
|
+
this.middlewares = [];
|
|
27
|
+
}
|
|
28
|
+
constructor(){
|
|
29
|
+
this.middlewares = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const server = ()=>({
|
|
33
|
+
name: '@modern-js/plugin-bff',
|
|
34
|
+
setup: (api)=>{
|
|
35
|
+
const storage = new Storage();
|
|
36
|
+
let apiRouter = null;
|
|
37
|
+
const appContext = api.getServerContext();
|
|
38
|
+
const runtimeFramework = resolveRuntimeFramework(appContext.bffRuntimeFramework);
|
|
39
|
+
const runtimeAdapters = RUNTIME_ADAPTER_FACTORIES[runtimeFramework].map((createAdapter)=>createAdapter(api));
|
|
40
|
+
api.onPrepare(async ()=>{
|
|
41
|
+
const appContext = api.getServerContext();
|
|
42
|
+
const { render } = appContext;
|
|
43
|
+
const { middlewares } = storage;
|
|
44
|
+
api.updateServerContext({
|
|
45
|
+
...appContext,
|
|
46
|
+
apiMiddlewares: middlewares
|
|
47
|
+
});
|
|
48
|
+
const config = api.getServerConfig();
|
|
49
|
+
const prefixList = normalizePrefixList(config?.bff?.prefix);
|
|
50
|
+
const prefix = getPrimaryPrefix(config?.bff?.prefix);
|
|
51
|
+
const enableHandleWeb = config?.bff?.enableHandleWeb;
|
|
52
|
+
const httpMethodDecider = config?.bff?.httpMethodDecider;
|
|
53
|
+
const { distDirectory: pwd, middlewares: globalMiddlewares } = api.getServerContext();
|
|
54
|
+
const webOnly = await isWebOnly();
|
|
55
|
+
if ('hono' === runtimeFramework) {
|
|
56
|
+
let handler;
|
|
57
|
+
if (webOnly) handler = async (c, next)=>{
|
|
58
|
+
c.body('');
|
|
59
|
+
await next();
|
|
60
|
+
};
|
|
61
|
+
else {
|
|
62
|
+
const runner = api.getHooks();
|
|
63
|
+
const renderHandler = enableHandleWeb ? render : null;
|
|
64
|
+
handler = await runner.prepareApiServer.call({
|
|
65
|
+
pwd: pwd,
|
|
66
|
+
prefix,
|
|
67
|
+
render: renderHandler,
|
|
68
|
+
httpMethodDecider
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (handler && isFunction(handler)) globalMiddlewares.push({
|
|
72
|
+
name: 'bind-bff',
|
|
73
|
+
handler: (c, next)=>{
|
|
74
|
+
if (!prefixList.some((item)=>c.req.path.startsWith(item)) && !enableHandleWeb) return next();
|
|
75
|
+
return handler(c, next);
|
|
76
|
+
},
|
|
77
|
+
order: 'post',
|
|
78
|
+
before: [
|
|
79
|
+
'custom-server-hook',
|
|
80
|
+
'custom-server-middleware',
|
|
81
|
+
'render'
|
|
82
|
+
]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(runtimeAdapters.map((adapter)=>adapter.registerMiddleware({
|
|
86
|
+
prefix,
|
|
87
|
+
enableHandleWeb
|
|
88
|
+
})));
|
|
89
|
+
});
|
|
90
|
+
api.onReset(async ({ event })=>{
|
|
91
|
+
storage.reset();
|
|
92
|
+
const appContext = api.getServerContext();
|
|
93
|
+
const { middlewares } = storage;
|
|
94
|
+
api.updateServerContext({
|
|
95
|
+
...appContext,
|
|
96
|
+
apiMiddlewares: middlewares
|
|
97
|
+
});
|
|
98
|
+
if ('file-change' === event.type) {
|
|
99
|
+
if ('hono' === runtimeFramework && apiRouter) {
|
|
100
|
+
const apiHandlerInfos = await apiRouter.getApiHandlers();
|
|
101
|
+
const appContext = api.getServerContext();
|
|
102
|
+
api.updateServerContext({
|
|
103
|
+
...appContext,
|
|
104
|
+
apiHandlerInfos
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
await Promise.all(runtimeAdapters.map((adapter)=>adapter.onApiHandlersUpdated?.()));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const prepareApiServer = async (input, next)=>{
|
|
111
|
+
if ('hono' !== runtimeFramework) return next(input);
|
|
112
|
+
const { pwd, prefix, httpMethodDecider } = input;
|
|
113
|
+
const defaultApiDirectory = path.resolve(pwd, API_DIR);
|
|
114
|
+
const appContext = api.getServerContext();
|
|
115
|
+
const apiDirectory = 'string' == typeof appContext.apiDirectory ? appContext.apiDirectory : defaultApiDirectory;
|
|
116
|
+
const lambdaDirectory = 'string' == typeof appContext.lambdaDirectory ? appContext.lambdaDirectory : void 0;
|
|
117
|
+
apiRouter = new ApiRouter({
|
|
118
|
+
appDir: pwd,
|
|
119
|
+
apiDir: apiDirectory,
|
|
120
|
+
lambdaDir: lambdaDirectory,
|
|
121
|
+
prefix,
|
|
122
|
+
httpMethodDecider
|
|
123
|
+
});
|
|
124
|
+
const apiHandlerInfos = await apiRouter.getApiHandlers();
|
|
125
|
+
api.updateServerContext({
|
|
126
|
+
...appContext,
|
|
127
|
+
apiRouter,
|
|
128
|
+
apiHandlerInfos
|
|
129
|
+
});
|
|
130
|
+
return next(input);
|
|
131
|
+
};
|
|
132
|
+
api.prepareApiServer(prepareApiServer);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
export default server;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { generateClient } from "@modern-js/bff-core";
|
|
2
|
+
import { fs, logger } from "@modern-js/utils";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile } from "./effectClientGenerator.mjs";
|
|
5
|
+
const API_DIR = 'api';
|
|
6
|
+
const PLUGIN_DIR = 'plugin';
|
|
7
|
+
const RUNTIME_DIR = 'runtime';
|
|
8
|
+
const CLIENT_DIR = 'client';
|
|
9
|
+
const EXPORT_PREFIX = `./${API_DIR}/`;
|
|
10
|
+
const TYPE_PREFIX = `${API_DIR}/`;
|
|
11
|
+
const GENERATED_RUNTIME_DIRS = [
|
|
12
|
+
CLIENT_DIR,
|
|
13
|
+
PLUGIN_DIR,
|
|
14
|
+
RUNTIME_DIR
|
|
15
|
+
];
|
|
16
|
+
const toPosixPath = (p)=>p.replace(/\\/g, '/');
|
|
17
|
+
const posixJoin = (...args)=>toPosixPath(path.join(...args));
|
|
18
|
+
function getPackageName(appDirectory) {
|
|
19
|
+
try {
|
|
20
|
+
const packageJsonPath = path.resolve(appDirectory, './package.json');
|
|
21
|
+
const packageJson = fs.readJSONSync(packageJsonPath);
|
|
22
|
+
return packageJson.name;
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function createFileDetails(options) {
|
|
28
|
+
const { appDirectory, baseDirectory, resourcePath, source, relativeDistPath } = options;
|
|
29
|
+
const relativePath = path.relative(baseDirectory, resourcePath);
|
|
30
|
+
const parsedPath = path.parse(relativePath);
|
|
31
|
+
const targetDir = posixJoin(`./${relativeDistPath}/${CLIENT_DIR}`, parsedPath.dir, `${parsedPath.name}.js`);
|
|
32
|
+
const absTargetDir = path.resolve(targetDir);
|
|
33
|
+
const relativePathFromAppDirectory = path.relative(appDirectory, path.dirname(resourcePath));
|
|
34
|
+
const typesFilePath = posixJoin(`./${relativeDistPath}`, relativePathFromAppDirectory, `${parsedPath.name}.d.ts`);
|
|
35
|
+
return {
|
|
36
|
+
resourcePath,
|
|
37
|
+
source,
|
|
38
|
+
targetDir,
|
|
39
|
+
name: parsedPath.name,
|
|
40
|
+
absTargetDir,
|
|
41
|
+
relativeTargetDistDir: `./${typesFilePath}`,
|
|
42
|
+
exportKey: toPosixPath(path.join(parsedPath.dir, parsedPath.name))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function readDirectoryFiles(appDirectory, directory, relativeDistPath) {
|
|
46
|
+
const filesList = [];
|
|
47
|
+
async function readFiles(currentPath) {
|
|
48
|
+
const entries = await fs.readdir(currentPath, {
|
|
49
|
+
withFileTypes: true
|
|
50
|
+
});
|
|
51
|
+
for (const entry of entries){
|
|
52
|
+
if ('_app.ts' === entry.name) continue;
|
|
53
|
+
const resourcePath = path.join(currentPath, entry.name);
|
|
54
|
+
if (entry.isDirectory()) await readFiles(resourcePath);
|
|
55
|
+
else {
|
|
56
|
+
const source = await fs.readFile(resourcePath, 'utf8');
|
|
57
|
+
filesList.push(createFileDetails({
|
|
58
|
+
appDirectory,
|
|
59
|
+
baseDirectory: directory,
|
|
60
|
+
resourcePath,
|
|
61
|
+
source,
|
|
62
|
+
relativeDistPath
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await readFiles(directory);
|
|
68
|
+
return filesList;
|
|
69
|
+
}
|
|
70
|
+
function mergePackageJson(packageJson, files, typesVersion, exports, relativeDistPath) {
|
|
71
|
+
const distPrefix = toPosixPath(`./${relativeDistPath}/`);
|
|
72
|
+
const generatedPrefixes = GENERATED_RUNTIME_DIRS.map((dir)=>toPosixPath(`${distPrefix}${dir}/`));
|
|
73
|
+
const isManagedExportEntry = (value)=>{
|
|
74
|
+
if (!value) return false;
|
|
75
|
+
const values = [
|
|
76
|
+
value.import,
|
|
77
|
+
value.require,
|
|
78
|
+
value.types
|
|
79
|
+
].filter(Boolean);
|
|
80
|
+
return values.every((entry)=>generatedPrefixes.some((prefix)=>entry.startsWith(prefix)));
|
|
81
|
+
};
|
|
82
|
+
const isManagedTypeEntry = (value)=>Array.isArray(value) && value.length > 0 && value.every((entry)=>generatedPrefixes.some((prefix)=>entry.startsWith(prefix)));
|
|
83
|
+
const normalizedFiles = [
|
|
84
|
+
...new Set(files.map((file)=>toPosixPath(file)))
|
|
85
|
+
];
|
|
86
|
+
const currentFiles = packageJson.files || [];
|
|
87
|
+
packageJson.files = [
|
|
88
|
+
...new Set([
|
|
89
|
+
...currentFiles.map((file)=>toPosixPath(file)),
|
|
90
|
+
...normalizedFiles
|
|
91
|
+
])
|
|
92
|
+
];
|
|
93
|
+
packageJson.typesVersions ??= {};
|
|
94
|
+
const typesVersions = packageJson.typesVersions;
|
|
95
|
+
const starTypes = typesVersions['*'] || {};
|
|
96
|
+
const generatedTypeEntries = typesVersion['*'] || {};
|
|
97
|
+
const generatedTypeKeys = new Set(Object.keys(generatedTypeEntries));
|
|
98
|
+
const typeConflicts = Object.entries(starTypes).filter(([key, value])=>{
|
|
99
|
+
if (!generatedTypeKeys.has(key) && !key.startsWith(TYPE_PREFIX)) return false;
|
|
100
|
+
const generatedValue = generatedTypeEntries[key];
|
|
101
|
+
if (generatedValue) return JSON.stringify(value) !== JSON.stringify(generatedValue) && !isManagedTypeEntry(value);
|
|
102
|
+
return !isManagedTypeEntry(value);
|
|
103
|
+
}).map(([key])=>key);
|
|
104
|
+
if (typeConflicts.length > 0) throw new Error(`[plugin-bff] package.json typesVersions conflict on keys: ${typeConflicts.sort().join(', ')}. Rename these keys or move them outside "${TYPE_PREFIX}" namespace.`);
|
|
105
|
+
Object.keys(starTypes).forEach((key)=>{
|
|
106
|
+
if (generatedTypeKeys.has(key) || key.startsWith(TYPE_PREFIX)) delete starTypes[key];
|
|
107
|
+
});
|
|
108
|
+
typesVersions['*'] = {
|
|
109
|
+
...starTypes,
|
|
110
|
+
...generatedTypeEntries
|
|
111
|
+
};
|
|
112
|
+
packageJson.exports ??= {};
|
|
113
|
+
const packageExports = packageJson.exports;
|
|
114
|
+
const generatedExportKeys = new Set(Object.keys(exports));
|
|
115
|
+
const exportConflicts = Object.entries(packageExports).filter(([key, value])=>{
|
|
116
|
+
if (!generatedExportKeys.has(key) && !key.startsWith(EXPORT_PREFIX)) return false;
|
|
117
|
+
const generatedValue = exports[key];
|
|
118
|
+
if (generatedValue) return JSON.stringify(value) !== JSON.stringify(generatedValue) && !isManagedExportEntry(value);
|
|
119
|
+
return !isManagedExportEntry(value);
|
|
120
|
+
}).map(([key])=>key);
|
|
121
|
+
if (exportConflicts.length > 0) throw new Error(`[plugin-bff] package.json exports conflict on keys: ${exportConflicts.sort().join(', ')}. Rename these exports or move them outside "${EXPORT_PREFIX}" namespace.`);
|
|
122
|
+
Object.keys(packageExports).forEach((key)=>{
|
|
123
|
+
if (generatedExportKeys.has(key) || key.startsWith(EXPORT_PREFIX)) delete packageExports[key];
|
|
124
|
+
});
|
|
125
|
+
Object.assign(packageExports, exports);
|
|
126
|
+
}
|
|
127
|
+
async function writeTargetFile(absTargetDir, content) {
|
|
128
|
+
await fs.mkdir(path.dirname(absTargetDir), {
|
|
129
|
+
recursive: true
|
|
130
|
+
});
|
|
131
|
+
await fs.writeFile(absTargetDir, content);
|
|
132
|
+
}
|
|
133
|
+
function getClientPackageName(appDirectory) {
|
|
134
|
+
const packageName = getPackageName(appDirectory) || path.basename(appDirectory);
|
|
135
|
+
if (packageName.startsWith('@') && packageName.includes('/')) {
|
|
136
|
+
const [scope, name] = packageName.split('/');
|
|
137
|
+
return `${scope}/${name}-bff-client`;
|
|
138
|
+
}
|
|
139
|
+
return `${packageName}-bff-client`;
|
|
140
|
+
}
|
|
141
|
+
async function writeClientModuleBoundary(appDirectory, relativeDistPath) {
|
|
142
|
+
await writeTargetFile(path.resolve(appDirectory, relativeDistPath, CLIENT_DIR, 'package.json'), `${JSON.stringify({
|
|
143
|
+
private: true,
|
|
144
|
+
name: getClientPackageName(appDirectory),
|
|
145
|
+
type: 'module'
|
|
146
|
+
}, null, 2)}\n`);
|
|
147
|
+
}
|
|
148
|
+
async function setPackage(files, appDirectory, relativeDistPath) {
|
|
149
|
+
const packagePath = path.resolve(appDirectory, './package.json');
|
|
150
|
+
const packageContent = await fs.readFile(packagePath, 'utf8');
|
|
151
|
+
const packageJson = JSON.parse(packageContent);
|
|
152
|
+
const sortedFiles = [
|
|
153
|
+
...files
|
|
154
|
+
].sort((a, b)=>a.exportKey.localeCompare(b.exportKey));
|
|
155
|
+
const addFiles = [
|
|
156
|
+
posixJoin(relativeDistPath, CLIENT_DIR, '**', '*'),
|
|
157
|
+
posixJoin(relativeDistPath, RUNTIME_DIR, '**', '*'),
|
|
158
|
+
posixJoin(relativeDistPath, PLUGIN_DIR, '**', '*')
|
|
159
|
+
];
|
|
160
|
+
const typesVersions = {
|
|
161
|
+
'*': sortedFiles.reduce((acc, file)=>{
|
|
162
|
+
const typeFilePath = toPosixPath(`./${file.targetDir}`).replace(/\.js$/, '.d.ts');
|
|
163
|
+
return {
|
|
164
|
+
...acc,
|
|
165
|
+
[toPosixPath(`${TYPE_PREFIX}${file.exportKey}`)]: [
|
|
166
|
+
typeFilePath
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}, {
|
|
170
|
+
[`${API_DIR}/*`]: [
|
|
171
|
+
toPosixPath(`./${relativeDistPath}/${CLIENT_DIR}/*.d.ts`)
|
|
172
|
+
],
|
|
173
|
+
[RUNTIME_DIR]: [
|
|
174
|
+
toPosixPath(`./${relativeDistPath}/${RUNTIME_DIR}/index.d.ts`)
|
|
175
|
+
],
|
|
176
|
+
[PLUGIN_DIR]: [
|
|
177
|
+
toPosixPath(`./${relativeDistPath}/${PLUGIN_DIR}/index.d.ts`)
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
};
|
|
181
|
+
const exports = sortedFiles.reduce((acc, file)=>{
|
|
182
|
+
const exportKey = `${EXPORT_PREFIX}${file.exportKey}`;
|
|
183
|
+
const jsFilePath = toPosixPath(`./${file.targetDir}`);
|
|
184
|
+
return {
|
|
185
|
+
...acc,
|
|
186
|
+
[toPosixPath(exportKey)]: {
|
|
187
|
+
import: jsFilePath,
|
|
188
|
+
types: toPosixPath(jsFilePath.replace(/\.js$/, '.d.ts'))
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}, {
|
|
192
|
+
[toPosixPath(`./${API_DIR}/*`)]: {
|
|
193
|
+
import: toPosixPath(`./${relativeDistPath}/${CLIENT_DIR}/*.js`),
|
|
194
|
+
types: toPosixPath(`./${relativeDistPath}/${CLIENT_DIR}/*.d.ts`)
|
|
195
|
+
},
|
|
196
|
+
[toPosixPath(`./${PLUGIN_DIR}`)]: {
|
|
197
|
+
import: toPosixPath(`./${relativeDistPath}/${PLUGIN_DIR}/index.js`),
|
|
198
|
+
require: toPosixPath(`./${relativeDistPath}/${PLUGIN_DIR}/index.js`),
|
|
199
|
+
types: toPosixPath(`./${relativeDistPath}/${PLUGIN_DIR}/index.d.ts`)
|
|
200
|
+
},
|
|
201
|
+
[toPosixPath(`./${RUNTIME_DIR}`)]: {
|
|
202
|
+
import: toPosixPath(`./${relativeDistPath}/${RUNTIME_DIR}/index.js`),
|
|
203
|
+
require: toPosixPath(`./${relativeDistPath}/${RUNTIME_DIR}/index.js`),
|
|
204
|
+
types: toPosixPath(`./${relativeDistPath}/${RUNTIME_DIR}/index.d.ts`)
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
mergePackageJson(packageJson, addFiles, typesVersions, exports, relativeDistPath);
|
|
208
|
+
await fs.promises.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
|
|
209
|
+
}
|
|
210
|
+
async function copyFiles(from, to) {
|
|
211
|
+
if (await fs.pathExists(from)) await fs.copy(toPosixPath(from), toPosixPath(to));
|
|
212
|
+
}
|
|
213
|
+
async function clientGenerator(draftOptions) {
|
|
214
|
+
const generatedClientDir = path.resolve(draftOptions.appDir, draftOptions.relativeDistPath, CLIENT_DIR);
|
|
215
|
+
await fs.remove(generatedClientDir);
|
|
216
|
+
const requestId = getPackageName(draftOptions.appDir) || process.env.npm_package_name;
|
|
217
|
+
const lambdaSourceList = draftOptions.existLambda ? await readDirectoryFiles(draftOptions.appDir, draftOptions.lambdaDir, draftOptions.relativeDistPath) : [];
|
|
218
|
+
const generatedSourceList = [
|
|
219
|
+
...lambdaSourceList
|
|
220
|
+
];
|
|
221
|
+
const getClitentCode = async (resourcePath, source)=>{
|
|
222
|
+
const warning = `The file ${resourcePath} is not allowed to be imported in src directory, only API definition files are allowed.`;
|
|
223
|
+
if (!draftOptions.existLambda) return void logger.warn(warning);
|
|
224
|
+
const options = {
|
|
225
|
+
prefix: Array.isArray(draftOptions.prefix) ? draftOptions.prefix[0] : draftOptions.prefix,
|
|
226
|
+
appDir: draftOptions.appDir,
|
|
227
|
+
apiDir: draftOptions.apiDir,
|
|
228
|
+
lambdaDir: draftOptions.lambdaDir,
|
|
229
|
+
port: Number(draftOptions.port),
|
|
230
|
+
source,
|
|
231
|
+
resourcePath,
|
|
232
|
+
target: 'bundle',
|
|
233
|
+
httpMethodDecider: draftOptions.httpMethodDecider,
|
|
234
|
+
requestCreator: draftOptions.requestCreator,
|
|
235
|
+
requestId
|
|
236
|
+
};
|
|
237
|
+
const { lambdaDir } = draftOptions;
|
|
238
|
+
if (!resourcePath.startsWith(lambdaDir)) return void logger.warn(warning);
|
|
239
|
+
const result = await generateClient(options);
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
for (const source of lambdaSourceList){
|
|
244
|
+
const code = await getClitentCode(source.resourcePath, source.source);
|
|
245
|
+
if (code?.value) {
|
|
246
|
+
await writeTargetFile(source.absTargetDir, code.value);
|
|
247
|
+
await copyFiles(source.relativeTargetDistDir, source.targetDir.replace("js", 'd.ts'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if ('effect' === draftOptions.bffRuntimeFramework) {
|
|
251
|
+
const effectEntryFile = resolveEffectEntryFile({
|
|
252
|
+
appDir: draftOptions.appDir,
|
|
253
|
+
apiDir: draftOptions.apiDir,
|
|
254
|
+
effectEntry: draftOptions.effectEntry
|
|
255
|
+
});
|
|
256
|
+
if (effectEntryFile) {
|
|
257
|
+
const effectSource = await fs.readFile(effectEntryFile, 'utf8');
|
|
258
|
+
const effectFileDetails = createFileDetails({
|
|
259
|
+
appDirectory: draftOptions.appDir,
|
|
260
|
+
baseDirectory: draftOptions.apiDir,
|
|
261
|
+
resourcePath: effectEntryFile,
|
|
262
|
+
source: effectSource,
|
|
263
|
+
relativeDistPath: draftOptions.relativeDistPath
|
|
264
|
+
});
|
|
265
|
+
const effectClientCode = await generateEffectClientCode({
|
|
266
|
+
appDir: draftOptions.appDir,
|
|
267
|
+
apiDir: draftOptions.apiDir,
|
|
268
|
+
resourcePath: effectEntryFile,
|
|
269
|
+
prefix: Array.isArray(draftOptions.prefix) ? draftOptions.prefix[0] : draftOptions.prefix,
|
|
270
|
+
port: Number(draftOptions.port),
|
|
271
|
+
target: 'bundle',
|
|
272
|
+
requestCreator: draftOptions.requestCreator,
|
|
273
|
+
httpMethodDecider: draftOptions.httpMethodDecider,
|
|
274
|
+
dataPlatformBatch: draftOptions.effectDataPlatformBatch
|
|
275
|
+
});
|
|
276
|
+
if (effectClientCode) {
|
|
277
|
+
const targetTypeFile = effectFileDetails.targetDir.replace(/\.js$/, '.d.ts');
|
|
278
|
+
await writeTargetFile(effectFileDetails.absTargetDir, effectClientCode);
|
|
279
|
+
await writeTargetFile(path.resolve(targetTypeFile), renderEffectClientDeclaration());
|
|
280
|
+
generatedSourceList.push(effectFileDetails);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
logger.info("Client bundle generate succeed");
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.error(`Client bundle generate failed: ${error}`);
|
|
287
|
+
}
|
|
288
|
+
if (generatedSourceList.length > 0) await writeClientModuleBoundary(draftOptions.appDir, draftOptions.relativeDistPath);
|
|
289
|
+
await setPackage(generatedSourceList, draftOptions.appDir, draftOptions.relativeDistPath);
|
|
290
|
+
}
|
|
291
|
+
const utils_clientGenerator = clientGenerator;
|
|
292
|
+
export default utils_clientGenerator;
|
|
293
|
+
export { copyFiles, readDirectoryFiles };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { HttpMetadata, ResponseMetaType, ValidationError, isWithMetaHandler } from "@modern-js/bff-core";
|
|
2
|
+
import { parse } from "qs";
|
|
3
|
+
import type_is from "type-is";
|
|
4
|
+
const createHonoRoutes = (handlerInfos = [])=>handlerInfos.map(({ routePath, handler, httpMethod })=>{
|
|
5
|
+
const routeMiddlwares = Reflect.getMetadata('middleware', handler) || [];
|
|
6
|
+
const honoHandler = createHonoHandler(handler);
|
|
7
|
+
return {
|
|
8
|
+
method: httpMethod.toLowerCase(),
|
|
9
|
+
path: routePath,
|
|
10
|
+
handler: routeMiddlwares.length > 0 ? [
|
|
11
|
+
...routeMiddlwares,
|
|
12
|
+
honoHandler
|
|
13
|
+
] : honoHandler
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
const handleResponseMeta = (c, handler)=>{
|
|
17
|
+
const responseMeta = Reflect.getMetadata(HttpMetadata.Response, handler);
|
|
18
|
+
if (Array.isArray(responseMeta)) for (const meta of responseMeta)switch(meta.type){
|
|
19
|
+
case ResponseMetaType.Headers:
|
|
20
|
+
for (const [key, value] of Object.entries(meta.value))c.header(key, value);
|
|
21
|
+
break;
|
|
22
|
+
case ResponseMetaType.Redirect:
|
|
23
|
+
return c.redirect(meta.value);
|
|
24
|
+
case ResponseMetaType.StatusCode:
|
|
25
|
+
c.status(meta.value);
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const createHonoHandler = (handler)=>async (c)=>{
|
|
33
|
+
const input = await getHonoInput(c);
|
|
34
|
+
if (isWithMetaHandler(handler)) try {
|
|
35
|
+
const response = handleResponseMeta(c, handler);
|
|
36
|
+
if (response) return response;
|
|
37
|
+
if (c.finalized) return;
|
|
38
|
+
const result = await handler(input);
|
|
39
|
+
if (result instanceof Response) return result;
|
|
40
|
+
return result && 'object' == typeof result ? c.json(result) : c.body(result);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof ValidationError) {
|
|
43
|
+
c.status(error.status);
|
|
44
|
+
return c.json({
|
|
45
|
+
message: error.message
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
{
|
|
51
|
+
const routePath = c.req.routePath;
|
|
52
|
+
const paramNames = routePath.match(/:\w+/g)?.map((s)=>s.slice(1)) || [];
|
|
53
|
+
const params = Object.fromEntries(paramNames.map((name)=>[
|
|
54
|
+
name,
|
|
55
|
+
input.params[name]
|
|
56
|
+
]));
|
|
57
|
+
const args = Object.values(params).concat(input);
|
|
58
|
+
const body = await handler(...args);
|
|
59
|
+
if (c.finalized) return await Promise.resolve();
|
|
60
|
+
if (void 0 !== body) {
|
|
61
|
+
if (body instanceof Response) return body;
|
|
62
|
+
return c.json(body);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const getHonoInput = async (c)=>{
|
|
67
|
+
const draft = {
|
|
68
|
+
params: c.req.param(),
|
|
69
|
+
query: parse(c.req.query()),
|
|
70
|
+
headers: c.req.header(),
|
|
71
|
+
cookies: c.req.header('cookie')
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const contentType = c.req.header('content-type') || '';
|
|
75
|
+
if (type_is.is(contentType, [
|
|
76
|
+
'application/json'
|
|
77
|
+
])) draft.data = await c.req.json();
|
|
78
|
+
else if (type_is.is(contentType, [
|
|
79
|
+
'multipart/form-data'
|
|
80
|
+
])) draft.formData = await c.req.parseBody();
|
|
81
|
+
else if (type_is.is(contentType, [
|
|
82
|
+
'application/x-www-form-urlencoded'
|
|
83
|
+
])) draft.formUrlencoded = await c.req.parseBody();
|
|
84
|
+
else draft.body = await c.req.json();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
draft.body = null;
|
|
87
|
+
}
|
|
88
|
+
return draft;
|
|
89
|
+
};
|
|
90
|
+
const utils_createHonoRoutes = createHonoRoutes;
|
|
91
|
+
export default utils_createHonoRoutes;
|
|
92
|
+
export { createHonoHandler };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
const PACKAGE_NAME = '{packageName}';
|
|
3
|
+
const PREFIX = '{prefix}';
|
|
4
|
+
const API_DIR = '{apiDirectory}';
|
|
5
|
+
const LAMBDA_DIR = '{lambdaDirectory}';
|
|
6
|
+
const DIST_DIR = '{distDirectory}';
|
|
7
|
+
const RUNTIME_FRAMEWORK = '{runtimeFramework}';
|
|
8
|
+
const NODE_MODULES = 'node_modules';
|
|
9
|
+
const crossProjectApiPlugin = ()=>({
|
|
10
|
+
name: '@modern-js/plugin-independent-bff',
|
|
11
|
+
post: [
|
|
12
|
+
'@modern-js/plugin-bff'
|
|
13
|
+
],
|
|
14
|
+
setup: (api)=>{
|
|
15
|
+
api.modifyResolvedConfig((resolvedConfig)=>{
|
|
16
|
+
const { appDirectory: originAppDirectory } = api.getAppContext();
|
|
17
|
+
const sdkPath = path.join(originAppDirectory, NODE_MODULES, PACKAGE_NAME);
|
|
18
|
+
const sdkDistPath = path.join(sdkPath, DIST_DIR);
|
|
19
|
+
const apiDirectory = path.join(sdkDistPath, API_DIR);
|
|
20
|
+
const lambdaDirectory = path.resolve(sdkDistPath, LAMBDA_DIR);
|
|
21
|
+
api.updateAppContext({
|
|
22
|
+
apiDirectory,
|
|
23
|
+
lambdaDirectory,
|
|
24
|
+
bffRuntimeFramework: RUNTIME_FRAMEWORK
|
|
25
|
+
});
|
|
26
|
+
const config = api.getConfig();
|
|
27
|
+
const configuredPrefix = config?.bff?.prefix;
|
|
28
|
+
if (configuredPrefix) {
|
|
29
|
+
const isSamePrefix = Array.isArray(configuredPrefix) ? 1 === configuredPrefix.length && configuredPrefix[0] === PREFIX : configuredPrefix === PREFIX;
|
|
30
|
+
if (!isSamePrefix) throw new Error(`[${PACKAGE_NAME}] Invalid bff.prefix for cross-project BFF. Detected "${configuredPrefix}", expected "${PREFIX}". Remove bff.prefix from the consumer app, or set it exactly to "${PREFIX}".`);
|
|
31
|
+
}
|
|
32
|
+
const configuredRuntimeFramework = config?.bff?.runtimeFramework;
|
|
33
|
+
if (configuredRuntimeFramework && configuredRuntimeFramework !== RUNTIME_FRAMEWORK) throw new Error(`[${PACKAGE_NAME}] Runtime framework mismatch for cross-project BFF. Detected "${configuredRuntimeFramework}", but producer SDK requires "${RUNTIME_FRAMEWORK}".`);
|
|
34
|
+
resolvedConfig.bff.prefix = PREFIX;
|
|
35
|
+
resolvedConfig.bff.runtimeFramework = RUNTIME_FRAMEWORK;
|
|
36
|
+
resolvedConfig.bff.isCrossProjectServer = true;
|
|
37
|
+
resolvedConfig.bff.requestId = resolvedConfig.bff.requestId || config?.bff?.requestId || PACKAGE_NAME || 'default';
|
|
38
|
+
resolvedConfig.bff.crossProjectPolicy = {
|
|
39
|
+
...resolvedConfig.bff.crossProjectPolicy || {},
|
|
40
|
+
enabled: resolvedConfig.bff.crossProjectPolicy?.enabled ?? true,
|
|
41
|
+
requireEnvelope: resolvedConfig.bff.crossProjectPolicy?.requireEnvelope ?? true,
|
|
42
|
+
requireOperationContext: resolvedConfig.bff.crossProjectPolicy?.requireOperationContext ?? true,
|
|
43
|
+
requireOperationContextDetails: resolvedConfig.bff.crossProjectPolicy?.requireOperationContextDetails ?? true,
|
|
44
|
+
requireOperationSchemaHash: resolvedConfig.bff.crossProjectPolicy?.requireOperationSchemaHash ?? true,
|
|
45
|
+
requireOperationVersion: resolvedConfig.bff.crossProjectPolicy?.requireOperationVersion ?? true,
|
|
46
|
+
allowUnknownOperations: resolvedConfig.bff.crossProjectPolicy?.allowUnknownOperations ?? false
|
|
47
|
+
};
|
|
48
|
+
return resolvedConfig;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const utils_crossProjectApiPlugin = crossProjectApiPlugin;
|
|
53
|
+
export default utils_crossProjectApiPlugin;
|
|
54
|
+
export { API_DIR, DIST_DIR, LAMBDA_DIR, PACKAGE_NAME, PREFIX, RUNTIME_FRAMEWORK, crossProjectApiPlugin };
|