@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.9 → 3.2.0-ultramodern.91

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 (43) hide show
  1. package/dist/cjs/loader.js +23 -0
  2. package/dist/cjs/runtime/data-platform/index.js +39 -8
  3. package/dist/cjs/runtime/effect/adapter.js +15 -82
  4. package/dist/cjs/runtime/effect/context.js +10 -2
  5. package/dist/cjs/runtime/effect/edge.js +169 -0
  6. package/dist/cjs/runtime/effect/handler.js +598 -0
  7. package/dist/cjs/runtime/effect/index.js +16 -545
  8. package/dist/cjs/runtime/effect/module.js +115 -0
  9. package/dist/cjs/runtime/effect/operation-context.js +111 -0
  10. package/dist/cjs/runtime/effect-client/index.js +13 -1
  11. package/dist/cjs/utils/effectClientGenerator.js +17 -0
  12. package/dist/esm/loader.mjs +23 -0
  13. package/dist/esm/runtime/data-platform/index.mjs +31 -9
  14. package/dist/esm/runtime/effect/adapter.mjs +16 -83
  15. package/dist/esm/runtime/effect/context.mjs +3 -1
  16. package/dist/esm/runtime/effect/edge.mjs +90 -0
  17. package/dist/esm/runtime/effect/handler.mjs +437 -0
  18. package/dist/esm/runtime/effect/index.mjs +2 -438
  19. package/dist/esm/runtime/effect/module.mjs +81 -0
  20. package/dist/esm/runtime/effect/operation-context.mjs +77 -0
  21. package/dist/esm/runtime/effect-client/index.mjs +14 -2
  22. package/dist/esm/utils/effectClientGenerator.mjs +17 -0
  23. package/dist/esm-node/loader.mjs +23 -0
  24. package/dist/esm-node/runtime/data-platform/index.mjs +31 -9
  25. package/dist/esm-node/runtime/effect/adapter.mjs +16 -83
  26. package/dist/esm-node/runtime/effect/context.mjs +3 -1
  27. package/dist/esm-node/runtime/effect/edge.mjs +91 -0
  28. package/dist/esm-node/runtime/effect/handler.mjs +438 -0
  29. package/dist/esm-node/runtime/effect/index.mjs +2 -438
  30. package/dist/esm-node/runtime/effect/module.mjs +82 -0
  31. package/dist/esm-node/runtime/effect/operation-context.mjs +78 -0
  32. package/dist/esm-node/runtime/effect-client/index.mjs +14 -2
  33. package/dist/esm-node/utils/effectClientGenerator.mjs +17 -0
  34. package/dist/types/runtime/create-request/index.d.ts +1 -0
  35. package/dist/types/runtime/data-platform/index.d.ts +4 -0
  36. package/dist/types/runtime/effect/context.d.ts +3 -6
  37. package/dist/types/runtime/effect/edge.d.ts +25 -0
  38. package/dist/types/runtime/effect/handler.d.ts +170 -0
  39. package/dist/types/runtime/effect/index.d.ts +2 -171
  40. package/dist/types/runtime/effect/module.d.ts +28 -0
  41. package/dist/types/runtime/effect/operation-context.d.ts +10 -0
  42. package/dist/types/runtime/effect-client/index.d.ts +6 -1
  43. package/package.json +27 -18
@@ -4,6 +4,24 @@ import { generateClient } from "@modern-js/bff-core";
4
4
  import { logger } from "@modern-js/utils";
5
5
  import path from "path";
6
6
  import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
7
+ async function transformEffectRuntimeSource(source, filename) {
8
+ const swc = await import("@swc/core");
9
+ const result = await swc.transform(source, {
10
+ filename,
11
+ sourceMaps: false,
12
+ jsc: {
13
+ parser: {
14
+ syntax: "typescript",
15
+ tsx: filename.endsWith('.tsx') || filename.endsWith('.jsx')
16
+ },
17
+ target: 'es2022'
18
+ },
19
+ module: {
20
+ type: 'es6'
21
+ }
22
+ });
23
+ return result.code;
24
+ }
7
25
  async function loader(source) {
8
26
  this.cacheable();
9
27
  const { resourcePath } = this;
@@ -15,6 +33,11 @@ async function loader(source) {
15
33
  apiDir: draftOptions.apiDir,
16
34
  effectEntry: draftOptions.effectEntry
17
35
  });
36
+ if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath) && this.resourceQuery.includes('modern-bff-runtime')) {
37
+ const code = await transformEffectRuntimeSource(source, resourcePath);
38
+ callback(void 0, code);
39
+ return;
40
+ }
18
41
  if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath)) {
19
42
  const code = await generateEffectClientCode({
20
43
  appDir: draftOptions.appDir,
@@ -1,4 +1,26 @@
1
1
  import "node:module";
2
+ import { trace as api_trace } from "@opentelemetry/api";
3
+ const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
4
+ function createDataBatchTransportTelemetryAttributes(event) {
5
+ return {
6
+ 'modernjs.data.batch.type': event.type,
7
+ 'modernjs.data.batch.endpoint': event.endpoint,
8
+ 'modernjs.data.batch.degraded': 'fallback' === event.type || 'disable' === event.type,
9
+ ...event.batchId ? {
10
+ 'modernjs.data.batch.id': event.batchId
11
+ } : {},
12
+ ...'number' == typeof event.size ? {
13
+ 'modernjs.data.batch.size': event.size
14
+ } : {},
15
+ ...event.reason ? {
16
+ 'modernjs.data.batch.reason': event.reason
17
+ } : {}
18
+ };
19
+ }
20
+ function emitDataBatchTransportEvent(onEvent, event) {
21
+ onEvent?.(event);
22
+ api_trace.getActiveSpan()?.addEvent(DATA_BATCH_TRANSPORT_OTEL_EVENT, createDataBatchTransportTelemetryAttributes(event));
23
+ }
2
24
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
3
25
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
4
26
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
@@ -416,7 +438,7 @@ function createDataBatchTransport(options = {}) {
416
438
  bucket.items = [];
417
439
  bucket.bytes = 0;
418
440
  if (1 === items.length || disabledEndpoints.has(endpoint)) {
419
- onEvent?.({
441
+ emitDataBatchTransportEvent(onEvent, {
420
442
  type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
421
443
  endpoint,
422
444
  size: items.length,
@@ -433,7 +455,7 @@ function createDataBatchTransport(options = {}) {
433
455
  sentAt: Date.now(),
434
456
  items: items.map((item)=>item.item)
435
457
  };
436
- onEvent?.({
458
+ emitDataBatchTransportEvent(onEvent, {
437
459
  type: 'flush',
438
460
  endpoint,
439
461
  batchId,
@@ -460,7 +482,7 @@ function createDataBatchTransport(options = {}) {
460
482
  requestInit.signal = controller.signal;
461
483
  timeoutHandle = setTimeout(()=>{
462
484
  controller.abort();
463
- onEvent?.({
485
+ emitDataBatchTransportEvent(onEvent, {
464
486
  type: 'fallback',
465
487
  endpoint,
466
488
  batchId,
@@ -473,13 +495,13 @@ function createDataBatchTransport(options = {}) {
473
495
  if (!response.ok) {
474
496
  if (404 === response.status || 405 === response.status) {
475
497
  disabledEndpoints.add(endpoint);
476
- onEvent?.({
498
+ emitDataBatchTransportEvent(onEvent, {
477
499
  type: 'disable',
478
500
  endpoint,
479
501
  batchId,
480
502
  reason: `batch-endpoint-unavailable-${String(response.status)}`
481
503
  });
482
- } else onEvent?.({
504
+ } else emitDataBatchTransportEvent(onEvent, {
483
505
  type: 'fallback',
484
506
  endpoint,
485
507
  batchId,
@@ -492,7 +514,7 @@ function createDataBatchTransport(options = {}) {
492
514
  }
493
515
  const result = await response.json();
494
516
  if (!isBatchResponsePayload(result)) {
495
- onEvent?.({
517
+ emitDataBatchTransportEvent(onEvent, {
496
518
  type: 'fallback',
497
519
  endpoint,
498
520
  batchId,
@@ -515,7 +537,7 @@ function createDataBatchTransport(options = {}) {
515
537
  return parseResponseLikeCreateRequest(reconstructedResponse);
516
538
  });
517
539
  } catch (error) {
518
- onEvent?.({
540
+ emitDataBatchTransportEvent(onEvent, {
519
541
  type: 'fallback',
520
542
  endpoint,
521
543
  batchId,
@@ -582,7 +604,7 @@ function createDataBatchTransport(options = {}) {
582
604
  };
583
605
  bucket.items.push(queued);
584
606
  bucket.bytes += size;
585
- onEvent?.({
607
+ emitDataBatchTransportEvent(onEvent, {
586
608
  type: 'enqueue',
587
609
  endpoint,
588
610
  size: bucket.items.length
@@ -597,4 +619,4 @@ function createDataBatchTransport(options = {}) {
597
619
  return promise;
598
620
  };
599
621
  }
600
- export { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
622
+ export { DATA_BATCH_TRANSPORT_OTEL_EVENT, DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createDataBatchTransportTelemetryAttributes, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, emitDataBatchTransportEvent, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
@@ -1,10 +1,9 @@
1
1
  import __rslib_shim_module__ from "node:module";
2
2
  const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
3
3
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
4
- import { HttpApi } from "effect/unstable/httpapi";
5
4
  import path from "path";
6
- import { runWithEffectContext } from "./context.mjs";
7
- import { createHttpApiHandler } from "./index.mjs";
5
+ import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
6
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
8
7
  const before = [
9
8
  'custom-server-hook',
10
9
  'custom-server-middleware',
@@ -37,24 +36,9 @@ function createRequestForMountedPrefix(req, prefix) {
37
36
  url.pathname = nextPath;
38
37
  return new Request(url, req);
39
38
  }
40
- function isRequestHandler(value) {
41
- return 'function' == typeof value;
42
- }
43
39
  function maybeResponse(value) {
44
40
  return value instanceof Response;
45
41
  }
46
- function isRecord(value) {
47
- return 'object' == typeof value && null !== value;
48
- }
49
- function includesRuntimeExports(value) {
50
- return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
51
- }
52
- function isHttpApiWithProps(value) {
53
- return HttpApi.isHttpApi(value) && isRecord(value) && 'string' == typeof value.identifier && isRecord(value.groups);
54
- }
55
- function isEffectApiDefinition(module) {
56
- return isHttpApiWithProps(module.api) && void 0 !== module.layer;
57
- }
58
42
  class EffectAdapter {
59
43
  resolveEntryFile() {
60
44
  const { appDirectory, apiDirectory } = this.api.getServerContext();
@@ -65,70 +49,13 @@ class EffectAdapter {
65
49
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
66
50
  }
67
51
  async loadEffectHandlerFromModule(mod) {
68
- let normalizedModule = mod;
69
- const mergeRuntimeExports = (value)=>{
70
- if (!isRecord(value) || !includesRuntimeExports(value)) return;
71
- normalizedModule = {
72
- ...normalizedModule,
73
- ...value
74
- };
75
- };
76
- if (isRequestHandler(normalizedModule.handler)) return {
77
- handler: normalizedModule.handler
78
- };
79
- const entry = normalizedModule.default;
80
- if (isRequestHandler(entry)) return {
81
- handler: entry
82
- };
83
- if ('function' == typeof entry && 0 === entry.length) {
84
- const out = await entry();
85
- if (isRequestHandler(out)) return {
86
- handler: out
87
- };
88
- mergeRuntimeExports(out);
89
- }
90
- if (isRecord(entry)) normalizedModule = {
91
- ...normalizedModule,
92
- ...entry
93
- };
94
- if (isRecord(entry) && 'handler' in entry) {
95
- const maybeHandler = entry.handler;
96
- if (isRequestHandler(maybeHandler)) normalizedModule = {
97
- ...normalizedModule,
98
- handler: maybeHandler
99
- };
100
- }
101
- if (isRequestHandler(normalizedModule.handler)) return {
102
- handler: normalizedModule.handler
103
- };
104
- if ('function' == typeof normalizedModule.createHandler) {
105
- const webHandler = normalizedModule.createHandler({
106
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
107
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
108
- });
109
- return {
110
- handler: async (request)=>webHandler.handler(request),
111
- dispose: async ()=>{
112
- await webHandler.dispose();
113
- }
114
- };
115
- }
116
- if (isEffectApiDefinition(normalizedModule)) {
117
- logger.warn('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
118
- const webHandler = createHttpApiHandler({
119
- api: normalizedModule.api,
120
- layer: normalizedModule.layer,
121
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
122
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
123
- });
124
- return {
125
- handler: async (request)=>webHandler.handler(request),
126
- dispose: async ()=>{
127
- await webHandler.dispose();
128
- }
129
- };
130
- }
131
- return null;
52
+ return resolveEffectBffModuleHandler(mod, {
53
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
54
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
55
+ onWarning: (message)=>{
56
+ logger.warn(message);
57
+ }
58
+ });
132
59
  }
133
60
  async reloadHandler() {
134
61
  if (!this.isEffect) return;
@@ -246,7 +173,13 @@ class EffectAdapter {
246
173
  request: effectRequest,
247
174
  env: c.env,
248
175
  path: c.req.path,
249
- method: c.req.method
176
+ method: c.req.method,
177
+ operationContext: createEffectOperationContext({
178
+ request: effectRequest,
179
+ env: c.env,
180
+ path: c.req.path,
181
+ method: c.req.method
182
+ })
250
183
  };
251
184
  response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
252
185
  } catch (error) {
@@ -9,4 +9,6 @@ const useEffectContext = ()=>{
9
9
  if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
10
10
  return context;
11
11
  };
12
- export { runWithEffectContext, useEffectContext };
12
+ const useOperationContext = ()=>useEffectContext().operationContext;
13
+ export { createEffectOperationContext } from "./operation-context.mjs";
14
+ export { runWithEffectContext, useEffectContext, useOperationContext };
@@ -0,0 +1,91 @@
1
+ import "node:module";
2
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
3
+ import { createEffectOperationContext } from "./operation-context.mjs";
4
+ export * from "./handler.mjs";
5
+ function normalizePrefix(prefix) {
6
+ if (!prefix || '/' === prefix) return '';
7
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
8
+ }
9
+ function removePrefixFromPath(pathname, prefix) {
10
+ const normalized = normalizePrefix(prefix);
11
+ if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
12
+ const sliced = pathname.slice(normalized.length);
13
+ return sliced.startsWith('/') ? sliced : `/${sliced}`;
14
+ }
15
+ function matchesPrefix(pathname, prefix) {
16
+ const normalized = normalizePrefix(prefix);
17
+ return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
18
+ }
19
+ function createRequestForMountedPrefix(req, prefix) {
20
+ const url = new URL(req.url);
21
+ const nextPath = removePrefixFromPath(url.pathname, prefix);
22
+ if (nextPath === url.pathname) return req;
23
+ url.pathname = nextPath;
24
+ return new Request(url, req);
25
+ }
26
+ function createEdgeEffectContext(originalRequest, effectRequest, options) {
27
+ const originalPath = options.path || new URL(originalRequest.url).pathname;
28
+ const method = options.method || originalRequest.method;
29
+ return {
30
+ request: effectRequest,
31
+ env: options.env || {},
32
+ path: originalPath,
33
+ method,
34
+ operationContext: createEffectOperationContext({
35
+ request: effectRequest,
36
+ env: options.env || {},
37
+ path: originalPath,
38
+ method
39
+ })
40
+ };
41
+ }
42
+ function createRuntimeErrorResponse(error) {
43
+ const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
44
+ return new Response(JSON.stringify({
45
+ message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
46
+ }), {
47
+ status,
48
+ headers: {
49
+ 'content-type': 'application/json; charset=utf-8'
50
+ }
51
+ });
52
+ }
53
+ async function dispatchEffectBffRequest(handler, request, options = {}) {
54
+ const requestPathname = new URL(request.url).pathname;
55
+ if (!matchesPrefix(requestPathname, options.prefix)) return new Response(null, {
56
+ status: 404
57
+ });
58
+ const effectRequest = createRequestForMountedPrefix(request, options.prefix);
59
+ const effectContext = createEdgeEffectContext(request, effectRequest, options);
60
+ try {
61
+ const response = handler.length > 1 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
62
+ if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
63
+ return new Response(response.body, response);
64
+ } catch (error) {
65
+ if (error instanceof Response) return new Response(error.body, error);
66
+ if (options.onError) {
67
+ const errorResponse = await options.onError(error, effectContext);
68
+ if (errorResponse instanceof Response) return errorResponse;
69
+ }
70
+ return createRuntimeErrorResponse(error);
71
+ }
72
+ }
73
+ async function createEffectBffEdgeHandler(options) {
74
+ const loaded = await resolveEffectBffModuleHandler(options.module, {
75
+ openapi: options.openapi,
76
+ dataPlatform: options.dataPlatform,
77
+ onWarning: options.onWarning
78
+ });
79
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
80
+ return {
81
+ handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
82
+ ...dispatchOptions,
83
+ prefix: options.prefix,
84
+ onError: options.onError
85
+ }),
86
+ dispose: async ()=>{
87
+ await loaded.dispose?.();
88
+ }
89
+ };
90
+ }
91
+ export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };