wrangler 0.0.0-e95bea2 → 0.0.0-ea8e701

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 (55) hide show
  1. package/README.md +3 -3
  2. package/bin/wrangler.js +10 -1
  3. package/miniflare-dist/index.mjs +79 -0
  4. package/miniflare-dist/index.mjs.map +7 -0
  5. package/package.json +7 -6
  6. package/pages/functions/filepath-routing.test.ts +125 -25
  7. package/pages/functions/filepath-routing.ts +6 -3
  8. package/src/__tests__/configuration.test.ts +826 -223
  9. package/src/__tests__/dev.test.tsx +145 -9
  10. package/src/__tests__/guess-worker-format.test.ts +26 -1
  11. package/src/__tests__/helpers/mock-account-id.ts +4 -0
  12. package/src/__tests__/helpers/mock-kv.ts +9 -16
  13. package/src/__tests__/helpers/mock-oauth-flow.ts +178 -0
  14. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  15. package/src/__tests__/index.test.ts +9 -13
  16. package/src/__tests__/jest.setup.ts +5 -0
  17. package/src/__tests__/kv.test.ts +43 -71
  18. package/src/__tests__/pages.test.ts +48 -10
  19. package/src/__tests__/publish.test.ts +1456 -327
  20. package/src/__tests__/r2.test.ts +4 -4
  21. package/src/__tests__/tail.test.ts +78 -4
  22. package/src/__tests__/user.test.ts +83 -0
  23. package/src/__tests__/whoami.test.tsx +16 -7
  24. package/src/bundle.ts +15 -2
  25. package/src/cfetch/internal.ts +9 -5
  26. package/src/config/README.md +6 -6
  27. package/src/config/config.ts +17 -78
  28. package/src/config/environment.ts +67 -1
  29. package/src/config/index.ts +13 -3
  30. package/src/config/validation-helpers.ts +177 -11
  31. package/src/config/validation.ts +305 -100
  32. package/src/dev/dev.tsx +76 -41
  33. package/src/dev/local.tsx +105 -114
  34. package/src/dev/remote.tsx +2 -0
  35. package/src/dev/use-esbuild.ts +4 -0
  36. package/src/entry.ts +103 -6
  37. package/src/index.tsx +251 -189
  38. package/src/intl-polyfill.d.ts +139 -0
  39. package/src/{kv/index.tsx → kv.ts} +8 -28
  40. package/src/miniflare-cli/README.md +30 -0
  41. package/src/miniflare-cli/enum-keys.ts +17 -0
  42. package/src/miniflare-cli/index.ts +37 -0
  43. package/src/module-collection.ts +58 -53
  44. package/src/pages.tsx +26 -10
  45. package/src/proxy.ts +5 -3
  46. package/src/publish.ts +17 -21
  47. package/src/sites.tsx +67 -25
  48. package/src/tail/index.ts +1 -1
  49. package/src/tail/printing.ts +2 -2
  50. package/src/user.tsx +161 -81
  51. package/wrangler-dist/cli.js +3418 -1437
  52. package/wrangler-dist/cli.js.map +3 -3
  53. package/src/__tests__/dev2.test.tsx +0 -152
  54. package/src/__tests__/helpers/mock-user.ts +0 -27
  55. package/src/__tests__/logout.test.ts +0 -50
package/src/dev/dev.tsx CHANGED
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { watch } from "chokidar";
3
3
  import clipboardy from "clipboardy";
4
4
  import commandExists from "command-exists";
5
- import { Box, Text, useApp, useInput } from "ink";
5
+ import { Box, Text, useApp, useInput, useStdin } from "ink";
6
6
  import React, { useState, useEffect, useRef } from "react";
7
7
  import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
8
8
  import onExit from "signal-exit";
@@ -19,17 +19,20 @@ import type { Config } from "../config";
19
19
  import type { Entry } from "../entry";
20
20
  import type { AssetPaths } from "../sites";
