wrangler 0.0.13 → 0.0.17

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 (67) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +20 -11
  3. package/pages/functions/buildWorker.ts +1 -1
  4. package/pages/functions/filepath-routing.test.ts +112 -28
  5. package/pages/functions/filepath-routing.ts +44 -51
  6. package/pages/functions/routes.ts +11 -18
  7. package/pages/functions/template-worker.ts +3 -9
  8. package/src/__tests__/dev.test.tsx +42 -5
  9. package/src/__tests__/guess-worker-format.test.ts +66 -0
  10. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  11. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  12. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  13. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  14. package/src/__tests__/helpers/mock-bin.ts +36 -0
  15. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +43 -9
  16. package/src/__tests__/helpers/mock-console.ts +62 -0
  17. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  18. package/src/__tests__/helpers/mock-kv.ts +40 -0
  19. package/src/__tests__/helpers/mock-user.ts +27 -0
  20. package/src/__tests__/helpers/mock-web-socket.ts +37 -0
  21. package/src/__tests__/{run-in-tmp.ts → helpers/run-in-tmp.ts} +1 -1
  22. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  23. package/src/__tests__/helpers/write-wrangler-toml.ts +20 -0
  24. package/src/__tests__/index.test.ts +418 -71
  25. package/src/__tests__/jest.setup.ts +30 -2
  26. package/src/__tests__/kv.test.ts +147 -252
  27. package/src/__tests__/logout.test.ts +50 -0
  28. package/src/__tests__/package-manager.test.ts +206 -0
  29. package/src/__tests__/publish.test.ts +1136 -291
  30. package/src/__tests__/r2.test.ts +206 -0
  31. package/src/__tests__/secret.test.ts +210 -0
  32. package/src/__tests__/sentry.test.ts +146 -0
  33. package/src/__tests__/tail.test.ts +246 -0
  34. package/src/__tests__/whoami.test.tsx +6 -47
  35. package/src/api/form_data.ts +75 -25
  36. package/src/api/preview.ts +2 -2
  37. package/src/api/worker.ts +34 -15
  38. package/src/bundle.ts +127 -0
  39. package/src/cfetch/index.ts +7 -15
  40. package/src/cfetch/internal.ts +41 -6
  41. package/src/cli.ts +10 -0
  42. package/src/config.ts +125 -95
  43. package/src/dev.tsx +300 -193
  44. package/src/dialogs.tsx +2 -2
  45. package/src/guess-worker-format.ts +68 -0
  46. package/src/index.tsx +578 -192
  47. package/src/inspect.ts +29 -10
  48. package/src/kv.tsx +23 -17
  49. package/src/module-collection.ts +32 -12
  50. package/src/open-in-browser.ts +13 -0
  51. package/src/package-manager.ts +120 -0
  52. package/src/pages.tsx +28 -23
  53. package/src/paths.ts +26 -0
  54. package/src/proxy.ts +88 -14
  55. package/src/publish.ts +260 -297
  56. package/src/r2.ts +50 -0
  57. package/src/reporting.ts +115 -0
  58. package/src/sites.tsx +28 -27
  59. package/src/tail.tsx +178 -9
  60. package/src/user.tsx +58 -44
  61. package/templates/new-worker.js +15 -0
  62. package/templates/new-worker.ts +15 -0
  63. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  64. package/wrangler-dist/cli.js +124315 -104677
  65. package/wrangler-dist/cli.js.map +3 -3
  66. package/src/__tests__/mock-console.ts +0 -34
  67. package/src/__tests__/run-wrangler.ts +0 -8
package/src/dev.tsx CHANGED
@@ -1,45 +1,44 @@
1
- import esbuild from "esbuild";
2
1
  import assert from "node:assert";
3
2
  import { spawn } from "node:child_process";
4
- import { readFile } from "node:fs/promises";
5
3
  import { existsSync } from "node:fs";
4
+ import { readFile, writeFile } from "node:fs/promises";
6
5
  import path from "node:path";
7
- import { Box, Text, useApp, useInput } from "ink";
8
6
  import { watch } from "chokidar";
9
7
  import clipboardy from "clipboardy";
10
8
  import commandExists from "command-exists";
11
- import { execa } from "execa";
12
- import fetch from "node-fetch";
13
- import open from "open";
9
+ import { execaCommand } from "execa";
10
+ import { Box, Text, useApp, useInput } from "ink";
14
11
  import React, { useState, useEffect, useRef } from "react";
15
12
  import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
16
13
  import onExit from "signal-exit";
17
14
  import tmp from "tmp-promise";
18
- import type { DirectoryResult } from "tmp-promise";
19
-
20
- import type { CfPreviewToken } from "./api/preview";
21
- import type { CfModule } from "./api/worker";
15
+ import { fetch } from "undici";
22
16
  import { createWorker } from "./api/worker";
23
- import type { CfWorkerInit } from "./api/worker";
24
-
17
+ import { bundleWorker } from "./bundle";
18
+ import guessWorkerFormat from "./guess-worker-format";
25
19
  import useInspector from "./inspect";
26
- import makeModuleCollector from "./module-collection";
27
- import { usePreviewServer } from "./proxy";
28
- import type { AssetPaths } from "./sites";
20
+ import openInBrowser from "./open-in-browser";
21
+ import { usePreviewServer, waitForPortToBeAvailable } from "./proxy";
29
22
  import { syncAssets } from "./sites";
30
23
  import { getAPIToken } from "./user";
31
-
32
- type CfScriptFormat = undefined | "modules" | "service-worker";
24
+ import type { CfPreviewToken } from "./api/preview";
25
+ import type { CfModule, CfWorkerInit, CfScriptFormat } from "./api/worker";
26
+ import type { Entry } from "./bundle";
27
+ import type { AssetPaths } from "./sites";
28
+ import type { WatchMode } from "esbuild";
29
+ import type { ExecaChildProcess } from "execa";
30
+ import type { DirectoryResult } from "tmp-promise";
33
31
 
34
32
  export type DevProps = {
35
33
  name?: string;
36
- entry: string;
34
+ entry: Entry;
37
35
  port?: number;
38
- format: CfScriptFormat;
36
+ format: CfScriptFormat | undefined;
39
37
  accountId: undefined | string;
40
38
  initialMode: "local" | "remote";
41
39
  jsxFactory: undefined | string;
42
40
  jsxFragment: undefined | string;
41
+ enableLocalPersistence: boolean;
43
42
  bindings: CfWorkerInit["bindings"];
44
43
  public: undefined | string;
45
44
  assetPaths: undefined | AssetPaths;
@@ -51,16 +50,12 @@ export type DevProps = {
51
50
  cwd?: undefined | string;
52
51
  watch_dir?: undefined | string;
53
52
  };
53
+ env: string | undefined;
54
54
  };
55
55
 
