@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.14 → 3.9.0-ultramodern.16

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 (36) hide show
  1. package/dist/cjs/cloudflare/artifacts.js +8 -19
  2. package/dist/cjs/cloudflare/index.js +3 -1
  3. package/dist/cjs/deploy-output/plugin.js +22 -2
  4. package/dist/cjs/deploy-output/public-assets.js +146 -0
  5. package/dist/cjs/release-envelope/framework-output.js +24 -8
  6. package/dist/cjs/release-envelope/plugin.js +9 -1
  7. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  8. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  9. package/dist/esm/cloudflare/index.mjs +3 -1
  10. package/dist/esm/deploy-output/plugin.mjs +18 -1
  11. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  12. package/dist/esm/release-envelope/framework-output.mjs +24 -8
  13. package/dist/esm/release-envelope/plugin.mjs +9 -1
  14. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  15. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  16. package/dist/esm-node/cloudflare/index.mjs +3 -1
  17. package/dist/esm-node/deploy-output/plugin.mjs +18 -1
  18. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  19. package/dist/esm-node/release-envelope/framework-output.mjs +24 -8
  20. package/dist/esm-node/release-envelope/plugin.mjs +9 -1
  21. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  22. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  23. package/dist/types/config.d.ts +24 -0
  24. package/dist/types/deploy-output/plugin.d.ts +26 -0
  25. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  26. package/dist/types/release-envelope/framework-output.d.ts +6 -2
  27. package/dist/types/release-envelope/plugin.d.ts +4 -0
  28. package/package.json +14 -14
  29. package/src/cloudflare/artifacts.ts +22 -43
  30. package/src/cloudflare/index.ts +4 -2
  31. package/src/config.ts +26 -0
  32. package/src/deploy-output/plugin.ts +54 -0
  33. package/src/deploy-output/public-assets.ts +224 -0
  34. package/src/release-envelope/framework-output.ts +49 -1
  35. package/src/release-envelope/plugin.ts +22 -1
  36. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { normalizeRelativePath } from '../cloudflare/utils';
