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

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
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ resolveEffectBffModuleHandler: ()=>resolveEffectBffModuleHandler
28
+ });
29
+ const httpapi_namespaceObject = require("effect/unstable/httpapi");
30
+ const external_handler_js_namespaceObject = require("./handler.js");
31
+ function isRecord(value) {
32
+ return 'object' == typeof value && null !== value;
33
+ }
34
+ function includesRuntimeExports(value) {
35
+ return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
36
+ }
37
+ function isRequestHandler(value) {
38
+ return 'function' == typeof value;
39
+ }
40
+ function isEffectApiDefinition(module) {
41
+ return httpapi_namespaceObject.HttpApi.isHttpApi(module.api) && void 0 !== module.layer;
42
+ }
43
+ async function resolveEffectBffModuleHandler(mod, options = {}) {
44
+ let normalizedModule = mod;
45
+ const mergeRuntimeExports = (value)=>{
46
+ if (!isRecord(value) || !includesRuntimeExports(value)) return;
47
+ normalizedModule = {
48
+ ...normalizedModule,
49
+ ...value
50
+ };
51
+ };
52
+ if (isRequestHandler(normalizedModule.handler)) return {
53
+ handler: normalizedModule.handler
54
+ };
55
+ const entry = normalizedModule.default;
56
+ if (isRequestHandler(entry)) return {
57
+ handler: entry
58
+ };
59
+ if ('function' == typeof entry && 0 === entry.length) {
60
+ const out = await entry();
61
+ if (isRequestHandler(out)) return {
62
+ handler: out
63
+ };
64
+ mergeRuntimeExports(out);
65
+ }
66
+ if (isRecord(entry)) normalizedModule = {
67
+ ...normalizedModule,
68
+ ...entry
69
+ };
70
+ if (isRecord(entry) && 'handler' in entry) {
71
+ const maybeHandler = entry.handler;
72
+ if (isRequestHandler(maybeHandler)) normalizedModule = {
73
+ ...normalizedModule,
74
+ handler: maybeHandler
75
+ };
76
+ }
77
+ if (isRequestHandler(normalizedModule.handler)) return {
78
+ handler: normalizedModule.handler
79
+ };
80
+ if ('function' == typeof normalizedModule.createHandler) {
81
+ const webHandler = normalizedModule.createHandler({
82
+ openapi: options.openapi,
83
+ dataPlatform: options.dataPlatform
84
+ });
85
+ return {
86
+ handler: async (request, context)=>webHandler.handler(request, context),
87
+ dispose: async ()=>{
88
+ await webHandler.dispose();
89
+ }
90
+ };
91
+ }
92
+ if (isEffectApiDefinition(normalizedModule)) {
93
+ options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
94
+ const webHandler = (0, external_handler_js_namespaceObject.createHttpApiHandler)({
95
+ api: normalizedModule.api,
96
+ layer: normalizedModule.layer,
97
+ openapi: options.openapi,
98
+ dataPlatform: options.dataPlatform
99
+ });
100
+ return {
101
+ handler: async (request, context)=>webHandler.handler(request, context),
102
+ dispose: async ()=>{
103
+ await webHandler.dispose();
104
+ }
105
+ };
106
+ }
107
+ return null;
108
+ }
109
+ exports.resolveEffectBffModuleHandler = __webpack_exports__.resolveEffectBffModuleHandler;
110
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
111
+ "resolveEffectBffModuleHandler"
112
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
113
+ Object.defineProperty(exports, '__esModule', {
114
+ value: true
115
+ });
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ createEffectOperationContext: ()=>createEffectOperationContext
28
+ });
29
+ const create_request_namespaceObject = require("@modern-js/create-request");
30
+ const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
31
+ const readHeader = (headers, header)=>{
32
+ const value = headers.get(header);
33
+ return value && value.length > 0 ? value : void 0;
34
+ };
35
+ const copyStringField = (target, details, key)=>{
36
+ const value = details[key];
37
+ if ('string' == typeof value && value.length > 0) target[key] = value;
38
+ };
39
+ const parseTraceparent = (traceparent)=>{
40
+ if (!traceparent) return;
41
+ const match = traceparent.trim().match(TRACEPARENT_REGEX);
42
+ if (!match) return;
43
+ const [, traceId, spanId] = match;
44
+ if (!traceId || !spanId) return;
45
+ return {
46
+ traceId: traceId.toLowerCase(),
47
+ spanId: spanId.toLowerCase()
48
+ };
49
+ };
50
+ const readOperationContextDetails = (request)=>{
51
+ const rawDetails = readHeader(request.headers, create_request_namespaceObject.BFF_OPERATION_CONTEXT_DETAIL_HEADER);
52
+ if (!rawDetails) return {};
53
+ try {
54
+ const parsed = JSON.parse(rawDetails);
55
+ if (!parsed || 'object' != typeof parsed || Array.isArray(parsed)) return {};
56
+ const details = parsed;
57
+ const safeDetails = {};
58
+ copyStringField(safeDetails, details, 'requestId');
59
+ copyStringField(safeDetails, details, 'operationId');
60
+ copyStringField(safeDetails, details, 'schemaHash');
61
+ copyStringField(safeDetails, details, 'traceparent');
62
+ copyStringField(safeDetails, details, 'traceId');
63
+ copyStringField(safeDetails, details, 'spanId');
64
+ if ('number' == typeof details.operationVersion) safeDetails.operationVersion = details.operationVersion;
65
+ return safeDetails;
66
+ } catch {
67
+ return {};
68
+ }
69
+ };
70
+ const createEffectOperationContext = ({ request, path, method })=>{
71
+ const details = readOperationContextDetails(request);
72
+ const servicePath = new URL(request.url).pathname;
73
+ const traceparent = readHeader(request.headers, create_request_namespaceObject.BFF_TRACEPARENT_HEADER) || details.traceparent;
74
+ const parsedTraceparent = details.traceId && details.spanId ? {
75
+ traceId: details.traceId,
76
+ spanId: details.spanId
77
+ } : parseTraceparent(traceparent);
78
+ const locale = readHeader(request.headers, create_request_namespaceObject.BFF_LOCALE_HEADER);
79
+ const headerOperationId = readHeader(request.headers, create_request_namespaceObject.BFF_OPERATION_CONTEXT_HEADER);
80
+ return {
81
+ ...details,
82
+ ...headerOperationId || details.operationId ? {
83
+ operationId: headerOperationId || details.operationId
84
+ } : {},
85
+ routePath: servicePath,
86
+ method: (method || request.method || 'GET').toUpperCase(),
87
+ source: 'effect-adapter',
88
+ ...path && path !== servicePath ? {
89
+ attributes: {
90
+ mountedPath: path
91
+ }
92
+ } : {},
93
+ ...locale ? {
94
+ locale
95
+ } : {},
96
+ ...traceparent ? {
97
+ traceparent
98
+ } : {},
99
+ ...parsedTraceparent ? {
100
+ traceId: parsedTraceparent.traceId,
101
+ spanId: parsedTraceparent.spanId
102
+ } : {}
103
+ };
104
+ };
105
+ exports.createEffectOperationContext = __webpack_exports__.createEffectOperationContext;
106
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
107
+ "createEffectOperationContext"
108
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
109
+ Object.defineProperty(exports, '__esModule', {
110
+ value: true
111
+ });
@@ -45,6 +45,7 @@ __webpack_require__.d(__webpack_exports__, {
45
45
  runEffectView: ()=>runEffectView,
46
46
  view: ()=>view
47
47
  });
