@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/cli.js +1 -0
  4. package/dist/cjs/cli.js +294 -0
  5. package/dist/cjs/constants.js +48 -0
  6. package/dist/cjs/index.js +58 -0
  7. package/dist/cjs/loader.js +106 -0
  8. package/dist/cjs/runtime/create-request/index.js +48 -0
  9. package/dist/cjs/runtime/data-platform/index.js +693 -0
  10. package/dist/cjs/runtime/effect/adapter.js +311 -0
  11. package/dist/cjs/runtime/effect/context.js +48 -0
  12. package/dist/cjs/runtime/effect/index.js +608 -0
  13. package/dist/cjs/runtime/effect-client/index.js +178 -0
  14. package/dist/cjs/runtime/hono/adapter.js +168 -0
  15. package/dist/cjs/runtime/hono/index.js +65 -0
  16. package/dist/cjs/runtime/hono/operators.js +68 -0
  17. package/dist/cjs/server.js +179 -0
  18. package/dist/cjs/utils/clientGenerator.js +342 -0
  19. package/dist/cjs/utils/createHonoRoutes.js +138 -0
  20. package/dist/cjs/utils/crossProjectApiPlugin.js +118 -0
  21. package/dist/cjs/utils/effectClientGenerator.js +673 -0
  22. package/dist/cjs/utils/pluginGenerator.js +73 -0
  23. package/dist/cjs/utils/runtimeGenerator.js +133 -0
  24. package/dist/esm/cli.mjs +245 -0
  25. package/dist/esm/constants.mjs +11 -0
  26. package/dist/esm/index.mjs +1 -0
  27. package/dist/esm/loader.mjs +62 -0
  28. package/dist/esm/runtime/create-request/index.mjs +1 -0
  29. package/dist/esm/runtime/data-platform/index.mjs +599 -0
  30. package/dist/esm/runtime/effect/adapter.mjs +267 -0
  31. package/dist/esm/runtime/effect/context.mjs +11 -0
  32. package/dist/esm/runtime/effect/index.mjs +438 -0
  33. package/dist/esm/runtime/effect-client/index.mjs +90 -0
  34. package/dist/esm/runtime/hono/adapter.mjs +124 -0
  35. package/dist/esm/runtime/hono/index.mjs +2 -0
  36. package/dist/esm/runtime/hono/operators.mjs +31 -0
  37. package/dist/esm/server.mjs +135 -0
  38. package/dist/esm/utils/clientGenerator.mjs +293 -0
  39. package/dist/esm/utils/createHonoRoutes.mjs +92 -0
  40. package/dist/esm/utils/crossProjectApiPlugin.mjs +54 -0
  41. package/dist/esm/utils/effectClientGenerator.mjs +623 -0
  42. package/dist/esm/utils/pluginGenerator.mjs +29 -0
  43. package/dist/esm/utils/runtimeGenerator.mjs +89 -0
  44. package/dist/esm-node/cli.mjs +249 -0
  45. package/dist/esm-node/constants.mjs +12 -0
  46. package/dist/esm-node/index.mjs +2 -0
  47. package/dist/esm-node/loader.mjs +64 -0
  48. package/dist/esm-node/runtime/create-request/index.mjs +2 -0
  49. package/dist/esm-node/runtime/data-platform/index.mjs +600 -0
  50. package/dist/esm-node/runtime/effect/adapter.mjs +269 -0
  51. package/dist/esm-node/runtime/effect/context.mjs +12 -0
  52. package/dist/esm-node/runtime/effect/index.mjs +439 -0
  53. package/dist/esm-node/runtime/effect-client/index.mjs +91 -0
  54. package/dist/esm-node/runtime/hono/adapter.mjs +125 -0
  55. package/dist/esm-node/runtime/hono/index.mjs +3 -0
  56. package/dist/esm-node/runtime/hono/operators.mjs +32 -0
  57. package/dist/esm-node/server.mjs +136 -0
  58. package/dist/esm-node/utils/clientGenerator.mjs +294 -0
  59. package/dist/esm-node/utils/createHonoRoutes.mjs +93 -0
  60. package/dist/esm-node/utils/crossProjectApiPlugin.mjs +55 -0
  61. package/dist/esm-node/utils/effectClientGenerator.mjs +625 -0
  62. package/dist/esm-node/utils/pluginGenerator.mjs +33 -0
  63. package/dist/esm-node/utils/runtimeGenerator.mjs +91 -0
  64. package/dist/types/cli.d.ts +3 -0
  65. package/dist/types/constants.d.ts +2 -0
  66. package/dist/types/index.d.ts +1 -0
  67. package/dist/types/loader.d.ts +27 -0
  68. package/dist/types/runtime/create-request/index.d.ts +2 -0
  69. package/dist/types/runtime/data-platform/index.d.ts +187 -0
  70. package/dist/types/runtime/effect/adapter.d.ts +22 -0
  71. package/dist/types/runtime/effect/context.d.ts +8 -0
  72. package/dist/types/runtime/effect/index.d.ts +171 -0
  73. package/dist/types/runtime/effect-client/index.d.ts +47 -0
  74. package/dist/types/runtime/hono/adapter.d.ts +19 -0
  75. package/dist/types/runtime/hono/index.d.ts +2 -0
  76. package/dist/types/runtime/hono/operators.d.ts +10 -0
  77. package/dist/types/server.d.ts +3 -0
  78. package/dist/types/utils/clientGenerator.d.ts +37 -0
  79. package/dist/types/utils/createHonoRoutes.d.ts +10 -0
  80. package/dist/types/utils/crossProjectApiPlugin.d.ts +9 -0
  81. package/dist/types/utils/effectClientGenerator.d.ts +27 -0
  82. package/dist/types/utils/pluginGenerator.d.ts +9 -0
  83. package/dist/types/utils/runtimeGenerator.d.ts +7 -0
  84. package/docs/data-platform-architecture.md +61 -0
  85. package/package.json +172 -0
  86. package/rslib.config.mts +4 -0
  87. package/rstest.config.mts +10 -0
  88. package/server.js +1 -0
