wrangler 0.0.0-e2a26a0 → 0.0.0-e3cab74

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 (68) hide show
  1. package/package.json +16 -7
  2. package/pages/functions/filepath-routing.test.ts +112 -36
  3. package/pages/functions/filepath-routing.ts +66 -119
  4. package/pages/functions/routes.ts +8 -17
  5. package/pages/functions/template-worker.ts +3 -9
  6. package/src/__tests__/configuration.test.ts +2185 -0
  7. package/src/__tests__/dev.test.tsx +48 -6
  8. package/src/__tests__/guess-worker-format.test.ts +66 -0
  9. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  10. package/src/__tests__/helpers/mock-account-id.ts +6 -6
  11. package/src/__tests__/helpers/mock-bin.ts +4 -1
  12. package/src/__tests__/helpers/mock-cfetch.ts +2 -2
  13. package/src/__tests__/helpers/mock-console.ts +11 -5
  14. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  15. package/src/__tests__/helpers/run-wrangler.ts +5 -3
  16. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  17. package/src/__tests__/index.test.ts +343 -20
  18. package/src/__tests__/jest.setup.ts +21 -1
  19. package/src/__tests__/kv.test.ts +82 -80
  20. package/src/__tests__/package-manager.test.ts +2 -2
  21. package/src/__tests__/parse.test.ts +322 -0
  22. package/src/__tests__/publish.test.ts +1428 -244
  23. package/src/__tests__/r2.test.ts +198 -0
  24. package/src/__tests__/secret.test.ts +176 -60
  25. package/src/__tests__/sentry.test.ts +146 -0
  26. package/src/__tests__/tail.test.ts +564 -0
  27. package/src/api/form_data.ts +106 -23
  28. package/src/api/preview.ts +32 -13
  29. package/src/api/worker.ts +49 -18
  30. package/src/bundle.ts +133 -0
  31. package/src/cfetch/index.ts +8 -6
  32. package/src/cfetch/internal.ts +49 -17
  33. package/src/cli.ts +10 -0
  34. package/src/config/README.md +107 -0
  35. package/src/config/config.ts +265 -0
  36. package/src/config/diagnostics.ts +80 -0
  37. package/src/config/environment.ts +305 -0
  38. package/src/config/index.ts +52 -0
  39. package/src/config/validation-helpers.ts +370 -0
  40. package/src/config/validation.ts +1017 -0
  41. package/src/dev.tsx +338 -211
  42. package/src/entry.ts +57 -0
  43. package/src/environment-variables.ts +33 -0
  44. package/src/guess-worker-format.ts +68 -0
  45. package/src/index.tsx +860 -634
  46. package/src/inspect.ts +16 -3
  47. package/src/kv.tsx +20 -29
  48. package/src/module-collection.ts +141 -34
  49. package/src/open-in-browser.ts +13 -0
  50. package/src/package-manager.ts +21 -15
  51. package/src/pages.tsx +9 -10
  52. package/src/parse.ts +183 -0
  53. package/src/proxy.ts +11 -5
  54. package/src/publish.ts +272 -314
  55. package/src/r2.ts +50 -0
  56. package/src/reporting.ts +115 -0
  57. package/src/sites.tsx +16 -15
  58. package/src/tail/filters.ts +279 -0
  59. package/src/tail/index.ts +299 -0
  60. package/src/tail/printing.ts +62 -0
  61. package/src/user.tsx +74 -44
  62. package/templates/new-worker.js +1 -1
  63. package/templates/new-worker.ts +1 -1
  64. package/templates/tsconfig.json +105 -0
  65. package/wrangler-dist/cli.js +51308 -36440
  66. package/wrangler-dist/cli.js.map +3 -3
  67. package/src/config.ts +0 -641
  68. package/src/tail.tsx +0 -73
package/src/dev.tsx CHANGED
@@ -1,40 +1,43 @@
1
1
  import assert from "node:assert";
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { watch } from "chokidar";
7
7
  import clipboardy from "clipboardy";
8
8
  import commandExists from "command-exists";
9
- import * as esbuild from "esbuild";
10
9
  import { execaCommand } from "execa";
11
10
  import { Box, Text, useApp, useInput } from "ink";
12
- import open from "open";
13
11
  import React, { useState, useEffect, useRef } from "react";
14
12
  import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
15
13
  import onExit from "signal-exit";
16
14
  import tmp from "tmp-promise";
17
15
  import { fetch } from "undici";
18
-
19
16
  import { createWorker } from "./api/worker";
20
-
17
+ import { bundleWorker } from "./bundle";
18
+ import guessWorkerFormat from "./guess-worker-format";
21
19
  import useInspector from "./inspect";