4
+
5
+ /**
6
+ * Deploy output directory that receives declared public assets on every
7
+ * target: Cloudflare Worker Static Assets and the Node deploy output.
8
+ */
9
+ export const DECLARED_PUBLIC_ASSET_DIRECTORY = 'public';
10
+
11
+ export interface DeclaredPublicAssetConfig {
12
+ from: string;
13
+ to: string;
14
+ }
15
+
16
+ export interface DeclaredPublicAssetScope {
17
+ /** Config path used in errors, for example `deploy.node.publicAssets`. */
18
+ label: string;
19
+ /** Output described in destination errors. */
20
+ output: string;
21
+ }
22
+
23
+ export interface DeclaredPublicAsset {
24
+ from: string;
25
+ to: string;
26
+ index: number;
27
+ }
28
+
29
+ export const CLOUDFLARE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope = {
30
+ label: 'deploy.worker.publicAssets',
31
+ output: 'Cloudflare public output',
32
+ };
33
+
34
+ export const NODE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope = {
35
+ label: 'deploy.node.publicAssets',
36
+ output: 'Node public output',
37
+ };
38
+
39
+ export const normalizeDeclaredPublicAssets = (
40
+ assets: readonly DeclaredPublicAssetConfig[] | undefined,
41
+ scope: DeclaredPublicAssetScope,
42
+ ): DeclaredPublicAsset[] =>
43
+ (assets ?? []).map((asset, index) => ({
44
+ from: normalizeRelativePath(
45
+ asset?.from,
46
+ `${scope.label}[${index}].from`,
47
+ 'app root',
48
+ ),
49
+ to: normalizeRelativePath(
50
+ asset?.to,
51
+ `${scope.label}[${index}].to`,
52
+ scope.output,
53
+ { allowRoot: true },
54
+ ),
55
+ index,
56
+ }));
57
+
58
+ interface DeclaredPublicAssetFile {
59
+ index: number;
60
+ /** Destination relative to the deploy output root, POSIX separators. */
61
+ logicalPath: string;
62
+ }
63
+
64
+ const isMissingPathError = (error: unknown) =>
65
+ error instanceof Error &&
66
+ 'code' in error &&
67
+ (error as NodeJS.ErrnoException).code === 'ENOENT';
68
+
69
+ const pathExists = async (filePath: string) => {
70
+ try {
71
+ await fs.lstat(filePath);
72
+ return true;
73
+ } catch (error) {
74
+ if (isMissingPathError(error)) {
75
+ return false;
76
+ }
77
+ throw error;
78
+ }
79
+ };
80
+
81
+ const listDeclaredPublicAssetFiles = async (
82
+ appDirectory: string,
83
+ assets: readonly DeclaredPublicAsset[],
84
+ scope: DeclaredPublicAssetScope,
85
+ ): Promise<DeclaredPublicAssetFile[]> => {
86
+ const files: DeclaredPublicAssetFile[] = [];
87
+ const owners = new Map<string, number>();
88
+ const add = (index: number, logicalPath: string) => {
89
+ const owner = owners.get(logicalPath);
90
+ if (owner !== undefined) {
91
+ throw new Error(
92
+ `${scope.label}[${index}] and ${scope.label}[${owner}] both stage ${JSON.stringify(logicalPath)}.`,
93
+ );
94
+ }
95
+ owners.set(logicalPath, index);
96
+ files.push({ index, logicalPath });
97
+ };
98
+ const visit = async (
99
+ asset: DeclaredPublicAsset,
100
+ sourcePath: string,
101
+ destination: string,
102
+ ): Promise<void> => {
103
+ const stat = await fs.lstat(sourcePath);
104
+ if (stat.isSymbolicLink()) {
105
+ throw new Error(
106
+ `${scope.label}[${asset.index}].from must not contain symbolic links: ${path
107
+ .relative(appDirectory, sourcePath)
108
+ .replace(/\\/gu, '/')}`,
109
+ );
110
+ }
111
+ if (stat.isFile()) {
112
+ if (destination === DECLARED_PUBLIC_ASSET_DIRECTORY) {
113
+ throw new Error(
114
+ `${scope.label}[${asset.index}].to must name a file when ${scope.label}[${asset.index}].from is a file.`,
115
+ );
116
+ }
117
+ add(asset.index, destination);
118
+ return;
119
+ }
120
+ if (!stat.isDirectory()) {
121
+ throw new Error(
122
+ `${scope.label}[${asset.index}].from must contain only files and directories: ${path
123
+ .relative(appDirectory, sourcePath)
124
+ .replace(/\\/gu, '/')}`,
125
+ );
126
+ }
127
+ const entries = (await fs.readdir(sourcePath)).sort();
128
+ for (const entry of entries) {
129
+ await visit(
130
+ asset,
131
+ path.join(sourcePath, entry),
132
+ path.posix.join(destination, entry),
133
+ );
134
+ }
135
+ };
136
+
137
+ for (const asset of assets) {
138
+ const sourcePath = path.join(appDirectory, asset.from);
139
+ if (!(await pathExists(sourcePath))) {
140
+ throw new Error(
141
+ `${scope.label}[${asset.index}].from does not exist: ${asset.from}`,
142
+ );
143
+ }
144
+ await visit(
145
+ asset,
146
+ sourcePath,
147
+ path.posix.join(DECLARED_PUBLIC_ASSET_DIRECTORY, asset.to),
148
+ );
149
+ }
150
+ return files;
151
+ };
152
+
153
+ /**
154
+ * Resolve the files declared public assets add to a deploy output that was
155
+ * copied from `generatedRoot`, relative to the deploy output root. Declared
156
+ * files that replace a generated file are not part of the result, matching
157
+ * {@link stageDeclaredPublicAssets}.
158
+ */
159
+ export const resolveAddedDeclaredPublicAssetPaths = async ({
160
+ appDirectory,
161
+ assets,
162
+ generatedRoot,
163
+ scope,
164
+ }: {
165
+ appDirectory: string;
166
+ assets: readonly DeclaredPublicAsset[];
167
+ generatedRoot: string;
168
+ scope: DeclaredPublicAssetScope;
169
+ }): Promise<string[]> => {
170
+ const added: string[] = [];
171
+ for (const file of await listDeclaredPublicAssetFiles(
172
+ appDirectory,
173
+ assets,
174
+ scope,
175
+ )) {
176
+ if (!(await pathExists(path.join(generatedRoot, file.logicalPath)))) {
177
+ added.push(file.logicalPath);
178
+ }
179
+ }
180
+ return added.sort();
181
+ };
182
+
183
+ /**
184
+ * Copy declared public assets into the deploy output after the framework has
185
+ * staged its own public files.
186
+ *
187
+ * A declared file may replace a generated public file, such as an app-owned
188
+ * `robots.txt`; the replaced file keeps its generated release classification.
189
+ *
190
+ * @returns the files the declarations added, relative to the deploy output
191
+ * root. The release envelope records these as declared public assets.
192
+ */
193
+ export const stageDeclaredPublicAssets = async ({
194
+ appDirectory,
195
+ outputDirectory,
196
+ assets,
197
+ scope,
198
+ }: {
199
+ appDirectory: string;
200
+ outputDirectory: string;
201
+ assets: readonly DeclaredPublicAsset[];
202
+ scope: DeclaredPublicAssetScope;
203
+ }): Promise<string[]> => {
204
+ const files = await listDeclaredPublicAssetFiles(appDirectory, assets, scope);
205
+ const added: string[] = [];
206
+ for (const file of files) {
207
+ const asset = assets.find(candidate => candidate.index === file.index)!;
208
+ const relativePath = path.posix.relative(
209
+ path.posix.join(DECLARED_PUBLIC_ASSET_DIRECTORY, asset.to),
210
+ file.logicalPath,
211
+ );
212
+ const destination = path.join(outputDirectory, file.logicalPath);
213
+ const replacesGeneratedFile = await pathExists(destination);
214
+ await fs.mkdir(path.dirname(destination), { recursive: true });
215
+ await fs.copyFile(
216
+ path.join(appDirectory, asset.from, relativePath),
217
+ destination,
218
+ );
219
+ if (!replacesGeneratedFile) {
220
+ added.push(file.logicalPath);
221
+ }
222
+ }
223
+ return added.sort();
224
+ };
@@ -43,6 +43,28 @@ const isPublicMetadataPath = (logicalPath: string) =>
43
43
  logicalPath === 'public/_headers' ||
44
44
  /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
45
45
  const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
46
+ // Files an app declared through `deploy.<target>.publicAssets`. The deploy
47
+ // output stages them from app-root sources; they are served data, not a
48
+ // UI/client surface, so API-only units may carry them.
49
+ const DECLARED_PUBLIC_ASSET_RUNTIME = 'public-asset';
50
+
51
+ const resolveDeclaredPublicAssets = (
52
+ files: readonly string[],
53
+ declaredPublicAssets: readonly string[],
54
+ target: 'Node' | 'Cloudflare',
55
+ ) => {
56
+ const staged = new Set(files);
57
+ const declared = new Set<string>();
58
+ for (const logicalPath of declaredPublicAssets) {
59
+ if (!logicalPath.startsWith('public/') || !staged.has(logicalPath)) {
60
+ throw new Error(
61
+ `[ultramodern-release-envelope] final ${target} staging has no declared public asset file ${JSON.stringify(logicalPath)}.`,
62
+ );
63
+ }
64
+ declared.add(logicalPath);
65
+ }
66
+ return declared;
67
+ };
46
68
 
47
69
  const isRecord = (value: unknown): value is Record<string, unknown> =>
48
70
  typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -902,8 +924,14 @@ const createNodeStagedReleaseArtifactInputs = async (
902
924
  outputDirectory: string,
903
925
  identity: MicroVerticalReleaseIdentity,
904
926
  apiOnly: boolean,
927
+ declaredPublicAssets: readonly string[],
905
928
  ) => {
906
929
  const files = await collectFiles(outputDirectory);
930
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
931
+ files,
932
+ declaredPublicAssets,
933
+ 'Node',
934
+ );
907
935
  for (const requiredPath of [
908
936
  'index.js',
909
937
  'package.json',
@@ -928,7 +956,8 @@ const createNodeStagedReleaseArtifactInputs = async (
928
956
  topLevel === 'html' ||
929
957
  (topLevel === 'public' &&
930
958
  logicalPath !== CRAWLER_POLICY_PATH &&
931
- !isPublicMetadataPath(logicalPath)) ||
959
+ !isPublicMetadataPath(logicalPath) &&
960
+ !declaredPublicAssetPaths.has(logicalPath)) ||
932
961
  [
933
962
  'index.html',
934
963
  'mf-manifest.json',
@@ -992,6 +1021,9 @@ const createNodeStagedReleaseArtifactInputs = async (
992
1021
  for (const logicalPath of uiClientPaths) {
993
1022
  runtimeByPath.set(logicalPath, 'browser');
994
1023
  }
1024
+ for (const logicalPath of declaredPublicAssetPaths) {
1025
+ runtimeByPath.set(logicalPath, DECLARED_PUBLIC_ASSET_RUNTIME);
1026
+ }
995
1027
  if (files.includes(CRAWLER_POLICY_PATH)) {
996
1028
  runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
997
1029
  }
@@ -1058,9 +1090,12 @@ const createNodeStagedReleaseArtifactInputs = async (
1058
1090
  };
1059
1091
 
1060
1092
  export const emitNodeStagedReleaseEnvelope = async ({
1093
+ declaredPublicAssets = [],
1061
1094
  distDirectory,
1062
1095
  outputDirectory,
1063
1096
  }: {
1097
+ /** Staged `deploy.node.publicAssets` files, relative to `outputDirectory`. */
1098
+ declaredPublicAssets?: readonly string[];
1064
1099
  distDirectory: string;
1065
1100
  outputDirectory: string;
1066
1101
  }) => {
@@ -1072,6 +1107,7 @@ export const emitNodeStagedReleaseEnvelope = async ({
1072
1107
  outputDirectory,
1073
1108
  source.identity,
1074
1109
  source.surfaces.uiClient.length === 0,
1110
+ declaredPublicAssets,
1075
1111
  );
1076
1112
  const staged = await createMicroVerticalReleaseEnvelope({
1077
1113
  artifactRoot: outputDirectory,
@@ -1114,8 +1150,14 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1114
1150
  outputDirectory: string,
1115
1151
  identity: MicroVerticalReleaseIdentity,
1116
1152
  apiOnly: boolean,
1153
+ declaredPublicAssets: readonly string[],
1117
1154
  ) => {
1118
1155
  const files = await collectFiles(outputDirectory);
1156
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
1157
+ files,
1158
+ declaredPublicAssets,
1159
+ 'Cloudflare',
1160
+ );
1119
1161
  for (const requiredPath of [
1120
1162
  'server/index.mjs',
1121
1163
  'server/modern-worker-manifest.json',
@@ -1160,6 +1202,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1160
1202
  logicalPath.startsWith('public/') &&
1161
1203
  logicalPath !== CRAWLER_POLICY_PATH &&
1162
1204
  !isPublicMetadataPath(logicalPath) &&
1205
+ !declaredPublicAssetPaths.has(logicalPath) &&
1163
1206
  logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
1164
1207
  logicalPath !== backendManifestPath &&
1165
1208
  logicalPath !== backendContainerPath,
@@ -1229,6 +1272,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1229
1272
  }
1230
1273
  };
1231
1274
  add(uiClientPaths, 'browser');
1275
+ add([...declaredPublicAssetPaths], DECLARED_PUBLIC_ASSET_RUNTIME);
1232
1276
  if (files.includes(CRAWLER_POLICY_PATH)) {
1233
1277
  runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1234
1278
  }
@@ -1285,9 +1329,12 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1285
1329
  };
1286
1330
 
1287
1331
  export const emitCloudflareStagedReleaseEnvelope = async ({
1332
+ declaredPublicAssets = [],
1288
1333
  distDirectory,
1289
1334
  outputDirectory,
1290
1335
  }: {
1336
+ /** Staged `deploy.worker.publicAssets` files, relative to `outputDirectory`. */
1337
+ declaredPublicAssets?: readonly string[];
1291
1338
  distDirectory: string;
1292
1339
  outputDirectory: string;
1293
1340
  }) => {
@@ -1303,6 +1350,7 @@ export const emitCloudflareStagedReleaseEnvelope = async ({
1303
1350
  outputDirectory,
1304
1351
  source.identity,
1305
1352
  source.surfaces.uiClient.length === 0,
1353
+ declaredPublicAssets,
1306
1354
  );
1307
1355
  const staged = await createMicroVerticalReleaseEnvelope({
1308
1356
  artifactRoot: outputDirectory,
@@ -1,4 +1,10 @@
1
1
  import path from 'node:path';
2
+ import type { NodePublicAssetConfig } from '../config';
3
+ import {
4
+ NODE_PUBLIC_ASSET_SCOPE,
5
+ normalizeDeclaredPublicAssets,
6
+ resolveAddedDeclaredPublicAssetPaths,
7
+ } from '../deploy-output/public-assets';
2
8
  import {
3
9
  emitFrameworkMicroVerticalReleaseEnvelope,
4
10
  emitNodeStagedReleaseEnvelope,
@@ -9,7 +15,10 @@ import {
9
15
  type ReleaseEnvelopeTarget = 'cloudflare' | 'node' | string;
10
16
 
11
17
  export interface ReleaseEnvelopeConfig {
12
- deploy?: { target?: string };
18
+ deploy?: {
19
+ target?: string;
20
+ node?: { publicAssets?: NodePublicAssetConfig[] };
21
+ };
13
22
  }
14
23
 
15
24
  export interface ReleaseEnvelopeAppContext {
@@ -71,6 +80,7 @@ export const createUltramodernReleaseEnvelopePlugin = <
71
80
  '@modern-js/backend-federation-build',
72
81
  '@modern-js/plugin-bff',
73
82
  '@modern-js/deploy-output-aliases',
83
+ '@modern-js/deploy-output-public-assets',
74
84
  ],
75
85
  post: ['@modern-js/plugin-deploy'],
76
86
  setup(api) {
@@ -123,6 +133,17 @@ export const createUltramodernReleaseEnvelopePlugin = <
123
133
  const { appDirectory, distDirectory } = api.getAppContext();
124
134
  const outputDirectory = path.join(appDirectory, '.output');
125
135
  const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
136
+ // The Node deploy output is a copy of dist, so a declared file
137
+ // added output exactly when dist has no file at its path.
138
+ declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
139
+ appDirectory,
140
+ assets: normalizeDeclaredPublicAssets(
141
+ api.getNormalizedConfig().deploy?.node?.publicAssets,
142
+ NODE_PUBLIC_ASSET_SCOPE,
143
+ ),
144
+ generatedRoot: distDirectory,
145
+ scope: NODE_PUBLIC_ASSET_SCOPE,
146
+ }),
126
147
  distDirectory,
127
148
  outputDirectory,
128
149
  });
@@ -160,14 +160,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
160
160
  );
161
161
  }
162
162
 
163
- function matchesPrefix(pathname, prefix) {
163
+ // Worker Static Assets decode percent-encoded paths, so an encoded spelling
164
+ // of a mounted prefix (`/%70refix/...`, `/prefix%2F...`) would otherwise skip
165
+ // the BFF or service binding that owns the prefix and reach ASSETS directly.
166
+ // Resolve such spellings to the canonical pathname the owner sees; a
167
+ // backslash or undecodable spelling becomes a path the owner rejects.
168
+ function resolvePrefixPathname(pathname, prefix) {
164
169
  if (!prefix || prefix === '/') {
165
- return true;
170
+ return pathname;
166
171
  }
167
172
 
168
173
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
174
+ let candidate = pathname;
175
+
176
+ for (let attempt = 0; attempt < 4; attempt += 1) {
177
+ if (candidate === normalized || candidate.startsWith(`${normalized}/`)) {
178
+ return candidate;
179
+ }
180
+ if (candidate.startsWith(`${normalized}\\`)) {
181
+ return `${normalized}/__invalid_encoded_path__`;
182
+ }
183
+
184
+ let decoded;
185
+ try {
186
+ decoded = decodeURIComponent(candidate);
187
+ } catch {
188
+ return candidate.toLowerCase().startsWith(normalized.toLowerCase())
189
+ ? `${normalized}/__invalid_encoded_path__`
190
+ : null;
191
+ }
192
+ if (decoded === candidate) {
193
+ return null;
194
+ }
195
+ candidate = decoded;
196
+ }
169
197
 
170
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
198
+ return null;
199
+ }
200
+
201
+ function matchesPrefix(pathname, prefix) {
202
+ return resolvePrefixPathname(pathname, prefix) !== null;
171
203
  }
172
204
 
173
205
  function createRequestForMountedPrefix(request, prefix) {
@@ -177,12 +209,13 @@ function createRequestForMountedPrefix(request, prefix) {
177
209
 
178
210
  const url = new URL(request.url);
179
211
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
212
+ const matchedPathname = resolvePrefixPathname(url.pathname, normalized);
180
213
 
181
- if (!matchesPrefix(url.pathname, normalized)) {
214
+ if (matchedPathname === null) {
182
215
  return request;
183
216
  }
184
217
 
185
- const nextPath = url.pathname.slice(normalized.length) || '/';
218
+ const nextPath = matchedPathname.slice(normalized.length) || '/';
186
219
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
187
220
 
188
221
  return new Request(url, request);
@@ -309,12 +342,21 @@ function getEffectBffDispatcher(bff, runtime) {
309
342
  async function dispatchBffRequest(request, env) {
310
343
  const bff = MODERN_WORKER_MANIFEST.bff;
311
344
 
312
- if (
313
- !bff?.worker ||
314
- !matchesPrefix(new URL(request.url).pathname, bff.prefix)
315
- ) {
345
+ const requestUrl = new URL(request.url);
346
+ const matchedPathname = resolvePrefixPathname(
347
+ requestUrl.pathname,
348
+ bff?.prefix,
349
+ );
350
+ if (!bff?.worker || matchedPathname === null) {
316
351
  return null;
317
352
  }
353
+ const canonicalRequest =
354
+ matchedPathname === requestUrl.pathname
355
+ ? request
356
+ : new Request(
357
+ Object.assign(requestUrl, { pathname: matchedPathname }),
358
+ request,
359
+ );
318
360
  if (bff.runtimeFramework !== 'effect') {
319
361
  return createEffectBffDispatcherErrorResponse(
320
362
  bff,
@@ -347,7 +389,7 @@ async function dispatchBffRequest(request, env) {
347
389
  return createEffectBffDispatcherErrorResponse(bff, error);
348
390
  }
349
391
 
350
- return effectDispatcher.dispatch(request, { env });
392
+ return effectDispatcher.dispatch(canonicalRequest, { env });
351
393
  }
352
394
 
353
395
  const directHandler =