@@ -0,0 +1,267 @@
1
+ import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
2
+ import { HttpApi } from "effect/unstable/httpapi";
3
+ import path from "path";
4
+ import { runWithEffectContext } from "./context.mjs";
5
+ import { createHttpApiHandler } from "./index.mjs";
6
+ const before = [
7
+ 'custom-server-hook',
8
+ 'custom-server-middleware',
9
+ 'render'
10
+ ];
11
+ const JS_OR_TS_EXTS = [
12
+ '.js',
13
+ '.jsx',
14
+ '.ts',
15
+ '.tsx',
16
+ '.mjs',
17
+ '.mts',
18
+ '.cjs',
19
+ '.cts'
20
+ ];
21
+ function normalizePrefix(prefix) {
22
+ if ('/' === prefix) return '';
23
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
24
+ }
25
+ function removePrefixFromPath(pathname, prefix) {
26
+ const normalized = normalizePrefix(prefix);
27
+ if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
28
+ const sliced = pathname.slice(normalized.length);
29
+ return sliced.startsWith('/') ? sliced : `/${sliced}`;
30
+ }
31
+ function createRequestForMountedPrefix(req, prefix) {
32
+ const url = new URL(req.url);
33
+ const nextPath = removePrefixFromPath(url.pathname, prefix);
34
+ if (nextPath === url.pathname) return req;
35
+ url.pathname = nextPath;
36
+ return new Request(url, req);
37
+ }
38
+ function isRequestHandler(value) {
39
+ return 'function' == typeof value;
40
+ }
41
+ function maybeResponse(value) {
42
+ return value instanceof Response;
43
+ }
44
+ function isRecord(value) {
45
+ return 'object' == typeof value && null !== value;
46
+ }
47
+ function includesRuntimeExports(value) {
48
+ return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
49
+ }
50
+ function isHttpApiWithProps(value) {
51
+ return HttpApi.isHttpApi(value) && isRecord(value) && 'string' == typeof value.identifier && isRecord(value.groups);
52
+ }
53
+ function isEffectApiDefinition(module) {
54
+ return isHttpApiWithProps(module.api) && void 0 !== module.layer;
55
+ }
56
+ class EffectAdapter {
57
+ resolveEntryFile() {
58
+ const { appDirectory, apiDirectory } = this.api.getServerContext();
59
+ const bffConfig = this.api.getServerConfig()?.bff;
60
+ const configuredEntry = bffConfig?.effect?.entry;
61
+ const defaultEntry = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR, 'effect', 'index');
62
+ const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
63
+ return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
64
+ }
65
+ async loadEffectHandlerFromModule(mod) {
66
+ let normalizedModule = mod;
67
+ const mergeRuntimeExports = (value)=>{
68
+ if (!isRecord(value) || !includesRuntimeExports(value)) return;
69
+ normalizedModule = {
70
+ ...normalizedModule,
71
+ ...value
72
+ };
73
+ };
74
+ if (isRequestHandler(normalizedModule.handler)) return {
75
+ handler: normalizedModule.handler
76
+ };
77
+ const entry = normalizedModule.default;
78
+ if (isRequestHandler(entry)) return {
79
+ handler: entry
80
+ };
81
+ if ('function' == typeof entry && 0 === entry.length) {
82
+ const out = await entry();
83
+ if (isRequestHandler(out)) return {
84
+ handler: out
85
+ };
86
+ mergeRuntimeExports(out);
87
+ }
88
+ if (isRecord(entry)) normalizedModule = {
89
+ ...normalizedModule,
90
+ ...entry
91
+ };
92
+ if (isRecord(entry) && 'handler' in entry) {
93
+ const maybeHandler = entry.handler;
94
+ if (isRequestHandler(maybeHandler)) normalizedModule = {
95
+ ...normalizedModule,
96
+ handler: maybeHandler
97
+ };
98
+ }
99
+ if (isRequestHandler(normalizedModule.handler)) return {
100
+ handler: normalizedModule.handler
101
+ };
102
+ if ('function' == typeof normalizedModule.createHandler) {
103
+ const webHandler = normalizedModule.createHandler({
104
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
105
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
106
+ });
107
+ return {
108
+ handler: async (request)=>webHandler.handler(request),
109
+ dispose: async ()=>{
110
+ await webHandler.dispose();
111
+ }
112
+ };
113
+ }
114
+ if (isEffectApiDefinition(normalizedModule)) {
115
+ logger.warn('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
116
+ const webHandler = createHttpApiHandler({
117
+ api: normalizedModule.api,
118
+ layer: normalizedModule.layer,
119
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
120
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
121
+ });
122
+ return {
123
+ handler: async (request)=>webHandler.handler(request),
124
+ dispose: async ()=>{
125
+ await webHandler.dispose();
126
+ }
127
+ };
128
+ }
129
+ return null;
130
+ }
131
+ async reloadHandler() {
132
+ if (!this.isEffect) return;
133
+ const entryFile = this.resolveEntryFile();
134
+ if (!entryFile) {
135
+ await this.disposeCurrentHandler();
136
+ this.handler = null;
137
+ return;
138
+ }
139
+ if (!await fs.pathExists(entryFile)) {
140
+ await this.disposeCurrentHandler();
141
+ this.handler = null;
142
+ return;
143
+ }
144
+ await this.disposeCurrentHandler();
145
+ const resolvedEntryFile = require.resolve(entryFile);
146
+ if (Object.hasOwn(require.cache, resolvedEntryFile)) delete require.cache[resolvedEntryFile];
147
+ let mod;
148
+ try {
149
+ mod = await compatibleRequire(entryFile, false);
150
+ } catch (error) {
151
+ logger.error(`[BFF][Effect] Failed to load Effect entry: ${entryFile}\n${String(error)}`);
152
+ this.handler = null;
153
+ return;
154
+ }
155
+ const loaded = await this.loadEffectHandlerFromModule(mod);
156
+ if (!loaded) {
157
+ logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
158
+ this.handler = null;
159
+ return;
160
+ }
161
+ this.handler = loaded.handler;
162
+ this.dispose = loaded.dispose || null;
163
+ }
164
+ async disposeCurrentHandler() {
165
+ if (!this.dispose) return;
166
+ try {
167
+ await this.dispose();
168
+ } catch (error) {
169
+ logger.warn(`[BFF][Effect] Failed to dispose previous handler: ${String(error)}`);
170
+ } finally{
171
+ this.dispose = null;
172
+ }
173
+ }
174
+ async handleRuntimeError(error, c) {
175
+ try {
176
+ const serverConfig = this.api.getServerConfig();
177
+ const onErrorHandler = serverConfig?.onError;
178
+ if (onErrorHandler) {
179
+ const onErrorContext = this.ensureJsonContext(c);
180
+ const result = await onErrorHandler(error instanceof Error ? error : new Error(String(error)), onErrorContext);
181
+ if (result instanceof Response) return result;
182
+ } else logger.error(error);
183
+ } catch (configError) {
184
+ logger.error(`Error in serverConfig.onError handler: ${configError}`);
185
+ }
186
+ const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
187
+ return new Response(JSON.stringify({
188
+ message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
189
+ }), {
190
+ status,
191
+ headers: {
192
+ 'content-type': 'application/json; charset=utf-8'
193
+ }
194
+ });
195
+ }
196
+ ensureJsonContext(c) {
197
+ const maybeJsonContext = c;
198
+ if ('function' == typeof maybeJsonContext.json) return c;
199
+ const headers = {
200
+ 'content-type': 'application/json; charset=utf-8'
201
+ };
202
+ const withJson = Object.assign({}, c, {
203
+ json: (data, status = 200, extraHeaders)=>{
204
+ const responseHeaders = new Headers(headers);
205
+ if (extraHeaders) new Headers(extraHeaders).forEach((value, key)=>{
206
+ responseHeaders.set(key, value);
207
+ });
208
+ return new Response(JSON.stringify(data), {
209
+ status,
210
+ headers: responseHeaders
211
+ });
212
+ }
213
+ });
214
+ return withJson;
215
+ }
216
+ constructor(api){
217
+ this.isEffect = true;
218
+ this.effectMiddleware = null;
219
+ this.handler = null;
220
+ this.dispose = null;
221
+ this.registerMiddleware = async (options)=>{
222
+ const { prefix, enableHandleWeb } = options;
223
+ const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
224
+ if ('hono' === bffRuntimeFramework) {
225
+ this.isEffect = false;
226
+ return;
227
+ }
228
+ await this.reloadHandler();
229
+ this.effectMiddleware = {
230
+ name: 'effect-bff-handler',
231
+ path: enableHandleWeb ? '*' : `${prefix}/*`,
232
+ method: 'all',
233
+ order: 'post',
234
+ before: before,
235
+ handler: async (c, next)=>{
236
+ if (!this.handler) {
237
+ if (enableHandleWeb) return void await next();
238
+ return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
239
+ }
240
+ let response;
241
+ try {
242
+ const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
243
+ const effectContext = {
244
+ request: effectRequest,
245
+ env: c.env,
246
+ path: c.req.path,
247
+ method: c.req.method
248
+ };
249
+ response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
250
+ } catch (error) {
251
+ return this.handleRuntimeError(error, c);
252
+ }
253
+ if (!maybeResponse(response)) return this.handleRuntimeError(new Error('[BFF][Effect] Effect handler must return a Response instance.'), c);
254
+ if (404 === response.status && enableHandleWeb) return void await next();
255
+ return new Response(response.body, response);
256
+ }
257
+ };
258
+ globalMiddlewares.push(this.effectMiddleware);
259
+ };
260
+ this.onApiHandlersUpdated = async ()=>{
261
+ if (!this.isEffect || isProd()) return;
262
+ await this.reloadHandler();
263
+ };
264
+ this.api = api;
265
+ }
266
+ }
267
+ export { EffectAdapter };
@@ -0,0 +1,11 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const kEffectContextStorage = Symbol.for('modernjs.plugin-bff.effectContextStorage');
3
+ const globalStore = globalThis;
4
+ const effectContextStorage = globalStore[kEffectContextStorage] ?? (globalStore[kEffectContextStorage] = new AsyncLocalStorage());
5
+ const runWithEffectContext = (context, cb)=>effectContextStorage.run(context, cb);
6
+ const useEffectContext = ()=>{
7
+ const context = effectContextStorage.getStore();
8
+ if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
9
+ return context;
10
+ };
11
+ export { runWithEffectContext, useEffectContext };