22
- import makeModuleCollector from "./module-collection";
20
+ import { DEFAULT_MODULE_RULES } from "./module-collection";
21
+ import openInBrowser from "./open-in-browser";
23
22
  import { usePreviewServer, waitForPortToBeAvailable } from "./proxy";
24
23
  import { syncAssets } from "./sites";
25
24
  import { getAPIToken } from "./user";
26
25
  import type { CfPreviewToken } from "./api/preview";
27
- import type { CfModule, CfWorkerInit } from "./api/worker";
26
+ import type { CfModule, CfWorkerInit, CfScriptFormat } from "./api/worker";
27
+ import type { Config } from "./config";
28
+ import type { Entry } from "./entry";
28
29
  import type { AssetPaths } from "./sites";
30
+ import type { WatchMode } from "esbuild";
31
+ import type { ExecaChildProcess } from "execa";
29
32
  import type { DirectoryResult } from "tmp-promise";
30
33
 
31
- type CfScriptFormat = undefined | "modules" | "service-worker";
32
-
33
34
  export type DevProps = {
34
35
  name?: string;
35
- entry: string;
36
+ entry: Entry;
36
37
  port?: number;
37
- format: CfScriptFormat;
38
+ inspectorPort: number;
39
+ format: CfScriptFormat | undefined;
40
+ rules: Config["rules"];
38
41
  accountId: undefined | string;
39
42
  initialMode: "local" | "remote";
40
43
  jsxFactory: undefined | string;
@@ -52,16 +55,13 @@ export type DevProps = {
52
55
  watch_dir?: undefined | string;
53
56
  };
54
57
  env: string | undefined;
58
+ legacyEnv: boolean;
59
+ zone: string | undefined;
55
60
  };
56
61
 