48
+ const create_request_namespaceObject = require("@modern-js/create-request");
48
49
  const Data_namespaceObject = require("effect/Data");
49
50
  const Effect_namespaceObject = require("effect/Effect");
50
51
  const Exit_namespaceObject = require("effect/Exit");
@@ -94,8 +95,19 @@ function getRpcSerializationLayer(serialization) {
94
95
  }
95
96
  }
96
97
  function makeEffectHttpApiClient(api, options) {
98
+ const requestContextHeaders = (0, create_request_namespaceObject.createRequestContextHeaders)(options?.requestContext);
99
+ const transformClient = (client)=>{
100
+ const contextClient = 0 === Object.keys(requestContextHeaders).length ? client : client.pipe(http_namespaceObject.HttpClient.mapRequest((request)=>{
101
+ let nextRequest = request;
102
+ for (const [header, value] of Object.entries(requestContextHeaders))if (void 0 === nextRequest.headers[header.toLowerCase()]) nextRequest = http_namespaceObject.HttpClientRequest.setHeader(nextRequest, header, value);
103
+ return nextRequest;
104
+ }));
105
+ return options?.transformClient ? options.transformClient(contextClient) : contextClient;
106
+ };
97
107
  return httpapi_namespaceObject.HttpApiClient.make(api, {
98
- baseUrl: options?.baseUrl
108
+ baseUrl: options?.baseUrl,
109
+ transformClient,
110
+ transformResponse: options?.transformResponse
99
111
  }).pipe(Effect_namespaceObject.provide(http_namespaceObject.FetchHttpClient.layer));
100
112
  }