56
56
  function Dev(props: DevProps): JSX.Element {
57
- if (props.public && props.format === "service-worker") {
58
- throw new Error(
59
- "You cannot use the service worker format with a `public` directory."
60
- );
61
- }
62
57
  const port = props.port ?? 8787;
63
- const apiToken = getAPIToken();
58
+ const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
64
59
  const directory = useTmpDir();
65
60
 
66
61
  // if there isn't a build command, we just return the entry immediately
@@ -68,18 +63,28 @@ function Dev(props: DevProps): JSX.Element {
68
63
  // kinda forbid that, so we thread the entry through useCustomBuild
69
64
  const entry = useCustomBuild(props.entry, props.buildCommand);
70
65
 
66
+ const format = useWorkerFormat({ entry: props.entry, format: props.format });
67
+ if (format && props.public && format === "service-worker") {
68
+ throw new Error(
69
+ "You cannot use the service worker format with a `public` directory."
70
+ );
71
+ }
72
+
73
+ if (props.bindings.wasm_modules && format === "modules") {
74
+ throw new Error(
75
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
76
+ );
77
+ }
78
+
71
79
  const bundle = useEsbuild({
72
80
  entry,
81
+ format,
73
82
  destination: directory,
74
83
  staticRoot: props.public,
75
84
  jsxFactory: props.jsxFactory,
76
85
  jsxFragment: props.jsxFragment,
86
+ serveAssetsFromWorker: !!props.public,
77
87
  });
78
- if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
79
- throw new Error(
80
- "You cannot use the service worker format with a `public` directory."
81
- );
82
- }
83
88
 
84
89
  const toggles = useHotkeys(
85
90
  {
@@ -97,17 +102,18 @@ function Dev(props: DevProps): JSX.Element {
97
102
  <Local
98
103
  name={props.name}
99
104
  bundle={bundle}
100
- format={props.format}
105
+ format={format}
101
106
  bindings={props.bindings}
102
- site={props.assetPaths}
107
+ assetPaths={props.assetPaths}
103
108
  public={props.public}
104
109
  port={port}
110
+ enableLocalPersistence={props.enableLocalPersistence}
105
111
  />
106
112
  ) : (
107
113
  <Remote
108
114
  name={props.name}
109
115
  bundle={bundle}
110
- format={props.format}
116
+ format={format}
111
117
  accountId={props.accountId}
112
118
  apiToken={apiToken}
113
119
  bindings={props.bindings}
@@ -117,6 +123,7 @@ function Dev(props: DevProps): JSX.Element {
117
123
  compatibilityDate={props.compatibilityDate}
118
124
  compatibilityFlags={props.compatibilityFlags}
119
125
  usageModel={props.usageModel}
126
+ env={props.env}
120
127
  />
121
128
  )}
122
129
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
@@ -132,10 +139,29 @@ function Dev(props: DevProps): JSX.Element {
132
139
  );
133
140
  }
134
141
 
142
+ function useWorkerFormat(props: {
143
+ entry: Entry | undefined;
144
+ format: undefined | CfScriptFormat;
145
+ }): CfScriptFormat | undefined {
146
+ const [format, setFormat] = useState<CfScriptFormat | undefined>();
147
+ useEffect(() => {
148
+ async function validateFormat() {
149
+ if (!props.entry || format) {
150
+ return;
151
+ }
152
+ setFormat(await guessWorkerFormat(props.entry, props.format));
153
+ }
154
+ validateFormat().catch((err) => {
155
+ console.error("Failed to validate worker format:", err);
156
+ });
157
+ }, [props.entry, props.format, format]);
158
+ return format;
159
+ }
160
+
135
161
  function Remote(props: {
136
162
  name: undefined | string;
137
163
  bundle: EsbuildBundle | undefined;
138
- format: CfScriptFormat;
164
+ format: CfScriptFormat | undefined;
139
165
  public: undefined | string;
140
166
  assetPaths: undefined | AssetPaths;
141
167
  port: number;
@@ -145,6 +171,7 @@ function Remote(props: {
145
171
  compatibilityDate: string | undefined;
146
172
  compatibilityFlags: undefined | string[];
147
173
  usageModel: undefined | "bundled" | "unbound";
174
+ env: string | undefined;
148
175
  }) {
149
176
  assert(props.accountId, "accountId is required");
150
177
  assert(props.apiToken, "apiToken is required");
@@ -161,6 +188,7 @@ function Remote(props: {
161
188
  compatibilityDate: props.compatibilityDate,
162
189
  compatibilityFlags: props.compatibilityFlags,
163
190
  usageModel: props.usageModel,
191
+ env: props.env,
164
192
  });
165
193
 
166
194
  usePreviewServer({
@@ -179,18 +207,22 @@ function Remote(props: {
179
207
  function Local(props: {
180
208
  name: undefined | string;
181
209
  bundle: EsbuildBundle | undefined;
182
- format: CfScriptFormat;
210
+ format: CfScriptFormat | undefined;
183
211
  bindings: CfWorkerInit["bindings"];
212
+ assetPaths: undefined | AssetPaths;
184
213
  public: undefined | string;
185
- site: undefined | AssetPaths;
186
214
  port: number;
215
+ enableLocalPersistence: boolean;
187
216
  }) {
188
217
  const { inspectorUrl } = useLocalWorker({
189
218
  name: props.name,
190
219
  bundle: props.bundle,
191
220
  format: props.format,
192
221
  bindings: props.bindings,
222
+ assetPaths: props.assetPaths,
223
+ public: props.public,
193
224
  port: props.port,
225
+ enableLocalPersistence: props.enableLocalPersistence,
194
226
  });
195
227
  useInspector({ inspectorUrl, port: 9229, logToTerminal: false });
196
228
  return null;
@@ -199,65 +231,115 @@ function Local(props: {
199
231
  function useLocalWorker(props: {
200
232
  name: undefined | string;
201
233
  bundle: EsbuildBundle | undefined;
202
- format: CfScriptFormat;
234
+ format: CfScriptFormat | undefined;
203
235
  bindings: CfWorkerInit["bindings"];
236
+ assetPaths: undefined | AssetPaths;
237
+ public: undefined | string;
204
238
  port: number;
239
+ enableLocalPersistence: boolean;
205
240
  }) {
206
241
  // TODO: pass vars via command line
207
- const { bundle, format, bindings, port } = props;
242
+ const { bundle, format, bindings, port, assetPaths } = props;
208
243
  const local = useRef<ReturnType<typeof spawn>>();
209
244
  const removeSignalExitListener = useRef<() => void>();
210
245
  const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
211
246
  useEffect(() => {
212
247
  async function startLocalWorker() {
213
- if (!bundle) return;
214
- if (format === "modules" && bundle.type === "commonjs") {
215
- console.error("⎔ Cannot use modules with a commonjs bundle.");
216
- // TODO: a much better error message here, with what to do next
217
- return;
248
+ if (!bundle || !format) return;
249
+
250
+ await waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 });
251
+ if (props.public) {
252
+ throw new Error(
253
+ '⎔ A "public" folder is not yet supported in local mode.'
254
+ );
218
255
  }
219
- if (format === "service-worker" && bundle.type !== "esm") {
220
- console.error("⎔ Cannot use service-worker with a esm bundle.");
221
- // TODO: a much better error message here, with what to do next
222
- return;
256
+
257
+ // In local mode, we want to copy all referenced modules into
258
+ // the output bundle directory before starting up
259
+ for (const module of bundle.modules) {
260
+ await writeFile(
261
+ path.join(path.dirname(bundle.path), module.name),
262
+ module.content
263
+ );
223
264
  }
224
265
 
225
266
  console.log("⎔ Starting a local server...");
226
267
  // TODO: just use execa for this
227
- local.current = spawn("node", [
228
- "--experimental-vm-modules",
229
- "--inspect",
230
- require.resolve("miniflare/cli"),
231
- bundle.path,
232
- "--watch",
233
- "--wrangler-config",
234
- path.join(__dirname, "../miniflare-config-stubs/wrangler.empty.toml"),
235
- "--env",
236
- path.join(__dirname, "../miniflare-config-stubs/.env.empty"),
237
- "--package",
238
- path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
239
- "--port",
240
- port.toString(),
241
- "--kv-persist",
242
- "--cache-persist",
243
- "--do-persist",
244
- ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
245
- return ["--binding", `${key}=${value}`];
246
- }),
247
- ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
248
- return ["--kv", binding];
249
- }),
250
- ...(bindings.durable_objects?.bindings || []).flatMap(
251
- ({ name, class_name }) => {
252
- return ["--do", `${name}=${class_name}`];
253
- }
254
- ),
255
- "--modules",
256
- format ||
257
- (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
258
- ? "true"
259
- : "false",
260
- ]);
268
+ local.current = spawn(
269
+ "node",
270
+ [
271
+ "--experimental-vm-modules",
272
+ "--inspect",
273
+ require.resolve("miniflare/cli"),
274
+ bundle.path,
275
+ "--watch",
276
+ "--wrangler-config",
277
+ path.join(__dirname, "../miniflare-config-stubs/wrangler.empty.toml"),
278
+ "--env",
279
+ path.join(__dirname, "../miniflare-config-stubs/.env.empty"),
280
+ "--package",
281
+ path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
282
+ "--port",
283
+ port.toString(),
284
+ ...(assetPaths
285
+ ? [
286
+ "--site",
287
+ path.join(process.cwd(), assetPaths.baseDirectory),
288
+ ...assetPaths.includePatterns.map((pattern) => [
289
+ "--site-include",
290
+ pattern,
291
+ ]),
292
+ ...assetPaths.excludePatterns.map((pattern) => [
293
+ "--site-exclude",
294
+ pattern,
295
+ ]),
296
+ ].flatMap((x) => x)
297
+ : []),
298
+ ...(props.enableLocalPersistence
299
+ ? ["--kv-persist", "--cache-persist", "--do-persist"]
300
+ : []),
301
+ ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
302
+ return ["--binding", `${key}=${value}`];
303
+ }),
304
+ ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
305
+ return ["--kv", binding];
306
+ }),
307
+ ...(bindings.durable_objects?.bindings || []).flatMap(
308
+ ({ name, class_name }) => {
309
+ return ["--do", `${name}=${class_name}`];
310
+ }
311
+ ),
312
+ ...Object.entries(bindings.wasm_modules || {}).flatMap(
313
+ ([name, filePath]) => {
314
+ return [
315
+ "--wasm",
316
+ `${name}=${path.join(process.cwd(), filePath)}`,
317
+ ];
318
+ }
319
+ ),
320
+ ...bundle.modules.reduce<string[]>((cmd, { name }) => {
321
+ if (format === "service-worker") {
322
+ if (name.endsWith(".wasm")) {
323
+ // In service-worker format, .wasm modules are referenced
324
+ // by global identifiers, so we convert it here.
325
+ // This identifier has to be a valid JS identifier,
326
+ // so we replace all non alphanumeric characters
327
+ // with an underscore.
328
+ const identifier = name.replace(/[^a-zA-Z0-9_$]/g, "_");
329
+ return cmd.concat([`--wasm`, `${identifier}=${name}`]);
330
+ }
331
+ }
332
+ return cmd;
333
+ }, []),
334
+ "--modules",
335
+ String(format === "modules"),
336
+ "--modules-rule",
337
+ "CompiledWasm=**/*.wasm",
338
+ ],
339
+ {
340
+ cwd: path.dirname(bundle.path),
341
+ }
342
+ );
261
343
  console.log(`⬣ Listening at http://localhost:${port}`);
262
344
 
263
345
  local.current.on("close", (code) => {
@@ -319,6 +401,10 @@ function useLocalWorker(props: {
319
401
  bindings.durable_objects?.bindings,
320
402
  bindings.kv_namespaces,
321
403
  bindings.vars,
404
+ props.enableLocalPersistence,
405
+ assetPaths,
406
+ props.public,
407
+ bindings.wasm_modules,
322
408
  ]);
323
409
  return { inspectorUrl };
324
410
  }
@@ -346,7 +432,7 @@ function useTmpDir(): string | undefined {
346
432
  return () => {
347
433
  dir.cleanup().catch(() => {
348
434
  // extremely unlikely,
349
- // but it's 2021 after all
435
+ // but it's 2022 after all
350
436
  console.error("failed to cleanup tmp dir");
351
437
  });
352
438
  };
@@ -355,36 +441,38 @@ function useTmpDir(): string | undefined {
355
441
  }
356
442
 
357
443
  function useCustomBuild(
358
- expectedEntry: string,
444
+ expectedEntry: Entry,
359
445
  props: {
360
446
  command?: undefined | string;
361
447
  cwd?: undefined | string;
362
448
  watch_dir?: undefined | string;
363
449
  }
364
- ): undefined | string {
365
- const [entry, setEntry] = useState<string | undefined>(
450
+ ): undefined | Entry {
451
+ const [entry, setEntry] = useState<Entry | undefined>(
366
452
  // if there's no build command, just return the expected entry
367
- props.command || expectedEntry
453
+ !props.command ? expectedEntry : undefined
368
454
  );
369
455
  const { command, cwd, watch_dir } = props;
370
456
  useEffect(() => {
371
457
  if (!command) return;
372
- let cmd, interval;
458
+ let cmd: ExecaChildProcess<string> | undefined,
459
+ interval: NodeJS.Timeout | undefined;
373
460
  console.log("running:", command);
374
- const commandPieces = command.split(" ");
375
- cmd = execa(commandPieces[0], commandPieces.slice(1), {
461
+ cmd = execaCommand(command, {
376
462
  ...(cwd && { cwd }),
463
+ shell: true,
377
464
  stderr: "inherit",
378
465
  stdout: "inherit",
379
466
  });
380
467
  if (watch_dir) {
381
468
  watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
382
469
  "all",
383
- (_event, _path) => {
384
- console.log(`The file ${path} changed, restarting build...`);
385
- cmd.kill();
386
- cmd = execa(commandPieces[0], commandPieces.slice(1), {
470
+ (_event, filePath) => {
471
+ console.log(`The file ${filePath} changed, restarting build...`);
472
+ cmd?.kill();
473
+ cmd = execaCommand(command, {
387
474
  ...(cwd && { cwd }),
475
+ shell: true,
388
476
  stderr: "inherit",
389
477
  stdout: "inherit",
390
478
  });
@@ -396,27 +484,39 @@ function useCustomBuild(
396
484
  // if it does, we're done
397
485
  const startedAt = Date.now();
398
486
  interval = setInterval(() => {
399
- if (existsSync(expectedEntry)) {
400
- clearInterval(interval);
487
+ let fileExists = false;
488
+ try {
489
+ // Use require.resolve to use node's resolution algorithm,
490
+ // this lets us use paths without explicit .js extension
491
+ // TODO: we should probably remove this, because it doesn't
492
+ // take into consideration other extensions like .tsx, .ts, .jsx, etc
493
+ fileExists = existsSync(require.resolve(expectedEntry.file));
494
+ } catch (e) {
495
+ // fail silently, usually means require.resolve threw MODULE_NOT_FOUND
496
+ }
497
+
498
+ if (fileExists === true) {
499
+ interval && clearInterval(interval);
401
500
  setEntry(expectedEntry);
402
501
  } else {
403
502
  const elapsed = Date.now() - startedAt;
404
503
  // timeout after 30 seconds of waiting
405
- if (elapsed > 1000 * 60 * 30) {
406
- console.error("⎔ Build timed out.");
407
- clearInterval(interval);
408
- cmd.kill();
504
+ if (elapsed > 1000 * 30) {
505
+ console.error(
506
+ `⎔ Build timed out, Could not resolve ${expectedEntry.file}`
507
+ );
508
+ interval && clearInterval(interval);
509
+ cmd?.kill();
409
510
  }
410
511
  }
411
512
  }, 200);
412
- // TODO: we could probably timeout here after a while
413
513
 
414
514
  return () => {
415
515
  if (cmd) {
416
516
  cmd.kill();
417
517
  cmd = undefined;
418
518
  }
419
- clearInterval(interval);
519
+ interval && clearInterval(interval);
420
520
  interval = undefined;
421
521
  };
422
522
  }, [command, cwd, expectedEntry, watch_dir]);
@@ -426,99 +526,100 @@ function useCustomBuild(
426
526
  type EsbuildBundle = {
427
527
  id: number;
428
528
  path: string;
429
- entry: string;
529
+ entry: Entry;
430
530
  type: "esm" | "commonjs";
431
- exports: string[];
432
531
  modules: CfModule[];
532
+ serveAssetsFromWorker: boolean;
433
533
  };
434
534
 
435
- function useEsbuild(props: {
436
- entry: undefined | string;
535
+ function useEsbuild({
536
+ entry,
537
+ destination,
538
+ staticRoot,
539
+ jsxFactory,
540
+ jsxFragment,
541
+ format,
542
+ serveAssetsFromWorker,
543
+ }: {
544
+ entry: undefined | Entry;
437
545
  destination: string | undefined;
546
+ format: CfScriptFormat | undefined;
438
547
  staticRoot: undefined | string;
439
548
  jsxFactory: string | undefined;
440
549
  jsxFragment: string | undefined;
550
+ serveAssetsFromWorker: boolean;
441
551
  }): EsbuildBundle | undefined {
442
- const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
443
552
  const [bundle, setBundle] = useState<EsbuildBundle>();
444
553
  useEffect(() => {
445
- let result: esbuild.BuildResult | undefined;
554
+ let stopWatching: (() => void) | undefined = undefined;
555
+
556
+ const watchMode: WatchMode = {
557
+ async onRebuild(error) {
558
+ if (error) console.error("watch build failed:", error);
559
+ else {
560
+ // nothing really changes here, so let's increment the id
561
+ // to change the return object's identity
562
+ setBundle((previousBundle) => {
563
+ assert(
564
+ previousBundle,
565
+ "Rebuild triggered with no previous build available"
566
+ );
567
+ return { ...previousBundle, id: previousBundle.id + 1 };
568
+ });
569
+ }
570
+ },
571
+ };
572
+
446
573
  async function build() {
447
- if (!destination || !entry) return;
448
- const moduleCollector = makeModuleCollector();
449
- result = await esbuild.build({
450
- entryPoints: [entry],
451
- bundle: true,
452
- outdir: destination,
453
- metafile: true,
454
- format: "esm",
455
- sourcemap: true,
456
- loader: {
457
- ".js": "jsx",
458
- },
459
- ...(jsxFactory && { jsxFactory }),
460
- ...(jsxFragment && { jsxFragment }),
461
- external: ["__STATIC_CONTENT_MANIFEST"],
462
- conditions: ["worker", "browser"],
463
- plugins: [moduleCollector.plugin],
464
- // TODO: import.meta.url
465
- watch: {
466
- async onRebuild(error) {
467
- if (error) console.error("watch build failed:", error);
468
- else {
469
- // nothing really changes here, so let's increment the id
470
- // to change the return object's identity
471
- setBundle((previousBundle) => {
472
- if (previousBundle === undefined) {
473
- assert.fail(
474
- "Rebuild triggered with no previous build available"
475
- );
476
- }
477
- return { ...previousBundle, id: previousBundle.id + 1 };
478
- });
479
- }
480
- },
481
- },
482
- });
574
+ if (!destination || !entry || !format) return;
575
+
576
+ const { resolvedEntryPointPath, bundleType, modules, stop } =
577
+ await bundleWorker(
578
+ entry,
579
+ // In dev, we server assets from the local proxy before we send the request to the worker.
580
+ /* serveAssetsFromWorker */ false,
581
+ destination,
582
+ jsxFactory,
583
+ jsxFragment,
584
+ format,
585
+ watchMode
586
+ );
483
587
 
484
- // result.metafile is defined because of the `metafile: true` option above.
485
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
486
- const metafile = result.metafile!;
487
- const outputEntry = Object.entries(metafile.outputs).find(
488
- ([_path, { entryPoint }]) => entryPoint === entry
489
- ); // assumedly only one entry point
588
+ // Capture the `stop()` method to use as the `useEffect()` destructor.
589
+ stopWatching = stop;
490
590
 
491
- if (outputEntry === undefined) {
492
- throw new Error(
493
- `Cannot find entry-point "${entry}" in generated bundle.`
494
- );
495
- }
496
591
  setBundle({
497
592
  id: 0,
498
593
  entry,
499
- path: outputEntry[0],
500
- type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
501
- exports: outputEntry[1].exports,
502
- modules: moduleCollector.modules,
594
+ path: resolvedEntryPointPath,
595
+ type: bundleType,
596
+ modules,
597
+ serveAssetsFromWorker,
503
598
  });
504
599
  }
505
- build().catch((_err) => {
506
- // esbuild already logs errors to stderr
507
- // and we don't want to end the process
508
- // on build errors anyway
509
- // so this is a no-op error handler
600
+
601
+ build().catch(() => {
602
+ // esbuild already logs errors to stderr and we don't want to end the process
603
+ // on build errors anyway so this is a no-op error handler
510
604
  });
511
- return () => {
512
- result?.stop?.();
513
- };
514
- }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
605
+
606
+ return stopWatching;
607
+ }, [
608
+ entry,
609
+ destination,
610
+ staticRoot,
611
+ jsxFactory,
612
+ jsxFragment,
613
+ format,
614
+ serveAssetsFromWorker,
615
+ ]);
515
616
  return bundle;
516
617
  }
517
618
 
518
619
  function useWorker(props: {
519
620
  name: undefined | string;
520
621
  bundle: EsbuildBundle | undefined;
521
- format: CfScriptFormat;
622
+ format: CfScriptFormat | undefined;
522
623
  modules: CfModule[];
523
624
  accountId: string;
524
625
  apiToken: string;
@@ -528,6 +629,7 @@ function useWorker(props: {
528
629
  compatibilityDate: string | undefined;
529
630
  compatibilityFlags: string[] | undefined;
530
631
  usageModel: undefined | "bundled" | "unbound";
632
+ env: string | undefined;
531
633
  }): CfPreviewToken | undefined {
532
634
  const {
533
635
  name,
@@ -554,17 +656,7 @@ function useWorker(props: {
554
656
  async function start() {
555
657
  setToken(undefined); // reset token in case we're re-running
556
658
 
557
- if (!bundle) return;
558
- if (format === "modules" && bundle.type === "commonjs") {
559
- console.error("⎔ Cannot use modules with a commonjs bundle.");
560
- // TODO: a much better error message here, with what to do next
561
- return;
562
- }
563
- if (format === "service-worker" && bundle.type !== "esm") {
564
- console.error("⎔ Cannot use service-worker with a esm bundle.");
565
- // TODO: a much better error message here, with what to do next
566
- return;
567
- }
659
+ if (!bundle || !format) return;
568
660
 
569
661
  if (!startedRef.current) {
570
662
  startedRef.current = true;
@@ -574,21 +666,28 @@ function useWorker(props: {
574
666
 
575
667
  const assets = await syncAssets(
576
668
  accountId,
577
- path.basename(bundle.path),
669
+ name || path.basename(bundle.path),
578
670
  assetPaths,
579
- true
671
+ true,
672
+ props.env
580
673
  ); // TODO: cancellable?
581
674
 
582
- const content = await readFile(bundle.path, "utf-8");
675
+ let content = await readFile(bundle.path, "utf-8");
676
+ if (format === "service-worker" && assets.manifest) {
677
+ content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify(
678
+ assets.manifest
679
+ )};\n${content}`;
680
+ }
681
+
583
682
  const init: CfWorkerInit = {
584
683
  name,
585
684
  main: {
586
685
  name: path.basename(bundle.path),
587
- type: format || bundle.type === "esm" ? "esm" : "commonjs",
686
+ type: format === "modules" ? "esm" : "commonjs",
588
687
  content,
589
688
  },
590
689
  modules: modules.concat(
591
- assets.manifest
690
+ assets.manifest && format === "modules"
592
691
  ? {
593
692
  name: "__STATIC_CONTENT_MANIFEST",
594
693
  content: JSON.stringify(assets.manifest),
@@ -634,6 +733,7 @@ function useWorker(props: {
634
733
  usageModel,
635
734
  bindings,
636
735
  modules,
736
+ props.env,
637
737
  ]);
638
738
  return token;
639
739
  }
@@ -731,29 +831,36 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
731
831
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
732
832
  key
733
833
  ) => {
734
- switch (input) {
735
- case "b": // open browser
736
- await open(`http://localhost:${port}/`);
834
+ switch (input.toLowerCase()) {
835
+ // open browser
836
+ case "b": {
837
+ await openInBrowser(`http://localhost:${port}`);
737
838
  break;
738
- case "d": // toggle inspector
739
- await open(
839
+ }
840
+ // toggle inspector
841
+ case "d": {
842
+ await openInBrowser(
740
843
  `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
741
844
  );
742
845
  break;
743
- case "s": // toggle tunnel
846
+ }
847
+ // toggle tunnel
848
+ case "s":
744
849
  setToggles((previousToggles) => ({
745
850
  ...previousToggles,
746
851
  tunnel: !previousToggles.tunnel,
747
852
  }));
748
853
  break;
749
- case "l": // toggle local
854
+ // toggle local
855
+ case "l":
750
856
  setToggles((previousToggles) => ({
751
857
  ...previousToggles,
752
858
  local: !previousToggles.local,
753
859
  }));
754
860
  break;
755
- case "q": // shut down
756
- case "x": // shut down
861
+ // shut down
862
+ case "q":
863
+ case "x":
757
864
  process.exit(0);
758
865
  break;
759
866
  default: