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

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 (42) hide show
  1. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  2. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  3. package/dist/cjs/cloudflare-builder.js +235 -14
  4. package/dist/cjs/cloudflare-output-contract.js +31 -2
  5. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  6. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  7. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  8. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  9. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  10. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  11. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  12. package/dist/esm/cloudflare-builder.mjs +229 -17
  13. package/dist/esm/cloudflare-output-contract.mjs +24 -1
  14. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  15. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  16. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  17. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  18. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  19. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  20. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  21. package/dist/esm-node/cloudflare-builder.mjs +229 -17
  22. package/dist/esm-node/cloudflare-output-contract.mjs +24 -1
  23. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  24. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  25. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  26. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  27. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  28. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  29. package/dist/types/cloudflare-builder.d.ts +44 -1
  30. package/dist/types/cloudflare-output-contract.d.ts +3 -1
  31. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  32. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  33. package/package.json +17 -13
  34. package/src/cloudflare/worker-manifest.ts +19 -3
  35. package/src/cloudflare/wrangler-config.ts +86 -10
  36. package/src/cloudflare-builder.ts +447 -12
  37. package/src/cloudflare-output-contract.ts +34 -2
  38. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  39. package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  40. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  41. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  42. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
@@ -34,6 +34,12 @@ export declare const createWranglerConfig: (appDirectory: string, modernConfig:
34
34
  binding: string;
35
35
  run_worker_first: boolean;
36
36
  };
37
+ env?: {
38
+ [k: string]: {
39
+ compatibility_date?: string | undefined;
40
+ compatibility_flags?: JsonValue[] | undefined;
41
+ };
42
+ } | undefined;
37
43
  d1_databases?: {
38
44
  binding: string;
39
45
  database_name: string;
@@ -1,5 +1,5 @@
1
1
  import type { BffUserConfig } from '@modern-js/server-core';
2
- import type { EnvironmentConfig, ModifyBundlerChainFn } from '@rsbuild/core';
2
+ import type { EnvironmentConfig, ModifyBundlerChainFn, Rspack } from '@rsbuild/core';
3
3
  export interface CloudflareBuilderNormalizedConfig {
4
4
  bff?: BffUserConfig;
5
5
  deploy?: {
@@ -10,6 +10,10 @@ export interface CloudflareBuilderAppContext {
10
10
  apiOnly?: boolean;
11
11
  apiDirectory: string;
12
12
  appDirectory: string;
13
+ entrypoints?: ReadonlyArray<{
14
+ entryName: string;
15
+ }>;
16
+ internalDirectory?: string;
13
17
  }
14
18
  export interface CloudflareBuilderEnvironmentsOptions {
15
19
  appContext: CloudflareBuilderAppContext;
@@ -52,9 +56,48 @@ export interface CloudflareBuilderPlugin {
52
56
  name: string;
53
57
  setup(api: CloudflareBuilderPluginApi): void;
54
58
  }
59
+ type AliasField = string | readonly string[];
60
+ /**
61
+ * Leaves an absent optional dependency missing at runtime, exactly as Node
62
+ * does, instead of failing the worker build. It applies only when the
63
+ * importing package itself declares the request as an optional peer
64
+ * (`peerDependenciesMeta.<name>.optional`) or an `optionalDependencies`
65
+ * entry, the package is not found in any `resolve.modules` directory, and
66
+ * neither the app (`resolve.alias`, `resolve.fallback`, `externals`) nor the
67
+ * importing package (an enabled `resolve.aliasFields` entry such as
68
+ * `browser`) redirects the request. The import
69
+ * then rejects with "Cannot find module" so the library's own fallback
70
+ * handles it (for example `@redis/client` guards `import('@node-rs/xxhash')`).
71
+ */
72
+ export declare const createAbsentOptionalDependencyFilter: (isRedirected?: (request: string) => boolean, moduleDirectories?: readonly string[], aliasFields?: readonly AliasField[]) => (request: string, context: string) => boolean;
73
+ type AliasOption = Record<string, unknown> | {
74
+ name: string;
75
+ onlyModule?: boolean;
76
+ }[] | false | undefined;
77
+ /**
78
+ * Reads the final resolved `resolve.alias`, `resolve.fallback` and
79
+ * `externals` so an app's own
80
+ * replacement for an optional package still wins over the absent-module
81
+ * fallback (the fallback runs before resolution).
82
+ */
83
+ export declare const createRequestRedirectMatcher: (options: {
84
+ externals?: unknown;
85
+ resolve?: {
86
+ alias?: AliasOption;
87
+ fallback?: AliasOption;
88
+ };
89
+ }) => (request: string) => boolean;
90
+ type FunctionExternal = (data: Rspack.ExternalItemFunctionData, callback: (error?: Error | null, result?: unknown) => void) => unknown;
91
+ /**
92
+ * Reports whether any function external claims the request. A function that
93
+ * throws is treated as claiming it, so the request goes through ordinary
94
+ * external processing and surfaces that error instead of being ignored.
95
+ */
96
+ export declare const claimsByFunctionExternal: (externals: readonly FunctionExternal[], data: Rspack.ExternalItemFunctionData) => Promise<boolean>;
55
97
  export declare function getCloudflareWorkerRspackConfig(workerEntryNames: Iterable<string>): CloudflareWorkerRspackConfig;
56
98
  export declare function applyCloudflareWorkerRspackConfig(chain: Parameters<ModifyBundlerChainFn>[0], workerEntryNames: Iterable<string>): void;
57
99
  export declare function applyCloudflareWorkerMfRuntimeBoundary(chain: Parameters<ModifyBundlerChainFn>[0]): void;
58
100
  export declare function getCloudflareBuilderEnvironments({ appContext, environments, normalizedConfig, resolveDeployProvider, }: CloudflareBuilderEnvironmentsOptions): Record<string, EnvironmentConfig>;
59
101
  export declare function applyCloudflareBuilderEnvironments(options: CloudflareBuilderEnvironmentsOptions): CloudflareBuilderEnvironmentsResult;
60
102
  export declare const createCloudflareBuilderPlugin: (resolveDeployProvider?: ResolveCloudflareDeployProvider) => CloudflareBuilderPlugin;
103
+ export {};
@@ -10,6 +10,8 @@ export declare const CLOUDFLARE_RUNTIME_TYPE = "cloudflare-module-worker";
10
10
  export declare const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = "module";
11
11
  export declare const CLOUDFLARE_WORKER_PACKAGE_TYPE = "commonjs";
12
12
  export declare const CLOUDFLARE_WORKER_BUNDLE_FORMAT = "commonjs";
13
- export declare const CLOUDFLARE_WORKER_NODE_BUILTINS: readonly ['assert', 'assert/strict', 'async_hooks', 'buffer', 'child_process', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'process', 'readline', 'readline/promises', 'repl', 'sqlite', 'stream', 'stream/consumers', 'stream/promises', 'stream/web', 'string_decoder', 'timers', 'timers/promises', 'tls', 'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib'];
13
+ export declare const CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT = "handleRouteDataRequest";
14
+ export declare const getCloudflareWorkerRouteDataEntryName: (entryName: string) => string;
15
+ export declare const CLOUDFLARE_WORKER_NODE_BUILTINS: readonly ['_http_agent', '_http_client', '_http_common', '_http_incoming', '_http_outgoing', '_http_server', '_tls_common', '_tls_wrap', 'assert', 'assert/strict', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'dns/promises', 'domain', 'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector', 'inspector/promises', 'module', 'net', 'os', 'path', 'path/posix', 'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'readline/promises', 'repl', 'sqlite', 'stream', 'stream/consumers', 'stream/promises', 'stream/web', 'string_decoder', 'sys', 'test', 'timers', 'timers/promises', 'tls', 'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib'];
14
16
  export declare const CLOUDFLARE_WORKER_PLATFORM_MODULES: readonly ['cloudflare:sockets'];
15
17
  export declare const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS: readonly ['nodejs_compat', 'global_fetch_strictly_public'];
@@ -1,7 +1,7 @@
1
1
  import type { CloudflareOutputVerifierIssue, JsonObject } from './issues.js';
2
2
  interface WorkerBundleReference {
3
3
  dispatcherExport?: string;
4
- kind: 'effect-bff' | 'route';
4
+ kind: 'effect-bff' | 'route' | 'route-data';
5
5
  reference: string;
6
6
  }
7
7
  interface ResolvedWorkerBundleReference extends WorkerBundleReference {
@@ -10,7 +10,7 @@ interface ResolvedWorkerBundleReference extends WorkerBundleReference {
10
10
  export declare const getEffectBffWorker: (manifest: JsonObject) => any;
11
11
  export declare const getWorkerBundleReferences: (manifest: JsonObject) => WorkerBundleReference[];
12
12
  export declare const resolveWorkerBundleReference: (issues: CloudflareOutputVerifierIssue[], outputDirectory: string, reference: WorkerBundleReference, manifestPath: string) => ResolvedWorkerBundleReference | null;
13
- export declare const missingWorkerBundleMessage: (reference: WorkerBundleReference) => "Cloudflare Effect BFF manifest points to a missing worker bundle." | "Cloudflare route worker manifest points to a missing worker bundle.";
13
+ export declare const missingWorkerBundleMessage: (reference: WorkerBundleReference) => "Cloudflare Effect BFF manifest points to a missing worker bundle." | "Cloudflare route data worker manifest points to a missing worker bundle." | "Cloudflare route worker manifest points to a missing worker bundle.";
14
14
  interface WorkerBundleVerificationContract {
15
15
  providedPackages: ReadonlySet<string>;
16
16
  workerRoot: string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Create the worker route data request handler for an entry's route loaders
3
+ * (the `routes` export of its generated `route-server-loaders.js`).
4
+ *
5
+ * The handler takes the Node data handler's options and resolves to
6
+ * `undefined` for requests that are not route data requests of a known entry,
7
+ * so the caller renders the page instead.
8
+ */
9
+ export declare function createRouteDataRequestHandler(routes: any): ({ request, serverRoutes, context, onTiming }: {
10
+ context?: {} | undefined;
11
+ onTiming: any;
12
+ request: any;
13
+ serverRoutes: any;
14
+ }) => Promise<any>;
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "directory": "packages/solutions/app-tools-extensions"
12
12
  },
13
13
  "license": "MIT",
14
- "version": "3.9.0-ultramodern.13",
14
+ "version": "3.9.0-ultramodern.14",
15
15
  "files": [
16
16
  "dist",
17
17
  "src"
@@ -145,15 +145,16 @@
145
145
  "@babel/parser": "^8.0.6",
146
146
  "@babel/traverse": "^8.0.6",
147
147
  "@loadable/component": "5.16.7",
148
- "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.13",
149
- "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.13",
150
- "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.13",
151
- "@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.13",
152
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.13",
153
- "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.13",
154
- "@modern-js/surface-resolution": "npm:@bleedingdev/modern-js-surface-resolution@3.9.0-ultramodern.13",
155
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.13",
156
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.13",
148
+ "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.14",
149
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.14",
150
+ "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.14",
151
+ "@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.14",
152
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.14",
153
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.14",
154
+ "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.14",
155
+ "@modern-js/surface-resolution": "npm:@bleedingdev/modern-js-surface-resolution@3.9.0-ultramodern.14",
156
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.14",
157
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.14",
157
158
  "@swc/helpers": "^0.5.23",
158
159
  "compression-webpack-plugin": "^12.0.0",
159
160
  "esbuild": "^0.28.2",
@@ -163,8 +164,9 @@
163
164
  "@rsbuild/core": "2.2.9"
164
165
  },
165
166
  "devDependencies": {
166
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.13",
167
- "@modern-js/plugin-bff-extensions": "npm:@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.13",
167
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.14",
168
+ "@modern-js/plugin-bff-extensions": "npm:@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.14",
169
+ "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.14",
168
170
  "@module-federation/runtime-tools": "npm:@bleedingdev/mf-runtime-tools@2.9.1",
169
171
  "@rsbuild/core": "2.2.9",
170
172
  "@rslib/core": "1.0.2",
@@ -174,7 +176,9 @@
174
176
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
175
177
  "core-js": "^3.50.0",
176
178
  "effect": "npm:@bleedingdev/effect@4.0.0-rc.117",
177
- "ndepe": "^0.1.13"
179
+ "miniflare": "5.20260921.0-alpha",
180
+ "ndepe": "^0.1.13",
181
+ "pg": "8.23.0"
178
182
  },
179
183
  "sideEffects": false,
180
184
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import type {} from '@modern-js/server-runtime-extensions/server-config';
3
3
  import { fs as fse } from '@modern-js/utils';
4
+ import { getCloudflareWorkerRouteDataEntryName } from '../cloudflare-output-contract';
4
5
  import { readRouteSpec } from './artifacts';
5
6
  import {
6
7
  ASSETS_BINDING,
@@ -132,6 +133,15 @@ export const createWorkerManifest = async (
132
133
  routeSpec.routes.map(async (route: Record<string, any>) => {
133
134
  const worker =
134
135
  typeof route.worker === 'string' ? route.worker : undefined;
136
+ const routeDataWorker =
137
+ route.isSSR && typeof route.entryName === 'string'
138
+ ? `${WORKER_BUNDLE_DIRECTORY}/${getCloudflareWorkerRouteDataEntryName(
139
+ route.entryName,
140
+ )}.js`
141
+ : undefined;
142
+ const hasRouteDataWorker = routeDataWorker
143
+ ? await fse.pathExists(path.join(outputDirectory, routeDataWorker))
144
+ : false;
135
145
 
136
146
  return {
137
147
  urlPath: route.urlPath,
@@ -142,6 +152,7 @@ export const createWorkerManifest = async (
142
152
  workerExists: worker
143
153
  ? await fse.pathExists(path.join(outputDirectory, worker))
144
154
  : false,
155
+ ...(hasRouteDataWorker ? { routeDataWorker } : {}),
145
156
  };
146
157
  }),
147
158
  );
@@ -218,9 +229,14 @@ export const createWorkerModuleLoaders = (manifest: any) => {
218
229
  const imports = new Map<string, string>();
219
230
 
220
231
  for (const route of manifest.routeSpec.routes) {
221
- if (route.worker && route.workerExists) {
222
- const importPath = `../${String(route.worker).replace(/^\/+/u, '')}`;
223
- imports.set(route.worker, `() => import(${JSON.stringify(importPath)})`);
232
+ for (const worker of [
233
+ route.workerExists ? route.worker : undefined,
234
+ route.routeDataWorker,
235
+ ]) {
236
+ if (worker) {
237
+ const importPath = `../${String(worker).replace(/^\/+/u, '')}`;
238
+ imports.set(worker, `() => import(${JSON.stringify(importPath)})`);
239
+ }
224
240
  }
225
241
  }
226
242
 
@@ -15,21 +15,51 @@ import {
15
15
  import type { CloudflareModernConfig } from './types';
16
16
  import { isJsonRecord, normalizeRelativePath } from './utils';
17
17
 
18
- const getCompatibilityDate = (modernConfig: CloudflareModernConfig) => {
19
- const configuredDate = modernConfig.deploy?.worker?.compatibilityDate?.trim();
20
- const compatibilityDate = configuredDate || DEFAULT_COMPATIBILITY_DATE;
21
-
18
+ // Workers only add Node.js built-ins at later dates, and the bundler
19
+ // externalizes the set probed at DEFAULT_COMPATIBILITY_DATE; an earlier date
20
+ // could leave an externalized built-in unresolvable at deploy time.
21
+ const assertSupportedCompatibilityDate = (
22
+ compatibilityDate: JsonValue | undefined,
23
+ label: string,
24
+ ) => {
25
+ if (typeof compatibilityDate !== 'string') {
26
+ throw new Error(`${label} must be a YYYY-MM-DD string.`);
27
+ }
22
28
  if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) {
23
29
  throw new Error(
24
- `deploy.worker.compatibilityDate must use YYYY-MM-DD, received ${JSON.stringify(
30
+ `${label} must use YYYY-MM-DD, received ${JSON.stringify(
31
+ compatibilityDate,
32
+ )}.`,
33
+ );
34
+ }
35
+ if (compatibilityDate < DEFAULT_COMPATIBILITY_DATE) {
36
+ throw new Error(
37
+ `${label} must be ${DEFAULT_COMPATIBILITY_DATE} or later, the date the Cloudflare worker Node.js built-in contract is verified against; received ${JSON.stringify(
25
38
  compatibilityDate,
26
39
  )}.`,
27
40
  );
28
41
  }
29
-
30
42
  return compatibilityDate;
31
43
  };
32
44
 
45
+ const getCompatibilityDate = (
46
+ modernConfig: CloudflareModernConfig,
47
+ wranglerCompatibilityDate: JsonValue | undefined,
48
+ ) => {
49
+ if (wranglerCompatibilityDate !== undefined) {
50
+ // A raw Wrangler override is the effective date, so it is validated too.
51
+ return assertSupportedCompatibilityDate(
52
+ wranglerCompatibilityDate,
53
+ 'deploy.worker.wrangler.compatibility_date',
54
+ );
55
+ }
56
+ return assertSupportedCompatibilityDate(
57
+ modernConfig.deploy?.worker?.compatibilityDate?.trim() ||
58
+ DEFAULT_COMPATIBILITY_DATE,
59
+ 'deploy.worker.compatibilityDate',
60
+ );
61
+ };
62
+
33
63
  const getWorkerName = (appDirectory: string) => {
34
64
  const basename = path.basename(appDirectory);
35
65
  return basename.replace(/[^a-zA-Z0-9-_]/g, '-') || 'modern-cloudflare-worker';
@@ -59,6 +89,7 @@ const getConfiguredWrangler = (modernConfig: CloudflareModernConfig) => {
59
89
 
60
90
  const createWranglerCompatibilityFlags = (
61
91
  configuredFlags: JsonValue | undefined,
92
+ label = 'deploy.worker.wrangler.compatibility_flags',
62
93
  ) => {
63
94
  if (configuredFlags === undefined) {
64
95
  return [...REQUIRED_COMPATIBILITY_FLAGS];
@@ -68,14 +99,54 @@ const createWranglerCompatibilityFlags = (
68
99
  !Array.isArray(configuredFlags) ||
69
100
  configuredFlags.some(flag => typeof flag !== 'string')
70
101
  ) {
71
- throw new Error(
72
- 'deploy.worker.wrangler.compatibility_flags must be an array of strings.',
73
- );
102
+ throw new Error(`${label} must be an array of strings.`);
74
103
  }
75
104
 
76
105
  return [...new Set([...configuredFlags, ...REQUIRED_COMPATIBILITY_FLAGS])];
77
106
  };
78
107
 
108
+ // `wrangler deploy --env <name>` uses an environment's own compatibility_date
109
+ // and compatibility_flags instead of the top-level values, so each override
110
+ // is held to the same verified date floor and required flags.
111
+ const createWranglerEnvironments = (wranglerEnv: JsonValue | undefined) => {
112
+ if (wranglerEnv === undefined) {
113
+ return undefined;
114
+ }
115
+ if (!isJsonRecord(wranglerEnv)) {
116
+ throw new Error('deploy.worker.wrangler.env must be an object.');
117
+ }
118
+ return Object.fromEntries(
119
+ Object.entries(wranglerEnv).map(([name, environment]) => {
120
+ const label = `deploy.worker.wrangler.env.${name}`;
121
+ if (!isJsonRecord(environment)) {
122
+ throw new Error(`${label} must be an object.`);
123
+ }
124
+ return [
125
+ name,
126
+ {
127
+ ...environment,
128
+ ...(environment.compatibility_date === undefined
129
+ ? {}
130
+ : {
131
+ compatibility_date: assertSupportedCompatibilityDate(
132
+ environment.compatibility_date,
133
+ `${label}.compatibility_date`,
134
+ ),
135
+ }),
136
+ ...(environment.compatibility_flags === undefined
137
+ ? {}
138
+ : {
139
+ compatibility_flags: createWranglerCompatibilityFlags(
140
+ environment.compatibility_flags,
141
+ `${label}.compatibility_flags`,
142
+ ),
143
+ }),
144
+ },
145
+ ];
146
+ }),
147
+ );
148
+ };
149
+
79
150
  const createWranglerAssetsConfig = (
80
151
  configuredAssets: JsonValue | undefined,
81
152
  ) => {
@@ -312,6 +383,7 @@ export const createWranglerConfig = (
312
383
  modernConfig: CloudflareModernConfig,
313
384
  ) => {
314
385
  const wrangler = getConfiguredWrangler(modernConfig);
386
+ const environments = createWranglerEnvironments(wrangler.env);
315
387
  const d1Databases = createWranglerD1Databases(
316
388
  modernConfig,
317
389
  wrangler.d1_databases,
@@ -325,13 +397,17 @@ export const createWranglerConfig = (
325
397
  return {
326
398
  $schema: 'node_modules/wrangler/config-schema.json',
327
399
  name: getConfiguredWorkerName(appDirectory, modernConfig),
328
- compatibility_date: getCompatibilityDate(modernConfig),
329
400
  ...wrangler,
401
+ compatibility_date: getCompatibilityDate(
402
+ modernConfig,
403
+ wrangler.compatibility_date,
404
+ ),
330
405
  main: WORKER_ENTRY,
331
406
  compatibility_flags: createWranglerCompatibilityFlags(
332
407
  wrangler.compatibility_flags,
333
408
  ),
334
409
  assets: createWranglerAssetsConfig(wrangler.assets),
410
+ ...(environments === undefined ? {} : { env: environments }),
335
411
  ...(d1Databases === undefined ? {} : { d1_databases: d1Databases }),
336
412
  ...(wranglerServices === undefined ? {} : { services: wranglerServices }),
337
413
  };