21
21
  import type { CfWorkerInit } from "../worker";
22
+ import type { EsbuildBundle } from "./use-esbuild";
22
23
 
23
24
  export type DevProps = {
24
25
  name?: string;
25
26
  entry: Entry;
26
- port?: number;
27
+ port: number;
28
+ ip: string;
27
29
  inspectorPort: number;
28
30
  rules: Config["rules"];
29
31
  accountId: undefined | string;
30
32
  initialMode: "local" | "remote";
31
33
  jsxFactory: undefined | string;
32
34
  jsxFragment: undefined | string;
35
+ tsconfig: string | undefined;
33
36
  upstreamProtocol: "https" | "http";
34
37
  localProtocol: "https" | "http";
35
38
  enableLocalPersistence: boolean;
@@ -39,7 +42,7 @@ export type DevProps = {
39
42
  compatibilityDate: undefined | string;
40
43
  compatibilityFlags: undefined | string[];
41
44
  usageModel: undefined | "bundled" | "unbound";
42
- buildCommand: {
45
+ build: {
43
46
  command?: undefined | string;
44
47
  cwd?: undefined | string;
45
48
  watch_dir?: undefined | string;
@@ -55,11 +58,10 @@ export type DevProps = {
55
58
  };
56
59
 
57
60
  export function DevImplementation(props: DevProps): JSX.Element {
58
- const port = props.port ?? 8787;
59
61
  const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
60
62
  const directory = useTmpDir();
61
63
 
62
- useCustomBuild(props.entry, props.buildCommand);
64
+ useCustomBuild(props.entry, props.build);
63
65
 
64
66
  if (props.public && props.entry.format === "service-worker") {
65
67
  throw new Error(
@@ -87,14 +89,36 @@ export function DevImplementation(props: DevProps): JSX.Element {
87
89
  rules: props.rules,
88
90
  jsxFragment: props.jsxFragment,
89
91
  serveAssetsFromWorker: !!props.public,
92
+ tsconfig: props.tsconfig,
90
93
  });
91
94
 
95
+ // only load the UI if we're running in a supported environment
96
+ const { isRawModeSupported } = useStdin();
97
+ return isRawModeSupported ? (
98
+ <InteractiveDevSession {...props} bundle={bundle} apiToken={apiToken} />
99
+ ) : (
100
+ <DevSession
101
+ {...props}
102
+ bundle={bundle}
103
+ apiToken={apiToken}
104
+ local={props.initialMode === "local"}
105
+ />
106
+ );
107
+ }
108
+
109
+ type InteractiveDevSessionProps = DevProps & {
110
+ apiToken: string | undefined;
111
+ bundle: EsbuildBundle | undefined;
112
+ };
113
+
114
+ function InteractiveDevSession(props: InteractiveDevSessionProps) {
92
115
  const toggles = useHotkeys(
93
116
  {
94
117
  local: props.initialMode === "local",
95
118
  tunnel: false,
96
119
  },
97
- port,
120
+ props.port,
121
+ props.ip,
98
122
  props.inspectorPort,
99
123
  props.localProtocol
100
124
  );
@@ -103,40 +127,7 @@ export function DevImplementation(props: DevProps): JSX.Element {
103
127
 
104
128
  return (
105
129
  <>
106
- {toggles.local ? (
107
- <Local
108
- name={props.name}
109
- bundle={bundle}
110
- format={props.entry.format}
111
- bindings={props.bindings}
112
- assetPaths={props.assetPaths}
113
- public={props.public}
114
- port={port}
115
- rules={props.rules}
116
- inspectorPort={props.inspectorPort}
117
- enableLocalPersistence={props.enableLocalPersistence}
118
- />
119
- ) : (
120
- <Remote
121
- name={props.name}
122
- bundle={bundle}
123
- format={props.entry.format}
124
- accountId={props.accountId}
125
- apiToken={apiToken}
126
- bindings={props.bindings}
127
- assetPaths={props.assetPaths}
128
- public={props.public}
129
- port={port}
130
- localProtocol={props.localProtocol}
131
- inspectorPort={props.inspectorPort}
132
- compatibilityDate={props.compatibilityDate}
133
- compatibilityFlags={props.compatibilityFlags}
134
- usageModel={props.usageModel}
135
- env={props.env}
136
- legacyEnv={props.legacyEnv}
137
- zone={props.zone}
138
- />
139
- )}
130
+ <DevSession {...props} local={toggles.local} />
140
131
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
141
132
  <Text>
142
133
  {`B to open a browser, D to open Devtools, L to ${
@@ -148,6 +139,49 @@ export function DevImplementation(props: DevProps): JSX.Element {
148
139
  );
149
140
  }
150
141
 
142
+ type DevSessionProps = InteractiveDevSessionProps & { local: boolean };
143
+
144
+ function DevSession(props: DevSessionProps) {
145
+ return props.local ? (
146
+ <Local
147
+ name={props.name}
148
+ bundle={props.bundle}
149
+ format={props.entry.format}
150
+ compatibilityDate={props.compatibilityDate}
151
+ compatibilityFlags={props.compatibilityFlags}
152
+ bindings={props.bindings}
153
+ assetPaths={props.assetPaths}
154
+ public={props.public}
155
+ port={props.port}
156
+ ip={props.ip}
157
+ rules={props.rules}
158
+ inspectorPort={props.inspectorPort}
159
+ enableLocalPersistence={props.enableLocalPersistence}
160
+ />
161
+ ) : (
162
+ <Remote
163
+ name={props.name}
164
+ bundle={props.bundle}
165
+ format={props.entry.format}
166
+ accountId={props.accountId}
167
+ apiToken={props.apiToken}
168
+ bindings={props.bindings}
169
+ assetPaths={props.assetPaths}
170
+ public={props.public}
171
+ port={props.port}
172
+ ip={props.ip}
173
+ localProtocol={props.localProtocol}
174
+ inspectorPort={props.inspectorPort}
175
+ compatibilityDate={props.compatibilityDate}
176
+ compatibilityFlags={props.compatibilityFlags}
177
+ usageModel={props.usageModel}
178
+ env={props.env}
179
+ legacyEnv={props.legacyEnv}
180
+ zone={props.zone}
181
+ />
182
+ );
183
+ }
184
+
151
185
  export interface DirectorySyncResult {
152
186
  name: string;
153
187
  removeCallback: () => void;
@@ -295,6 +329,7 @@ type useHotkeysInitialState = {
295
329
  function useHotkeys(
296
330
  initial: useHotkeysInitialState,
297
331
  port: number,
332
+ ip: string,
298
333
  inspectorPort: number,
299
334
  localProtocol: "http" | "https"
300
335
  ) {
@@ -309,7 +344,7 @@ function useHotkeys(
309
344
  switch (input.toLowerCase()) {
310
345
  // open browser
311
346
  case "b": {
312
- await openInBrowser(`${localProtocol}://localhost:${port}`);
347
+ await openInBrowser(`${localProtocol}://${ip}:${port}`);
313
348
  break;
314
349
  }
315
350
  // toggle inspector
package/src/dev/local.tsx CHANGED
@@ -11,30 +11,26 @@ import type { Config } from "../config";
11
11
  import type { AssetPaths } from "../sites";
12
12
  import type { CfWorkerInit, CfScriptFormat } from "../worker";
13
13
  import type { EsbuildBundle } from "./use-esbuild";
14
+ import type { MiniflareOptions } from "miniflare";
14
15
 
15
- export function Local(props: {
16
+ interface LocalProps {
16
17
  name: undefined | string;
17
18
  bundle: EsbuildBundle | undefined;
18
19
  format: CfScriptFormat | undefined;
20
+ compatibilityDate: string | undefined;
21
+ compatibilityFlags: undefined | string[];
19
22
  bindings: CfWorkerInit["bindings"];
20
23
  assetPaths: undefined | AssetPaths;
21
24
  public: undefined | string;
22
25
  port: number;
26
+ ip: string;
23
27
  rules: Config["rules"];
24
28
  inspectorPort: number;
25
29
  enableLocalPersistence: boolean;
26
- }) {
27
- const { inspectorUrl } = useLocalWorker({
28
- name: props.name,
29
- bundle: props.bundle,
30
- format: props.format,
31
- bindings: props.bindings,
32
- assetPaths: props.assetPaths,
33
- public: props.public,
34
- port: props.port,
35
- rules: props.rules,
36
- enableLocalPersistence: props.enableLocalPersistence,
37
- });
30
+ }
31
+
32
+ export function Local(props: LocalProps) {
33
+ const { inspectorUrl } = useLocalWorker(props);
38
34
  useInspector({
39
35
  inspectorUrl,
40
36
  port: props.inspectorPort,
@@ -43,19 +39,21 @@ export function Local(props: {
43
39
  return null;
44
40
  }
45
41
 
46
- function useLocalWorker(props: {
47
- name: undefined | string;
48
- bundle: EsbuildBundle | undefined;
49
- rules: Config["rules"];
50
- format: CfScriptFormat | undefined;
51
- bindings: CfWorkerInit["bindings"];
52
- assetPaths: undefined | AssetPaths;
53
- public: undefined | string;
54
- port: number;
55
- enableLocalPersistence: boolean;
56
- }) {
42
+ function useLocalWorker({
43
+ name: workerName,
44
+ bundle,
45
+ format,
46
+ compatibilityDate,
47
+ compatibilityFlags,
48
+ bindings,
49
+ assetPaths,
50
+ public: publicDirectory,
51
+ port,
52
+ rules,
53
+ enableLocalPersistence,
54
+ ip,
55
+ }: LocalProps) {
57
56
  // TODO: pass vars via command line
58
- const { bundle, format, bindings, port, assetPaths } = props;
59
57
  const local = useRef<ReturnType<typeof spawn>>();
60
58
  const removeSignalExitListener = useRef<() => void>();
61
59
  const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
@@ -66,7 +64,7 @@ function useLocalWorker(props: {
66
64
  // port for the worker
67
65
  await waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 });
68
66
 
69
- if (props.public) {
67
+ if (publicDirectory) {
70
68
  throw new Error(
71
69
  '⎔ A "public" folder is not yet supported in local mode.'
72
70
  );
@@ -81,100 +79,89 @@ function useLocalWorker(props: {
81
79
  );
82
80
  }
83
81
 
82
+ const scriptPath = realpathSync(bundle.path);
83
+
84
+ const wasmBindings = { ...bindings.wasm_modules };
85
+ if (format === "service-worker") {
86
+ for (const { type, name } of bundle.modules) {
87
+ if (type === "compiled-wasm") {
88
+ // In service-worker format, .wasm modules are referenced by global identifiers,
89
+ // so we convert it here.
90
+ // This identifier has to be a valid JS identifier, so we replace all non alphanumeric
91
+ // characters with an underscore.
92
+ const identifier = name.replace(/[^a-zA-Z0-9_$]/g, "_");
93
+ wasmBindings[identifier] = name;
94
+ }
95
+ }
96
+ }
97
+
98
+ const options: MiniflareOptions = {
99
+ name: workerName,
100
+ port,
101
+ scriptPath,
102
+ host: ip,
103
+ modules: format === "modules",
104
+ modulesRules: (rules || [])
105
+ .concat(DEFAULT_MODULE_RULES)
106
+ .map(({ type, globs: include, fallthrough }) => ({
107
+ type,
108
+ include,
109
+ fallthrough,
110
+ })),
111
+ compatibilityDate,
112
+ compatibilityFlags,
113
+ kvNamespaces: bindings.kv_namespaces?.map((kv) => kv.binding),
114
+ kvPersist: enableLocalPersistence,
115
+ durableObjects: Object.fromEntries(
116
+ (bindings.durable_objects?.bindings ?? []).map<[string, string]>(
117
+ (value) => [value.name, value.class_name]
118
+ )
119
+ ),
120
+ durableObjectsPersist: enableLocalPersistence,
121
+ cachePersist: enableLocalPersistence,
122
+ sitePath: assetPaths?.assetDirectory
123
+ ? path.join(assetPaths.baseDirectory, assetPaths.assetDirectory)
124
+ : undefined,
125
+ siteInclude: assetPaths?.includePatterns.length
126
+ ? assetPaths?.includePatterns
127
+ : undefined,
128
+ siteExclude: assetPaths?.excludePatterns.length
129
+ ? assetPaths.excludePatterns
130
+ : undefined,
131
+ bindings: bindings.vars,
132
+ wasmBindings,
133
+ sourceMap: true,
134
+ logUnhandledRejections: true,
135
+ };
136
+
137
+ // The path to the Miniflare CLI assumes that this file is being run from
138
+ // `wrangler-dist` and that the CLI is found in `miniflare-dist`.
139
+ // If either of those paths change this line needs updating.
140
+ const miniflareCLIPath = path.resolve(
141
+ __dirname,
142
+ "../miniflare-dist/index.mjs"
143
+ );
144
+ const optionsArg = JSON.stringify(options, null);
145
+
84
146
  console.log("⎔ Starting a local server...");
85
- // TODO: just use execa for this
86
147
  local.current = spawn(
87
148
  "node",
88
149
  [
89
- "--experimental-vm-modules",
90
- "--inspect",
91
- require.resolve("miniflare/cli"),
92
- realpathSync(bundle.path),
93
- "--watch",
94
- "--wrangler-config",
95
- path.join(__dirname, "../miniflare-config-stubs/wrangler.empty.toml"),
96
- "--env",
97
- path.join(__dirname, "../miniflare-config-stubs/.env.empty"),
98
- "--package",
99
- path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
100
- "--port",
101
- port.toString(),
102
- ...(assetPaths
103
- ? [
104
- "--site",
105
- path.join(process.cwd(), assetPaths.baseDirectory),
106
- ...assetPaths.includePatterns.map((pattern) => [
107
- "--site-include",
108
- pattern,
109
- ]),
110
- ...assetPaths.excludePatterns.map((pattern) => [
111
- "--site-exclude",
112
- pattern,
113
- ]),
114
- ].flatMap((x) => x)
115
- : []),
116
- ...(props.enableLocalPersistence
117
- ? ["--kv-persist", "--cache-persist", "--do-persist"]
118
- : []),
119
- ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
120
- return ["--binding", `${key}=${value}`];
121
- }),
122
- ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
123
- return ["--kv", binding];
124
- }),
125
- ...(bindings.durable_objects?.bindings || []).flatMap(
126
- ({ name, class_name }) => {
127
- return ["--do", `${name}=${class_name}`];
128
- }
129
- ),
130
- ...Object.entries(bindings.wasm_modules || {}).flatMap(
131
- ([name, filePath]) => {
132
- return [
133
- "--wasm",
134
- `${name}=${path.join(process.cwd(), filePath)}`,
135
- ];
136
- }
137
- ),
138
- ...bundle.modules.reduce<string[]>((cmd, { name, type }) => {
139
- if (format === "service-worker") {
140
- if (type === "compiled-wasm") {
141
- // In service-worker format, .wasm modules are referenced
142
- // by global identifiers, so we convert it here.
143
- // This identifier has to be a valid JS identifier,
144
- // so we replace all non alphanumeric characters
145
- // with an underscore.
146
- const identifier = name.replace(/[^a-zA-Z0-9_$]/g, "_");
147
- return cmd.concat([`--wasm`, `${identifier}=${name}`]);
148
- } else {
149
- // TODO: we should actually support this
150
- throw new Error(
151
- `⎔ Unsupported module type ${type} for file ${name} in service-worker format`
152
- );
153
- }
154
- }
155
- return cmd;
156
- }, []),
157
- "--modules",
158
- String(format === "modules"),
159
- ...(props.rules || [])
160
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
161
- .concat(DEFAULT_MODULE_RULES!)
162
- .flatMap((rule) =>
163
- rule.globs.flatMap((glob) => [
164
- "--modules-rule",
165
- `${rule.type}=${glob}`,
166
- ])
167
- ),
150
+ "--experimental-vm-modules", // ensures that Miniflare can run ESM Workers
151
+ "--no-warnings", // hide annoying Node warnings
152
+ "--inspect", // start Miniflare listening for a debugger to attach
153
+ miniflareCLIPath,
154
+ optionsArg,
155
+ // "--log=VERBOSE", // uncomment this to Miniflare to log "everything"!
168
156
  ],
169
157
  {
170
- cwd: path.dirname(realpathSync(bundle.path)),
158
+ cwd: path.dirname(scriptPath),
171
159
  }
172
160
  );
173
- console.log(`⬣ Listening at http://localhost:${port}`);
174
161
 
175
162
  local.current.on("close", (code) => {
176
163
  if (code) {
177
- console.log(`miniflare process exited with code ${code}`);
164
+ console.log(`Miniflare process exited with code ${code}`);
178
165
  }
179
166
  });
180
167
 
@@ -195,12 +182,12 @@ function useLocalWorker(props: {
195
182
 
196
183
  local.current.on("exit", (code) => {
197
184
  if (code) {
198
- console.error(`miniflare process exited with code ${code}`);
185
+ console.error(`Miniflare process exited with code ${code}`);
199
186
  }
200
187
  });
201
188
 
202
189
  local.current.on("error", (error: Error) => {
203
- console.error(`miniflare process failed to spawn`);
190
+ console.error(`Miniflare process failed to spawn`);
204
191
  console.error(error);
205
192
  });
206
193
 
@@ -226,15 +213,19 @@ function useLocalWorker(props: {
226
213
  };
227
214
  }, [
228
215
  bundle,
216
+ workerName,
229
217
  format,
230
218
  port,
219
+ ip,
231
220
  bindings.durable_objects?.bindings,
232
221
  bindings.kv_namespaces,
233
222
  bindings.vars,
234
- props.enableLocalPersistence,
223
+ compatibilityDate,
224
+ compatibilityFlags,
225
+ enableLocalPersistence,
235
226
  assetPaths,
236
- props.public,
237
- props.rules,
227
+ publicDirectory,
228
+ rules,
238
229
  bindings.wasm_modules,
239
230
  ]);
240
231
  return { inspectorUrl };
@@ -18,6 +18,7 @@ export function Remote(props: {
18
18
  public: undefined | string;
19
19
  assetPaths: undefined | AssetPaths;
20
20
  port: number;
21
+ ip: string;
21
22
  localProtocol: "https" | "http";
22
23
  inspectorPort: number;
23
24
  accountId: undefined | string;
@@ -55,6 +56,7 @@ export function Remote(props: {
55
56
  publicRoot: props.public,
56
57
  localProtocol: props.localProtocol,
57
58
  localPort: props.port,
59
+ ip: props.ip,
58
60
  });
59
61
 
60
62
  useInspector({
@@ -23,6 +23,7 @@ export function useEsbuild({
23
23
  jsxFragment,
24
24
  rules,
25
25
  serveAssetsFromWorker,
26
+ tsconfig,
26
27
  }: {
27
28
  entry: Entry;
28
29
  destination: string | undefined;
@@ -31,6 +32,7 @@ export function useEsbuild({
31
32
  jsxFragment: string | undefined;
32
33
  rules: Config["rules"];
33
34
  serveAssetsFromWorker: boolean;
35
+ tsconfig: string | undefined;
34
36
  }): EsbuildBundle | undefined {
35
37
  const [bundle, setBundle] = useState<EsbuildBundle>();
36
38
  useEffect(() => {
@@ -64,6 +66,7 @@ export function useEsbuild({
64
66
  jsxFragment,
65
67
  rules,
66
68
  watch: watchMode,
69
+ tsconfig,
67
70
  });
68
71
 
69
72
  // Capture the `stop()` method to use as the `useEffect()` destructor.
@@ -95,6 +98,7 @@ export function useEsbuild({
95
98
  jsxFragment,
96
99
  serveAssetsFromWorker,
97
100
  rules,
101
+ tsconfig,
98
102
  ]);
99
103
  return bundle;
100
104
  }
package/src/entry.ts CHANGED
@@ -20,7 +20,7 @@ export type Entry = { file: string; directory: string; format: CfScriptFormat };
20
20
  export async function getEntry(
21
21
  args: { script?: string; format?: CfScriptFormat | undefined },
22
22
  config: Config,
23
- command: string
23
+ command: "dev" | "publish"
24
24
  ): Promise<Entry> {
25
25
  let file: string;
26
26
  let directory = process.cwd();
@@ -46,8 +46,37 @@ export async function getEntry(
46
46
  const format = await guessWorkerFormat(
47
47
  file,
48
48
  directory,
49
- args.format ?? config.build?.upload?.format
49
+ args.format ?? config.build?.upload?.format,
50
+ config.tsconfig
50
51
  );
52
+
53
+ const { localBindings, remoteBindings } =
54
+ partitionDurableObjectBindings(config);
55
+
56
+ if (command === "dev" && remoteBindings.length > 0) {
57
+ console.warn(
58
+ "WARNING: You have Durable Object bindings, which are not defined locally in the worker being developed.\n" +
59
+ "Be aware that changes to the data stored in these Durable Objects will be permanent and affect the live instances.\n" +
60
+ "Remote Durable Objects that are affected:\n" +
61
+ remoteBindings.map((b) => `- ${JSON.stringify(b)}`).join("\n")
62
+ );
63
+ }
64
+
65
+ if (format === "service-worker" && localBindings.length > 0) {
66
+ const errorMessage =
67
+ "You seem to be trying to use Durable Objects in a Worker written with Service Worker syntax.";
68
+ const addScriptName =
69
+ "You can use Durable Objects defined in other Workers by specifying a `script_name` in your wrangler.toml, where `script_name` is the name of the Worker that implements that Durable Object. For example:";
70
+ const addScriptNameExamples = generateAddScriptNameExamples(localBindings);
71
+ const migrateText =
72
+ "Alternatively, migrate your worker to ES Module syntax to implement a Durable Object in this Worker:";
73
+ const migrateUrl =
74
+ "https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/";
75
+ throw new Error(
76
+ `${errorMessage}\n${addScriptName}\n${addScriptNameExamples}\n${migrateText}\n${migrateUrl}`
77
+ );
78
+ }
79
+
51
80
  return { file, directory, format };
52
81
  }
53
82
 
@@ -88,7 +117,8 @@ export async function runCustomBuild(
88
117
  export default async function guessWorkerFormat(
89
118
  entryFile: string,
90
119
  entryWorkingDirectory: string,
91
- hint: CfScriptFormat | undefined
120
+ hint: CfScriptFormat | undefined,
121
+ tsconfig?: string | undefined
92
122
  ): Promise<CfScriptFormat> {
93
123
  const result = await esbuild.build({
94
124
  entryPoints: [entryFile],
@@ -97,6 +127,12 @@ export default async function guessWorkerFormat(
97
127
  bundle: false,
98
128
  format: "esm",
99
129
  write: false,
130
+ loader: {
131
+ ".js": "jsx",
132
+ ".mjs": "jsx",
133
+ ".cjs": "jsx",
134
+ },
135
+ ...(tsconfig && { tsconfig }),
100
136
  });
101
137
  // result.metafile is defined because of the `metafile: true` option above.
102
138
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -114,14 +150,30 @@ export default async function guessWorkerFormat(
114
150
  "More than one entry-point found for generated bundle." +
115
151
  listEntryPoints(entryPoints)
116
152
  );
117
- const guessedWorkerFormat =
118
- entryPoints[0][1].exports.length > 0 ? "modules" : "service-worker";
153
+
154
+ let guessedWorkerFormat: CfScriptFormat;
155
+ const scriptExports = entryPoints[0][1].exports;
156
+ if (scriptExports.length > 0) {
157
+ if (scriptExports.includes("default")) {
158
+ guessedWorkerFormat = "modules";
159
+ } else {
160
+ console.warn(
161
+ `The entrypoint ${path.relative(
162
+ process.cwd(),
163
+ entryFile
164
+ )} has exports like an ES Module, but hasn't defined a default export like a module worker normally would. Building the worker using "service-worker" format...`
165
+ );
166
+ guessedWorkerFormat = "service-worker";
167
+ }
168
+ } else {
169
+ guessedWorkerFormat = "service-worker";
170
+ }
119
171
 
120
172
  if (hint) {
121
173
  if (hint !== guessedWorkerFormat) {
122
174
  if (hint === "service-worker") {
123
175
  throw new Error(
124
- "You configured this worker to be a 'service-worker', but the file you are trying to build appears to have ES module exports. Please pass `--format modules`, or simply remove the configuration."
176
+ "You configured this worker to be a 'service-worker', but the file you are trying to build appears to have a `default` export like a module worker. Please pass `--format modules`, or simply remove the configuration."
125
177
  );
126
178
  } else {
127
179
  throw new Error(
@@ -157,3 +209,48 @@ export function fileExists(filePath: string): boolean {
157
209
  }
158
210
  return false;
159
211
  }
212
+
213
+ type DurableObjectBindings = Config["durable_objects"]["bindings"];
214
+
215
+ /**
216
+ * Groups the durable object bindings into two lists:
217
+ * those that are defined locally and those that refer to a durable object defined in another script.
218
+ */
219
+ function partitionDurableObjectBindings(config: Config): {
220
+ localBindings: DurableObjectBindings;
221
+ remoteBindings: DurableObjectBindings;
222
+ } {
223
+ const localBindings: DurableObjectBindings = [];
224
+ const remoteBindings: DurableObjectBindings = [];
225
+ for (const binding of config.durable_objects.bindings) {
226
+ if (binding.script_name === undefined) {
227
+ localBindings.push(binding);
228
+ } else {
229
+ remoteBindings.push(binding);
230
+ }
231
+ }
232
+ return { localBindings, remoteBindings };
233
+ }
234
+
235
+ /**
236
+ * Generates some help text based on the Durable Object bindings in a given
237
+ * config indicating how the user can add a `script_name` field to bind an
238
+ * externally defined Durable Object.
239
+ */
240
+ function generateAddScriptNameExamples(
241
+ localBindings: DurableObjectBindings
242
+ ): string {
243
+ function exampleScriptName(binding_name: string): string {
244
+ return `${binding_name.toLowerCase().replaceAll("_", "-")}-worker`;
245
+ }
246
+
247
+ return localBindings
248
+ .map(({ name, class_name }) => {
249
+ const script_name = exampleScriptName(name);
250
+ const currentBinding = `{ name = ${name}, class_name = ${class_name} }`;
251
+ const fixedBinding = `{ name = ${name}, class_name = ${class_name}, script_name = ${script_name} }`;
252
+
253
+ return `${currentBinding} ==> ${fixedBinding}`;
254
+ })
255
+ .join("\n");
256
+ }