57
62
  function Dev(props: DevProps): JSX.Element {
58
- if (props.public && props.format === "service-worker") {
59
- throw new Error(
60
- "You cannot use the service worker format with a `public` directory."
61
- );
62
- }
63
63
  const port = props.port ?? 8787;
64
- const apiToken = getAPIToken();
64
+ const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
65
65
  const directory = useTmpDir();
66
66
 
67
67
  // if there isn't a build command, we just return the entry immediately
@@ -69,57 +69,81 @@ function Dev(props: DevProps): JSX.Element {
69
69
  // kinda forbid that, so we thread the entry through useCustomBuild
70
70
  const entry = useCustomBuild(props.entry, props.buildCommand);
71
71
 
72
+ const format = useWorkerFormat({ entry: props.entry, format: props.format });
73
+ if (format && props.public && format === "service-worker") {
74
+ throw new Error(
75
+ "You cannot use the service worker format with a `public` directory."
76
+ );
77
+ }
78
+
79
+ if (props.bindings.wasm_modules && format === "modules") {
80
+ throw new Error(
81
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
82
+ );
83
+ }
84
+
85
+ if (props.bindings.text_blobs && format === "modules") {
86
+ throw new Error(
87
+ "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[build.upload.rules]` in your wrangler.toml"
88
+ );
89
+ }
90
+
72
91
  const bundle = useEsbuild({
73
92
  entry,
93
+ format,
74
94
  destination: directory,
75
95
  staticRoot: props.public,
76
96
  jsxFactory: props.jsxFactory,
97
+ rules: props.rules,
77
98
  jsxFragment: props.jsxFragment,
99
+ serveAssetsFromWorker: !!props.public,
78
100
  });
79
- if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
80
- throw new Error(
81
- "You cannot use the service worker format with a `public` directory."
82
- );
83
- }
84
101
 
85
102
  const toggles = useHotkeys(
86
103
  {
87
104
  local: props.initialMode === "local",
88
105
  tunnel: false,
89
106
  },
90
- port
107
+ port,
108
+ props.inspectorPort
91
109
  );
92
110
 
93
111
  useTunnel(toggles.tunnel);
112
+ const scriptName = props.name || path.basename(props.entry.file);
94
113
 
95
114
  return (
96
115
  <>
97
116
  {toggles.local ? (
98
117
  <Local
99
- name={props.name}
118
+ name={scriptName}
100
119
  bundle={bundle}
101
- format={props.format}
120
+ format={format}
102
121
  bindings={props.bindings}
103
122
  assetPaths={props.assetPaths}
104
123
  public={props.public}
105
124
  port={port}
125
+ rules={props.rules}
126
+ inspectorPort={props.inspectorPort}
106
127
  enableLocalPersistence={props.enableLocalPersistence}
107
128
  />
108
129
  ) : (
109
130
  <Remote
110
131
  name={props.name}
111
132
  bundle={bundle}
112
- format={props.format}
133
+ format={format}
113
134
  accountId={props.accountId}
114
135
  apiToken={apiToken}
115
136
  bindings={props.bindings}
116
137
  assetPaths={props.assetPaths}
117
138
  public={props.public}
118
139
  port={port}
140
+ inspectorPort={props.inspectorPort}
119
141
  compatibilityDate={props.compatibilityDate}
120
142
  compatibilityFlags={props.compatibilityFlags}
121
143
  usageModel={props.usageModel}
122
144
  env={props.env}
145
+ legacyEnv={props.legacyEnv}
146
+ zone={props.zone}
123
147
  />
124
148
  )}
125
149
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
@@ -135,13 +159,33 @@ function Dev(props: DevProps): JSX.Element {
135
159
  );
136
160
  }
137
161
 
162
+ function useWorkerFormat(props: {
163
+ entry: Entry | undefined;
164
+ format: undefined | CfScriptFormat;
165
+ }): CfScriptFormat | undefined {
166
+ const [format, setFormat] = useState<CfScriptFormat | undefined>();
167
+ useEffect(() => {
168
+ async function validateFormat() {
169
+ if (!props.entry || format) {
170
+ return;
171
+ }
172
+ setFormat(await guessWorkerFormat(props.entry, props.format));
173
+ }
174
+ validateFormat().catch((err) => {
175
+ console.error("Failed to validate worker format:", err);
176
+ });
177
+ }, [props.entry, props.format, format]);
178
+ return format;
179
+ }
180
+
138
181
  function Remote(props: {
139
182
  name: undefined | string;
140
183
  bundle: EsbuildBundle | undefined;
141
- format: CfScriptFormat;
184
+ format: CfScriptFormat | undefined;
142
185
  public: undefined | string;
143
186
  assetPaths: undefined | AssetPaths;
144
187
  port: number;
188
+ inspectorPort: number;
145
189
  accountId: undefined | string;
146
190
  apiToken: undefined | string;
147
191
  bindings: CfWorkerInit["bindings"];
@@ -149,6 +193,8 @@ function Remote(props: {
149
193
  compatibilityFlags: undefined | string[];
150
194
  usageModel: undefined | "bundled" | "unbound";
151
195
  env: string | undefined;
196
+ legacyEnv: boolean | undefined;
197
+ zone: string | undefined;
152
198
  }) {
153
199
  assert(props.accountId, "accountId is required");
154
200
  assert(props.apiToken, "apiToken is required");
@@ -166,6 +212,8 @@ function Remote(props: {
166
212
  compatibilityFlags: props.compatibilityFlags,
167
213
  usageModel: props.usageModel,
168
214
  env: props.env,
215
+ legacyEnv: props.legacyEnv,
216
+ zone: props.zone,
169
217
  });
170
218
 
171
219
  usePreviewServer({
@@ -176,7 +224,7 @@ function Remote(props: {
176
224
 
177
225
  useInspector({
178
226
  inspectorUrl: previewToken ? previewToken.inspectorUrl.href : undefined,
179
- port: 9229,
227
+ port: props.inspectorPort,
180
228
  logToTerminal: true,
181
229
  });
182
230
  return null;
@@ -184,11 +232,13 @@ function Remote(props: {
184
232
  function Local(props: {
185
233
  name: undefined | string;
186
234
  bundle: EsbuildBundle | undefined;
187
- format: CfScriptFormat;
235
+ format: CfScriptFormat | undefined;
188
236
  bindings: CfWorkerInit["bindings"];
189
237
  assetPaths: undefined | AssetPaths;
190
238
  public: undefined | string;
191
239
  port: number;
240
+ rules: Config["rules"];
241
+ inspectorPort: number;
192
242
  enableLocalPersistence: boolean;
193
243
  }) {
194
244
  const { inspectorUrl } = useLocalWorker({
@@ -199,98 +249,151 @@ function Local(props: {
199
249
  assetPaths: props.assetPaths,
200
250
  public: props.public,
201
251
  port: props.port,
252
+ rules: props.rules,
253
+ inspectorPort: props.port,
202
254
  enableLocalPersistence: props.enableLocalPersistence,
203
255
  });
204
- useInspector({ inspectorUrl, port: 9229, logToTerminal: false });
256
+ useInspector({
257
+ inspectorUrl,
258
+ port: props.inspectorPort,
259
+ logToTerminal: false,
260
+ });
205
261
  return null;
206
262
  }
207
263
 
208
264
  function useLocalWorker(props: {
209
265
  name: undefined | string;
210
266
  bundle: EsbuildBundle | undefined;
211
- format: CfScriptFormat;
267
+ rules: Config["rules"];
268
+ format: CfScriptFormat | undefined;
212
269
  bindings: CfWorkerInit["bindings"];
213
270
  assetPaths: undefined | AssetPaths;
214
271
  public: undefined | string;
215
272
  port: number;
273
+ inspectorPort: number;
216
274
  enableLocalPersistence: boolean;
217
275
  }) {
218
276
  // TODO: pass vars via command line
219
- const { bundle, format, bindings, port, assetPaths } = props;
277
+ const { bundle, format, bindings, port, assetPaths, inspectorPort } = props;
220
278
  const local = useRef<ReturnType<typeof spawn>>();
221
279
  const removeSignalExitListener = useRef<() => void>();
222
280
  const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
223
281
  useEffect(() => {
224
282
  async function startLocalWorker() {
225
- if (!bundle) return;
226
- if (format === "modules" && bundle.type === "commonjs") {
227
- console.error("⎔ Cannot use modules with a commonjs bundle.");
228
- // TODO: a much better error message here, with what to do next
229
- return;
230
- }
231
- if (format === "service-worker" && bundle.type !== "esm") {
232
- console.error("⎔ Cannot use service-worker with a esm bundle.");
233
- // TODO: a much better error message here, with what to do next
234
- return;
235
- }
283
+ if (!bundle || !format) return;
236
284
 
285
+ // port for the worker
237
286
  await waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 });
287
+ // port for inspector
288
+ await waitForPortToBeAvailable(inspectorPort, {
289
+ retryPeriod: 200,
290
+ timeout: 2000,
291
+ });
292
+
238
293
  if (props.public) {
239
294
  throw new Error(
240
295
  '⎔ A "public" folder is not yet supported in local mode.'
241
296
  );
242
297
  }
243
298
 
299
+ // In local mode, we want to copy all referenced modules into
300
+ // the output bundle directory before starting up
301
+ for (const module of bundle.modules) {
302
+ await writeFile(
303
+ path.join(path.dirname(bundle.path), module.name),
304
+ module.content
305
+ );
306
+ }
307
+
244
308
  console.log("⎔ Starting a local server...");
245
309
  // TODO: just use execa for this
246
- local.current = spawn("node", [
247
- "--experimental-vm-modules",
248
- "--inspect",
249
- require.resolve("miniflare/cli"),
250
- bundle.path,
251
- "--watch",
252
- "--wrangler-config",
253
- path.join(__dirname, "../miniflare-config-stubs/wrangler.empty.toml"),
254
- "--env",
255
- path.join(__dirname, "../miniflare-config-stubs/.env.empty"),
256
- "--package",
257
- path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
258
- "--port",
259
- port.toString(),
260
- ...(assetPaths
261
- ? [
262
- "--site",
263
- assetPaths.baseDirectory,
264
- ...assetPaths.includePatterns.map((pattern) => [
265
- "--site-include",
266
- pattern,
267
- ]),
268
- ...assetPaths.excludePatterns.map((pattern) => [
269
- "--site-exclude",
270
- pattern,
271
- ]),
272
- ].flatMap((x) => x)
273
- : []),
274
- ...(props.enableLocalPersistence
275
- ? ["--kv-persist", "--cache-persist", "--do-persist"]
276
- : []),
277
- ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
278
- return ["--binding", `${key}=${value}`];
279
- }),
280
- ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
281
- return ["--kv", binding];
282
- }),
283
- ...(bindings.durable_objects?.bindings || []).flatMap(
284
- ({ name, class_name }) => {
285
- return ["--do", `${name}=${class_name}`];
286
- }
287
- ),
288
- "--modules",
289
- format ||
290
- (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
291
- ? "true"
292
- : "false",
293
- ]);
310
+ local.current = spawn(
311
+ "node",
312
+ [
313
+ "--experimental-vm-modules",
314
+ "--inspect",
315
+ require.resolve("miniflare/cli"),
316
+ bundle.path,
317
+ "--watch",
318
+ "--wrangler-config",
319
+ path.join(__dirname, "../miniflare-config-stubs/wrangler.empty.toml"),
320
+ "--env",
321
+ path.join(__dirname, "../miniflare-config-stubs/.env.empty"),
322
+ "--package",
323
+ path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
324
+ "--port",
325
+ port.toString(),
326
+ ...(assetPaths
327
+ ? [
328
+ "--site",
329
+ path.join(process.cwd(), assetPaths.baseDirectory),
330
+ ...assetPaths.includePatterns.map((pattern) => [
331
+ "--site-include",
332
+ pattern,
333
+ ]),
334
+ ...assetPaths.excludePatterns.map((pattern) => [
335
+ "--site-exclude",
336
+ pattern,
337
+ ]),
338
+ ].flatMap((x) => x)
339
+ : []),
340
+ ...(props.enableLocalPersistence
341
+ ? ["--kv-persist", "--cache-persist", "--do-persist"]
342
+ : []),
343
+ ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
344
+ return ["--binding", `${key}=${value}`];
345
+ }),
346
+ ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
347
+ return ["--kv", binding];
348
+ }),
349
+ ...(bindings.durable_objects?.bindings || []).flatMap(
350
+ ({ name, class_name }) => {
351
+ return ["--do", `${name}=${class_name}`];
352
+ }
353
+ ),
354
+ ...Object.entries(bindings.wasm_modules || {}).flatMap(
355
+ ([name, filePath]) => {
356
+ return [
357
+ "--wasm",
358
+ `${name}=${path.join(process.cwd(), filePath)}`,
359
+ ];
360
+ }
361
+ ),
362
+ ...bundle.modules.reduce<string[]>((cmd, { name, type }) => {
363
+ if (format === "service-worker") {
364
+ if (type === "compiled-wasm") {
365
+ // In service-worker format, .wasm modules are referenced
366
+ // by global identifiers, so we convert it here.
367
+ // This identifier has to be a valid JS identifier,
368
+ // so we replace all non alphanumeric characters
369
+ // with an underscore.
370
+ const identifier = name.replace(/[^a-zA-Z0-9_$]/g, "_");
371
+ return cmd.concat([`--wasm`, `${identifier}=${name}`]);
372
+ } else {
373
+ // TODO: we should actually support this
374
+ throw new Error(
375
+ `⎔ Unsupported module type ${type} for file ${name} in service-worker format`
376
+ );
377
+ }
378
+ }
379
+ return cmd;
380
+ }, []),
381
+ "--modules",
382
+ String(format === "modules"),
383
+ ...(props.rules || [])
384
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
385
+ .concat(DEFAULT_MODULE_RULES!)
386
+ .flatMap((rule) =>
387
+ rule.globs.flatMap((glob) => [
388
+ "--modules-rule",
389
+ `${rule.type}=${glob}`,
390
+ ])
391
+ ),
392
+ ],
393
+ {
394
+ cwd: path.dirname(bundle.path),
395
+ }
396
+ );
294
397
  console.log(`⬣ Listening at http://localhost:${port}`);
295
398
 
296
399
  local.current.on("close", (code) => {
@@ -306,7 +409,7 @@ function useLocalWorker(props: {
306
409
  local.current.stderr?.on("data", (data: Buffer) => {
307
410
  console.error(`${data.toString()}`);
308
411
  const matches =
309
- /Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
412
+ /Debugger listening on (ws:\/\/127\.0\.0\.1:\d+\/[A-Za-z0-9-]+)/.exec(
310
413
  data.toString()
311
414
  );
312
415
  if (matches) {
@@ -349,12 +452,15 @@ function useLocalWorker(props: {
349
452
  bundle,
350
453
  format,
351
454
  port,
455
+ inspectorPort,
352
456
  bindings.durable_objects?.bindings,
353
457
  bindings.kv_namespaces,
354
458
  bindings.vars,
355
459
  props.enableLocalPersistence,
356
460
  assetPaths,
357
461
  props.public,
462
+ props.rules,
463
+ bindings.wasm_modules,
358
464
  ]);
359
465
  return { inspectorUrl };
360
466
  }
@@ -391,21 +497,22 @@ function useTmpDir(): string | undefined {
391
497
  }
392
498
 
393
499
  function useCustomBuild(
394
- expectedEntry: string,
500
+ expectedEntry: Entry,
395
501
  props: {
396
502
  command?: undefined | string;
397
503
  cwd?: undefined | string;
398
504
  watch_dir?: undefined | string;
399
505
  }
400
- ): undefined | string {
401
- const [entry, setEntry] = useState<string | undefined>(
506
+ ): undefined | Entry {
507
+ const [entry, setEntry] = useState<Entry | undefined>(
402
508
  // if there's no build command, just return the expected entry
403
- props.command || expectedEntry
509
+ !props.command ? expectedEntry : undefined
404
510
  );
405
511
  const { command, cwd, watch_dir } = props;
406
512
  useEffect(() => {
407
513
  if (!command) return;
408
- let cmd, interval;
514
+ let cmd: ExecaChildProcess<string> | undefined,
515
+ interval: NodeJS.Timeout | undefined;
409
516
  console.log("running:", command);
410
517
  cmd = execaCommand(command, {
411
518
  ...(cwd && { cwd }),
@@ -418,7 +525,7 @@ function useCustomBuild(
418
525
  "all",
419
526
  (_event, filePath) => {
420
527
  console.log(`The file ${filePath} changed, restarting build...`);
421
- cmd.kill();
528
+ cmd?.kill();
422
529
  cmd = execaCommand(command, {
423
530
  ...(cwd && { cwd }),
424
531
  shell: true,
@@ -433,27 +540,39 @@ function useCustomBuild(
433
540
  // if it does, we're done
434
541
  const startedAt = Date.now();
435
542
  interval = setInterval(() => {
436
- if (existsSync(expectedEntry)) {
437
- clearInterval(interval);
543
+ let fileExists = false;
544
+ try {
545
+ // Use require.resolve to use node's resolution algorithm,
546
+ // this lets us use paths without explicit .js extension
547
+ // TODO: we should probably remove this, because it doesn't
548
+ // take into consideration other extensions like .tsx, .ts, .jsx, etc
549
+ fileExists = existsSync(require.resolve(expectedEntry.file));
550
+ } catch (e) {
551
+ // fail silently, usually means require.resolve threw MODULE_NOT_FOUND
552
+ }
553
+
554
+ if (fileExists === true) {
555
+ interval && clearInterval(interval);
438
556
  setEntry(expectedEntry);
439
557
  } else {
440
558
  const elapsed = Date.now() - startedAt;
441
559
  // timeout after 30 seconds of waiting
442
- if (elapsed > 1000 * 60 * 30) {
443
- console.error("⎔ Build timed out.");
444
- clearInterval(interval);
445
- cmd.kill();
560
+ if (elapsed > 1000 * 30) {
561
+ console.error(
562
+ `⎔ Build timed out, Could not resolve ${expectedEntry.file}`
563
+ );
564
+ interval && clearInterval(interval);
565
+ cmd?.kill();
446
566
  }
447
567
  }
448
568
  }, 200);
449
- // TODO: we could probably timeout here after a while
450
569
 
451
570
  return () => {
452
571
  if (cmd) {
453
572
  cmd.kill();
454
573
  cmd = undefined;
455
574
  }
456
- clearInterval(interval);
575
+ interval && clearInterval(interval);
457
576
  interval = undefined;
458
577
  };
459
578
  }, [command, cwd, expectedEntry, watch_dir]);
@@ -463,102 +582,102 @@ function useCustomBuild(
463
582
  type EsbuildBundle = {
464
583
  id: number;
465
584
  path: string;
466
- entry: string;
585
+ entry: Entry;
467
586
  type: "esm" | "commonjs";
468
- exports: string[];
469
587
  modules: CfModule[];
588
+ serveAssetsFromWorker: boolean;
470
589
  };
471
590
 
472
- function useEsbuild(props: {
473
- entry: undefined | string;
591
+ function useEsbuild({
592
+ entry,
593
+ destination,
594
+ staticRoot,
595
+ jsxFactory,
596
+ jsxFragment,
597
+ format,
598
+ rules,
599
+ serveAssetsFromWorker,
600
+ }: {
601
+ entry: undefined | Entry;
474
602
  destination: string | undefined;
603
+ format: CfScriptFormat | undefined;
475
604
  staticRoot: undefined | string;
476
605
  jsxFactory: string | undefined;
477
606
  jsxFragment: string | undefined;
607
+ rules: Config["rules"];
608
+ serveAssetsFromWorker: boolean;
478
609
  }): EsbuildBundle | undefined {
479
- const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
480
610
  const [bundle, setBundle] = useState<EsbuildBundle>();
481
611
  useEffect(() => {
482
- let result: esbuild.BuildResult | undefined;
612
+ let stopWatching: (() => void) | undefined = undefined;
613
+
614
+ const watchMode: WatchMode = {
615
+ async onRebuild(error) {
616
+ if (error) console.error("watch build failed:", error);
617
+ else {
618
+ // nothing really changes here, so let's increment the id
619
+ // to change the return object's identity
620
+ setBundle((previousBundle) => {
621
+ assert(
622
+ previousBundle,
623
+ "Rebuild triggered with no previous build available"
624
+ );
625
+ return { ...previousBundle, id: previousBundle.id + 1 };
626
+ });
627
+ }
628
+ },
629
+ };
630
+
483
631
  async function build() {
484
- if (!destination || !entry) return;
485
- const moduleCollector = makeModuleCollector();
486
- result = await esbuild.build({
487
- entryPoints: [entry],
488
- bundle: true,
489
- outdir: destination,
490
- metafile: true,
491
- format: "esm",
492
- sourcemap: true,
493
- loader: {
494
- ".js": "jsx",
495
- ".html": "text",
496
- ".pem": "text",
497
- ".txt": "text",
498
- },
499
- ...(jsxFactory && { jsxFactory }),
500
- ...(jsxFragment && { jsxFragment }),
501
- external: ["__STATIC_CONTENT_MANIFEST"],
502
- conditions: ["worker", "browser"],
503
- plugins: [moduleCollector.plugin],
504
- // TODO: import.meta.url
505
- watch: {
506
- async onRebuild(error) {
507
- if (error) console.error("watch build failed:", error);
508
- else {
509
- // nothing really changes here, so let's increment the id
510
- // to change the return object's identity
511
- setBundle((previousBundle) => {
512
- if (previousBundle === undefined) {
513
- assert.fail(
514
- "Rebuild triggered with no previous build available"
515
- );
516
- }
517
- return { ...previousBundle, id: previousBundle.id + 1 };
518
- });
519
- }
520
- },
521
- },
522
- });
632
+ if (!destination || !entry || !format) return;
633
+
634
+ const { resolvedEntryPointPath, bundleType, modules, stop } =
635
+ await bundleWorker(entry, destination, {
636
+ // In dev, we serve assets from the local proxy before we send the request to the worker.
637
+ serveAssetsFromWorker: false,
638
+ jsxFactory,
639
+ jsxFragment,
640
+ format,
641
+ rules,
642
+ watch: watchMode,
643
+ });
523
644
 
524
- // result.metafile is defined because of the `metafile: true` option above.
525
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
526
- const metafile = result.metafile!;
527
- const outputEntry = Object.entries(metafile.outputs).find(
528
- ([_path, { entryPoint }]) => entryPoint === entry
529
- ); // assumedly only one entry point
645
+ // Capture the `stop()` method to use as the `useEffect()` destructor.
646
+ stopWatching = stop;
530
647
 
531
- if (outputEntry === undefined) {
532
- throw new Error(
533
- `Cannot find entry-point "${entry}" in generated bundle.`
534
- );
535
- }
536
648
  setBundle({
537
649
  id: 0,
538
650
  entry,
539
- path: outputEntry[0],
540
- type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
541
- exports: outputEntry[1].exports,
542
- modules: moduleCollector.modules,
651
+ path: resolvedEntryPointPath,
652
+ type: bundleType,
653
+ modules,
654
+ serveAssetsFromWorker,
543
655
  });
544
656
  }
545
- build().catch((_err) => {
546
- // esbuild already logs errors to stderr
547
- // and we don't want to end the process
548
- // on build errors anyway
549
- // so this is a no-op error handler
657
+
658
+ build().catch(() => {
659
+ // esbuild already logs errors to stderr and we don't want to end the process
660
+ // on build errors anyway so this is a no-op error handler
550
661
  });
551
- return () => {
552
- result?.stop?.();
553
- };
554
- }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
662
+
663
+ return stopWatching;
664
+ }, [
665
+ entry,
666
+ destination,
667
+ staticRoot,
668
+ jsxFactory,
669
+ jsxFragment,
670
+ format,
671
+ serveAssetsFromWorker,
672
+ rules,
673
+ ]);
555
674
  return bundle;
556
675
  }
557
676
 
558
677
  function useWorker(props: {
559
678
  name: undefined | string;
560
679
  bundle: EsbuildBundle | undefined;
561
- format: CfScriptFormat;
680
+ format: CfScriptFormat | undefined;
562
681
  modules: CfModule[];
563
682
  accountId: string;
564
683
  apiToken: string;
@@ -569,6 +688,8 @@ function useWorker(props: {
569
688
  compatibilityFlags: string[] | undefined;
570
689
  usageModel: undefined | "bundled" | "unbound";
571
690
  env: string | undefined;
691
+ legacyEnv: boolean | undefined;
692
+ zone: string | undefined;
572
693
  }): CfPreviewToken | undefined {
573
694
  const {
574
695
  name,
@@ -595,17 +716,7 @@ function useWorker(props: {
595
716
  async function start() {
596
717
  setToken(undefined); // reset token in case we're re-running
597
718
 
598
- if (!bundle) return;
599
- if (format === "modules" && bundle.type === "commonjs") {
600
- console.error("⎔ Cannot use modules with a commonjs bundle.");
601
- // TODO: a much better error message here, with what to do next
602
- return;
603
- }
604
- if (format === "service-worker" && bundle.type !== "esm") {
605
- console.error("⎔ Cannot use service-worker with a esm bundle.");
606
- // TODO: a much better error message here, with what to do next
607
- return;
608
- }
719
+ if (!bundle || !format) return;
609
720
 
610
721
  if (!startedRef.current) {
611
722
  startedRef.current = true;
@@ -615,29 +726,26 @@ function useWorker(props: {
615
726
 
616
727
  const assets = await syncAssets(
617
728
  accountId,
618
- name || path.basename(bundle.path),
729
+ // When we're using the newer service environments, we wouldn't
730
+ // have added the env name on to the script name. However, we must
731
+ // include it in the kv namespace name regardless (since there's no
732
+ // concept of service environments for kv namespaces yet).
733
+ name + (!props.legacyEnv && props.env ? `-${props.env}` : ""),
619
734
  assetPaths,
620
- true,
621
- props.env
735
+ true
622
736
  ); // TODO: cancellable?
623
737
 
624
- const workerType = format || bundle.type === "esm" ? "esm" : "commonjs";
625
- let content = await readFile(bundle.path, "utf-8");
626
- if (workerType !== "esm" && assets.manifest) {
627
- content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify(
628
- assets.manifest
629
- )};\n${content}`;
630
- }
738
+ const content = await readFile(bundle.path, "utf-8");
631
739
 
632
740
  const init: CfWorkerInit = {
633
741
  name,
634
742
  main: {
635
743
  name: path.basename(bundle.path),
636
- type: workerType,
744
+ type: format === "modules" ? "esm" : "commonjs",
637
745
  content,
638
746
  },
639
747
  modules: modules.concat(
640
- assets.manifest && workerType === "esm"
748
+ assets.manifest
641
749
  ? {
642
750
  name: "__STATIC_CONTENT_MANIFEST",
643
751
  content: JSON.stringify(assets.manifest),
@@ -652,6 +760,13 @@ function useWorker(props: {
652
760
  ? { binding: "__STATIC_CONTENT", id: assets.namespace }
653
761
  : []
654
762
  ),
763
+ text_blobs: {
764
+ ...bindings.text_blobs,
765
+ ...(assets.manifest &&
766
+ format === "service-worker" && {
767
+ __STATIC_CONTENT_MANIFEST: "__STATIC_CONTENT_MANIFEST",
768
+ }),
769
+ },
655
770
  },
656
771
  migrations: undefined, // no migrations in dev
657
772
  compatibility_date: compatibilityDate,
@@ -659,10 +774,14 @@ function useWorker(props: {
659
774
  usage_model: usageModel,
660
775
  };
661
776
  setToken(
662
- await createWorker(init, {
663
- accountId,
664
- apiToken,
665
- })
777
+ await createWorker(
778
+ init,
779
+ {
780
+ accountId,
781
+ apiToken,
782
+ },
783
+ { env: props.env, legacyEnv: props.legacyEnv, zone: props.zone }
784
+ )
666
785
  );
667
786
  }
668
787
  start().catch((err) => {
@@ -684,6 +803,8 @@ function useWorker(props: {
684
803
  bindings,
685
804
  modules,
686
805
  props.env,
806
+ props.legacyEnv,
807
+ props.zone,
687
808
  ]);
688
809
  return token;
689
810
  }
@@ -721,7 +842,7 @@ function useTunnel(toggle: boolean) {
721
842
  await commandExists("cloudflared");
722
843
  } catch (e) {
723
844
  console.error(
724
- "To share your worker on the internet, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
845
+ "To share your worker on the Internet, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
725
846
  );
726
847
  return;
727
848
  }
@@ -772,26 +893,32 @@ type useHotkeysInitialState = {
772
893
  local: boolean;
773
894
  tunnel: boolean;
774
895
  };
775
- function useHotkeys(initial: useHotkeysInitialState, port: number) {
896
+ function useHotkeys(
897
+ initial: useHotkeysInitialState,
898
+ port: number,
899
+ inspectorPort: number
900
+ ) {
776
901
  // UGH, we should put port in context instead
777
902
  const [toggles, setToggles] = useState(initial);
778
903
  useInput(
779
904
  async (
780
905
  input,
781
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
906
+ // eslint-disable-next-line unused-imports/no-unused-vars
782
907
  key
783
908
  ) => {
784
909
  switch (input.toLowerCase()) {
785
910
  // open browser
786
- case "b":
787
- await open(`http://localhost:${port}/`);
911
+ case "b": {
912
+ await openInBrowser(`http://localhost:${port}`);
788
913
  break;
914
+ }
789
915
  // toggle inspector
790
- case "d":
791
- await open(
792
- `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
916
+ case "d": {
917
+ await openInBrowser(
918
+ `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`
793
919
  );
794
920
  break;
921
+ }
795
922
  // toggle tunnel
796
923
  case "s":
797
924
  setToggles((previousToggles) => ({