101
113
  function makeEffectRpcClient(group, options) {
@@ -621,9 +621,26 @@ export type EffectOperationManifest = Record<
621
621
  string,
622
622
  Record<string, EffectOperationDescriptor>
623
623
  >;
624
+ export type EffectOperationContext = {
625
+ requestId?: string;
626
+ operationId?: string;
627
+ routePath?: string;
628
+ method?: string;
629
+ schemaHash?: string;
630
+ operationVersion?: number;
631
+ locale?: string;
632
+ traceparent?: string;
633
+ traceId?: string;
634
+ spanId?: string;
635
+ source?: string;
636
+ scope?: Record<string, unknown>;
637
+ sessionClaims?: Record<string, unknown>;
638
+ attributes?: Record<string, unknown>;
639
+ };
624
640
  export type EffectRequestContext = {
625
641
  headers?: Record<string, string>;
626
642
  locale?: string;
643
+ operationContext?: EffectOperationContext;
627
644
  traceparent?: string;
628
645
  traceId?: string;
629
646
  spanId?: string;
@@ -2,6 +2,24 @@ import { generateClient } from "@modern-js/bff-core";
2
2
  import { logger } from "@modern-js/utils";
3
3
  import path from "path";
4
4
  import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
5
+ async function transformEffectRuntimeSource(source, filename) {
6
+ const swc = await import("@swc/core");
7
+ const result = await swc.transform(source, {
8
+ filename,
9
+ sourceMaps: false,
10
+ jsc: {
11
+ parser: {
12
+ syntax: "typescript",
13
+ tsx: filename.endsWith('.tsx') || filename.endsWith('.jsx')
14
+ },
15
+ target: 'es2022'
16
+ },
17
+ module: {
18
+ type: 'es6'
19
+ }
20
+ });
21
+ return result.code;
22
+ }
5
23
  async function loader(source) {
6
24
  this.cacheable();
7
25
  const { resourcePath } = this;
@@ -13,6 +31,11 @@ async function loader(source) {
13
31
  apiDir: draftOptions.apiDir,
14
32
  effectEntry: draftOptions.effectEntry
15
33
  });
34
+ if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath) && this.resourceQuery.includes('modern-bff-runtime')) {
35
+ const code = await transformEffectRuntimeSource(source, resourcePath);
36
+ callback(void 0, code);
37
+ return;
38
+ }
16
39
  if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath)) {
17
40
  const code = await generateEffectClientCode({
18
41
  appDir: draftOptions.appDir,
@@ -1,3 +1,25 @@
1
+ import { trace as api_trace } from "@opentelemetry/api";
2
+ const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
3
+ function createDataBatchTransportTelemetryAttributes(event) {
4
+ return {
5
+ 'modernjs.data.batch.type': event.type,
6
+ 'modernjs.data.batch.endpoint': event.endpoint,
7
+ 'modernjs.data.batch.degraded': 'fallback' === event.type || 'disable' === event.type,
8
+ ...event.batchId ? {
9
+ 'modernjs.data.batch.id': event.batchId
10
+ } : {},
11
+ ...'number' == typeof event.size ? {
12
+ 'modernjs.data.batch.size': event.size
13
+ } : {},
14
+ ...event.reason ? {
15
+ 'modernjs.data.batch.reason': event.reason
16
+ } : {}
17
+ };
18
+ }
19
+ function emitDataBatchTransportEvent(onEvent, event) {
20
+ onEvent?.(event);
21
+ api_trace.getActiveSpan()?.addEvent(DATA_BATCH_TRANSPORT_OTEL_EVENT, createDataBatchTransportTelemetryAttributes(event));
22
+ }
1
23
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
2
24
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
3
25
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
@@ -415,7 +437,7 @@ function createDataBatchTransport(options = {}) {
415
437
  bucket.items = [];
416
438
  bucket.bytes = 0;
417
439
  if (1 === items.length || disabledEndpoints.has(endpoint)) {
418
- onEvent?.({
440
+ emitDataBatchTransportEvent(onEvent, {
419
441
  type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
420
442
  endpoint,
421
443
  size: items.length,
@@ -432,7 +454,7 @@ function createDataBatchTransport(options = {}) {
432
454
  sentAt: Date.now(),
433
455
  items: items.map((item)=>item.item)
434
456
  };
435
- onEvent?.({
457
+ emitDataBatchTransportEvent(onEvent, {
436
458
  type: 'flush',
437
459
  endpoint,
438
460
  batchId,
@@ -459,7 +481,7 @@ function createDataBatchTransport(options = {}) {
459
481
  requestInit.signal = controller.signal;
460
482
  timeoutHandle = setTimeout(()=>{
461
483
  controller.abort();
462
- onEvent?.({
484
+ emitDataBatchTransportEvent(onEvent, {
463
485
  type: 'fallback',
464
486
  endpoint,
465
487
  batchId,
@@ -472,13 +494,13 @@ function createDataBatchTransport(options = {}) {
472
494
  if (!response.ok) {
473
495
  if (404 === response.status || 405 === response.status) {
474
496
  disabledEndpoints.add(endpoint);
475
- onEvent?.({
497
+ emitDataBatchTransportEvent(onEvent, {
476
498
  type: 'disable',
477
499
  endpoint,
478
500
  batchId,
479
501
  reason: `batch-endpoint-unavailable-${String(response.status)}`
480
502
  });
481
- } else onEvent?.({
503
+ } else emitDataBatchTransportEvent(onEvent, {
482
504
  type: 'fallback',
483
505
  endpoint,
484
506
  batchId,
@@ -491,7 +513,7 @@ function createDataBatchTransport(options = {}) {
491
513
  }
492
514
  const result = await response.json();
493
515
  if (!isBatchResponsePayload(result)) {
494
- onEvent?.({
516
+ emitDataBatchTransportEvent(onEvent, {
495
517
  type: 'fallback',
496
518
  endpoint,
497
519
  batchId,
@@ -514,7 +536,7 @@ function createDataBatchTransport(options = {}) {
514
536
  return parseResponseLikeCreateRequest(reconstructedResponse);
515
537
  });
516
538
  } catch (error) {
517
- onEvent?.({
539
+ emitDataBatchTransportEvent(onEvent, {
518
540
  type: 'fallback',
519
541
  endpoint,
520
542
  batchId,
@@ -581,7 +603,7 @@ function createDataBatchTransport(options = {}) {
581
603
  };
582
604
  bucket.items.push(queued);
583
605
  bucket.bytes += size;
584
- onEvent?.({
606
+ emitDataBatchTransportEvent(onEvent, {
585
607
  type: 'enqueue',
586
608
  endpoint,
587
609
  size: bucket.items.length
@@ -596,4 +618,4 @@ function createDataBatchTransport(options = {}) {
596
618
  return promise;
597
619
  };
598
620
  }
599
- 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 };
621
+ 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,8 +1,7 @@
1
1
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
2
- import { HttpApi } from "effect/unstable/httpapi";
3
2
  import path from "path";
4
- import { runWithEffectContext } from "./context.mjs";
5
- import { createHttpApiHandler } from "./index.mjs";
3
+ import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
4
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
6
5
  const before = [
7
6
  'custom-server-hook',
8
7
  'custom-server-middleware',
@@ -35,24 +34,9 @@ function createRequestForMountedPrefix(req, prefix) {
35
34
  url.pathname = nextPath;
36
35
  return new Request(url, req);
37
36
  }
38
- function isRequestHandler(value) {
39
- return 'function' == typeof value;
40
- }
41
37
  function maybeResponse(value) {
42
38
  return value instanceof Response;
43
39
  }
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
40
  class EffectAdapter {
57
41
  resolveEntryFile() {
58
42
  const { appDirectory, apiDirectory } = this.api.getServerContext();
@@ -63,70 +47,13 @@ class EffectAdapter {
63
47
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
64
48
  }
65
49
  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;
50
+ return resolveEffectBffModuleHandler(mod, {
51
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
52
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
53
+ onWarning: (message)=>{
54
+ logger.warn(message);
55
+ }
56
+ });
130
57
  }
131
58
  async reloadHandler() {
132
59
  if (!this.isEffect) return;
@@ -244,7 +171,13 @@ class EffectAdapter {
244
171
  request: effectRequest,
245
172
  env: c.env,
246
173
  path: c.req.path,
247
- method: c.req.method
174
+ method: c.req.method,
175
+ operationContext: createEffectOperationContext({
176
+ request: effectRequest,
177
+ env: c.env,
178
+ path: c.req.path,
179
+ method: c.req.method
180
+ })
248
181
  };
249
182
  response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
250
183
  } catch (error) {
@@ -8,4 +8,6 @@ const useEffectContext = ()=>{
8
8
  if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
9
9
  return context;
10
10
  };
11
- export { runWithEffectContext, useEffectContext };
11
+ const useOperationContext = ()=>useEffectContext().operationContext;
12
+ export { createEffectOperationContext } from "./operation-context.mjs";
13
+ export { runWithEffectContext, useEffectContext, useOperationContext };
@@ -0,0 +1,90 @@
1
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
2
+ import { createEffectOperationContext } from "./operation-context.mjs";
3
+ export * from "./handler.mjs";
4
+ function normalizePrefix(prefix) {
5
+ if (!prefix || '/' === prefix) return '';
6
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
7
+ }
8
+ function removePrefixFromPath(pathname, prefix) {
9
+ const normalized = normalizePrefix(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 matchesPrefix(pathname, prefix) {
15
+ const normalized = normalizePrefix(prefix);
16
+ return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
17
+ }
18
+ function createRequestForMountedPrefix(req, prefix) {
19
+ const url = new URL(req.url);
20
+ const nextPath = removePrefixFromPath(url.pathname, prefix);
21
+ if (nextPath === url.pathname) return req;
22
+ url.pathname = nextPath;
23
+ return new Request(url, req);
24
+ }
25
+ function createEdgeEffectContext(originalRequest, effectRequest, options) {
26
+ const originalPath = options.path || new URL(originalRequest.url).pathname;
27
+ const method = options.method || originalRequest.method;
28
+ return {
29
+ request: effectRequest,
30
+ env: options.env || {},
31
+ path: originalPath,
32
+ method,
33
+ operationContext: createEffectOperationContext({
34
+ request: effectRequest,
35
+ env: options.env || {},
36
+ path: originalPath,
37
+ method
38
+ })
39
+ };
40
+ }
41
+ function createRuntimeErrorResponse(error) {
42
+ const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
43
+ return new Response(JSON.stringify({
44
+ message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
45
+ }), {
46
+ status,
47
+ headers: {
48
+ 'content-type': 'application/json; charset=utf-8'
49
+ }
50
+ });
51
+ }
52
+ async function dispatchEffectBffRequest(handler, request, options = {}) {
53
+ const requestPathname = new URL(request.url).pathname;
54
+ if (!matchesPrefix(requestPathname, options.prefix)) return new Response(null, {
55
+ status: 404
56
+ });
57
+ const effectRequest = createRequestForMountedPrefix(request, options.prefix);
58
+ const effectContext = createEdgeEffectContext(request, effectRequest, options);
59
+ try {
60
+ const response = handler.length > 1 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
61
+ if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
62
+ return new Response(response.body, response);
63
+ } catch (error) {
64
+ if (error instanceof Response) return new Response(error.body, error);
65
+ if (options.onError) {
66
+ const errorResponse = await options.onError(error, effectContext);
67
+ if (errorResponse instanceof Response) return errorResponse;
68
+ }
69
+ return createRuntimeErrorResponse(error);
70
+ }
71
+ }
72
+ async function createEffectBffEdgeHandler(options) {
73
+ const loaded = await resolveEffectBffModuleHandler(options.module, {
74
+ openapi: options.openapi,
75
+ dataPlatform: options.dataPlatform,
76
+ onWarning: options.onWarning
77
+ });
78
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
79
+ return {
80
+ handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
81
+ ...dispatchOptions,
82
+ prefix: options.prefix,
83
+ onError: options.onError
84
+ }),
85
+ dispose: async ()=>{
86
+ await loaded.dispose?.();
87
+ }
88
+ };
89
+ }
90
+ export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };