wrangler 0.0.0-e14e589 → 0.0.0-e1c615f

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 (41) hide show
  1. package/README.md +51 -0
  2. package/package.json +39 -26
  3. package/pages/functions/buildWorker.ts +57 -0
  4. package/pages/functions/filepath-routing.test.ts +39 -0
  5. package/pages/functions/filepath-routing.ts +221 -0
  6. package/pages/functions/identifiers.ts +78 -0
  7. package/pages/functions/routes.ts +158 -0
  8. package/pages/functions/template-worker.ts +137 -0
  9. package/src/__tests__/dev.test.tsx +66 -0
  10. package/src/__tests__/index.test.ts +254 -120
  11. package/src/__tests__/jest.setup.ts +22 -0
  12. package/src/__tests__/kv.test.ts +1098 -0
  13. package/src/__tests__/mock-cfetch.ts +171 -0
  14. package/src/__tests__/mock-dialogs.ts +65 -0
  15. package/src/__tests__/run-in-tmp.ts +19 -0
  16. package/src/__tests__/run-wrangler.ts +32 -0
  17. package/src/api/form_data.ts +78 -72
  18. package/src/api/inspect.ts +0 -11
  19. package/src/api/preview.ts +29 -24
  20. package/src/api/worker.ts +46 -31
  21. package/src/cfetch/index.ts +102 -0
  22. package/src/cfetch/internal.ts +69 -0
  23. package/src/cli.ts +6 -7
  24. package/src/config.ts +39 -4
  25. package/src/dev.tsx +332 -128
  26. package/src/dialogs.tsx +4 -12
  27. package/src/index.tsx +397 -222
  28. package/src/kv.tsx +101 -45
  29. package/src/module-collection.ts +64 -0
  30. package/src/pages.tsx +839 -211
  31. package/src/proxy.ts +103 -0
  32. package/src/publish.ts +117 -45
  33. package/src/sites.tsx +6 -7
  34. package/src/tail.tsx +6 -4
  35. package/src/user.tsx +9 -16
  36. package/wrangler-dist/cli.js +123794 -528
  37. package/wrangler-dist/cli.js.map +3 -3
  38. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  39. package/src/__tests__/mock-cfetch.js +0 -46
  40. package/src/cfetch.ts +0 -72
  41. package/src/util/fetch.ts +0 -74
package/src/dev.tsx CHANGED
@@ -1,15 +1,15 @@
1
1
  import esbuild from "esbuild";
2
- import httpProxy from "http-proxy";
3
2
  import { readFile } from "fs/promises";
3
+ import { existsSync } from "fs";
4
4
  import type { DirectoryResult } from "tmp-promise";
5
5
  import tmp from "tmp-promise";
6
6
  import type { CfPreviewToken } from "./api/preview";
7
- import { Box, Text, useInput } from "ink";
7
+ import { Box, Text, useApp, useInput } from "ink";
8
8
  import React, { useState, useEffect, useRef } from "react";
9
9
  import path from "path";
10
10
  import open from "open";
11
11
  import { DtInspector } from "./api/inspect";
12
- import type { CfModule, CfVariable } from "./api/worker";
12
+ import type { CfModule } from "./api/worker";
13
13
  import { createWorker } from "./api/worker";
14
14
  import type { CfWorkerInit } from "./api/worker";
15
15
  import { spawn } from "child_process";
@@ -17,27 +17,41 @@ import onExit from "signal-exit";
17
17
  import { syncAssets } from "./sites";
18
18
  import clipboardy from "clipboardy";
19
19
  import http from "node:http";
20
- import serveStatic from "serve-static";
21
20
  import commandExists from "command-exists";
22
21
  import assert from "assert";
23
22
  import { getAPIToken } from "./user";
24
23
  import fetch from "node-fetch";
24
+ import makeModuleCollector from "./module-collection";
25
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
26
+ import { createHttpProxy } from "./proxy";
27
+ import { execa } from "execa";
28
+ import { watch } from "chokidar";
25
29
 
26
30
  type CfScriptFormat = void | "modules" | "service-worker";
27
31
 
28
- type Props = {
32
+ export type DevProps = {
29
33
  name?: string;
30
34
  entry: string;
31
35
  port?: number;
32
36
  format: CfScriptFormat;
33
37
  accountId: void | string;
34
38
  initialMode: "local" | "remote";
35
- variables?: { [name: string]: CfVariable };
36
- public?: void | string;
37
- site?: void | string;
39
+ jsxFactory: void | string;
40
+ jsxFragment: void | string;
41
+ bindings: CfWorkerInit["bindings"];
42
+ public: void | string;
43
+ site: void | string;
44
+ compatibilityDate: void | string;
45
+ compatibilityFlags: void | string[];
46
+ usageModel: void | "bundled" | "unbound";
47
+ buildCommand: {
48
+ command?: undefined | string;
49
+ cwd?: undefined | string;
50
+ watch_dir?: undefined | string;
51
+ };
38
52
  };
39
53
 
40
- export function Dev(props: Props): JSX.Element {
54
+ function Dev(props: DevProps): JSX.Element {
41
55
  if (props.public && props.format === "service-worker") {
42
56
  throw new Error(
43
57
  "You cannot use the service worker format with a `public` directory."
@@ -47,7 +61,18 @@ export function Dev(props: Props): JSX.Element {
47
61
  const apiToken = getAPIToken();
48
62
  const directory = useTmpDir();
49
63
 
50
- const bundle = useEsbuild(props.entry, directory, props.public);
64
+ // if there isn't a build command, we just return the entry immediately
65
+ // ideally there would be a conditional here, but the rules of hooks
66
+ // kinda forbid that, so we thread the entry through useCustomBuild
67
+ const entry = useCustomBuild(props.entry, props.buildCommand);
68
+
69
+ const bundle = useEsbuild({
70
+ entry,
71
+ destination: directory,
72
+ staticRoot: props.public,
73
+ jsxFactory: props.jsxFactory,
74
+ jsxFragment: props.jsxFragment,
75
+ });
51
76
  if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
52
77
  throw new Error(
53
78
  "You cannot use the service worker format with a `public` directory."
@@ -74,7 +99,7 @@ export function Dev(props: Props): JSX.Element {
74
99
  name={props.name}
75
100
  bundle={bundle}
76
101
  format={props.format}
77
- variables={props.variables}
102
+ bindings={props.bindings}
78
103
  site={props.site}
79
104
  public={props.public}
80
105
  port={props.port}
@@ -86,10 +111,13 @@ export function Dev(props: Props): JSX.Element {
86
111
  format={props.format}
87
112
  accountId={props.accountId}
88
113
  apiToken={apiToken}
89
- variables={props.variables}
114
+ bindings={props.bindings}
90
115
  site={props.site}
91
116
  public={props.public}
92
117
  port={props.port}
118
+ compatibilityDate={props.compatibilityDate}
119
+ compatibilityFlags={props.compatibilityFlags}
120
+ usageModel={props.usageModel}
93
121
  />
94
122
  )}
95
123
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
@@ -144,7 +172,10 @@ function Remote(props: {
144
172
  port: number;
145
173
  accountId: void | string;
146
174
  apiToken: void | string;
147
- variables: { [name: string]: CfVariable };
175
+ bindings: CfWorkerInit["bindings"];
176
+ compatibilityDate: string | void;
177
+ compatibilityFlags: void | string[];
178
+ usageModel: void | "bundled" | "unbound";
148
179
  }) {
149
180
  assert(props.accountId, "accountId is required");
150
181
  assert(props.apiToken, "apiToken is required");
@@ -152,12 +183,15 @@ function Remote(props: {
152
183
  name: props.name,
153
184
  bundle: props.bundle,
154
185
  format: props.format,
155
- modules: [],
186
+ modules: props.bundle ? props.bundle.modules : [],
156
187
  accountId: props.accountId,
157
188
  apiToken: props.apiToken,
158
- variables: props.variables,
189
+ bindings: props.bindings,
159
190
  sitesFolder: props.site,
160
191
  port: props.port,
192
+ compatibilityDate: props.compatibilityDate,
193
+ compatibilityFlags: props.compatibilityFlags,
194
+ usageModel: props.usageModel,
161
195
  });
162
196
 
163
197
  useProxy({ token, publicRoot: props.public, port: props.port });
@@ -169,7 +203,7 @@ function Local(props: {
169
203
  name: void | string;
170
204
  bundle: EsbuildBundle | void;
171
205
  format: CfScriptFormat;
172
- variables: { [name: string]: CfVariable };
206
+ bindings: CfWorkerInit["bindings"];
173
207
  public: void | string;
174
208
  site: void | string;
175
209
  port: number;
@@ -178,7 +212,7 @@ function Local(props: {
178
212
  name: props.name,
179
213
  bundle: props.bundle,
180
214
  format: props.format,
181
- variables: props.variables,
215
+ bindings: props.bindings,
182
216
  port: props.port,
183
217
  });
184
218
  useInspector(inspectorUrl);
@@ -189,11 +223,11 @@ function useLocalWorker(props: {
189
223
  name: void | string;
190
224
  bundle: EsbuildBundle | void;
191
225
  format: CfScriptFormat;
192
- variables: { [name: string]: CfVariable };
226
+ bindings: CfWorkerInit["bindings"];
193
227
  port: number;
194
228
  }) {
195
229
  // TODO: pass vars via command line
196
- const { bundle, format, variables, port } = props;
230
+ const { bundle, format, bindings, port } = props;
197
231
  const local = useRef<ReturnType<typeof spawn>>();
198
232
  const removeSignalExitListener = useRef<() => void>();
199
233
  const [inspectorUrl, setInspectorUrl] = useState<string | void>();
@@ -212,6 +246,7 @@ function useLocalWorker(props: {
212
246
  }
213
247
 
214
248
  console.log("⎔ Starting a local server...");
249
+ // TODO: just use execa for this
215
250
  local.current = spawn("node", [
216
251
  "--experimental-vm-modules",
217
252
  "--inspect",
@@ -228,18 +263,18 @@ function useLocalWorker(props: {
228
263
  port.toString(),
229
264
  "--kv-persist",
230
265
  "--cache-persist",
231
- ...Object.entries(variables)
232
- .map(([varKey, varVal]) => {
233
- if (typeof varVal === "string") {
234
- return `--binding ${varKey}=${varVal}`;
235
- } else if (
236
- "namespaceId" in varVal &&
237
- typeof varVal.namespaceId === "string"
238
- ) {
239
- return `--kv ${varKey}`;
240
- }
241
- })
242
- .filter(Boolean),
266
+ "--do-persist",
267
+ ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
268
+ return ["--binding", `${key}=${value}`];
269
+ }),
270
+ ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
271
+ return ["--kv", binding];
272
+ }),
273
+ ...(bindings.durable_objects?.bindings || []).flatMap(
274
+ ({ name, class_name }) => {
275
+ return ["--do", `${name}=${class_name}`];
276
+ }
277
+ ),
243
278
  "--modules",
244
279
  format ||
245
280
  (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
@@ -254,29 +289,42 @@ function useLocalWorker(props: {
254
289
  }
255
290
  });
256
291
 
257
- local.current.stdout.on("data", (data: string) => {
258
- // console.log(`stdout: ${data}`);
292
+ local.current.stdout.on("data", (data: Buffer) => {
293
+ console.log(`${data.toString()}`);
259
294
  });
260
295
 
261
- local.current.stderr.on("data", (data: string) => {
262
- // console.error(`stderr: ${data}`);
296
+ local.current.stderr.on("data", (data: Buffer) => {
297
+ console.error(`${data.toString()}`);
263
298
  const matches =
264
299
  /Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
265
- data
300
+ data.toString()
266
301
  );
267
302
  if (matches) {
268
303
  setInspectorUrl(matches[1]);
269
304
  }
270
305
  });
271
306
 
272
- removeSignalExitListener.current = onExit((code, signal) => {
307
+ local.current.on("exit", (code) => {
308
+ if (code !== 0) {
309
+ console.error(`miniflare process exited with code ${code}`);
310
+ }
311
+ });
312
+
313
+ local.current.on("error", (error: Error) => {
314
+ console.error(`miniflare process failed to spawn`);
315
+ console.error(error);
316
+ });
317
+
318
+ removeSignalExitListener.current = onExit((_code, _signal) => {
273
319
  console.log("⎔ Shutting down local server.");
274
320
  local.current?.kill();
275
321
  local.current = undefined;
276
322
  });
277
323
  }
278
324
 
279
- startLocalWorker();
325
+ startLocalWorker().catch((err) => {
326
+ console.error("local worker:", err);
327
+ });
280
328
 
281
329
  return () => {
282
330
  if (local.current) {
@@ -287,12 +335,20 @@ function useLocalWorker(props: {
287
335
  removeSignalExitListener.current = undefined;
288
336
  }
289
337
  };
290
- }, [bundle, format, port]);
338
+ }, [
339
+ bundle,
340
+ format,
341
+ port,
342
+ bindings.durable_objects?.bindings,
343
+ bindings.kv_namespaces,
344
+ bindings.vars,
345
+ ]);
291
346
  return { inspectorUrl };
292
347
  }
293
348
 
294
349
  function useTmpDir(): string | void {
295
350
  const [directory, setDirectory] = useState<DirectoryResult>();
351
+ const handleError = useErrorHandler();
296
352
  useEffect(() => {
297
353
  let dir: DirectoryResult;
298
354
  async function create() {
@@ -302,36 +358,117 @@ function useTmpDir(): string | void {
302
358
  return;
303
359
  } catch (err) {
304
360
  console.error("failed to create tmp dir");
305
- console.error(err);
306
361
  throw err;
307
362
  }
308
363
  }
309
- create();
364
+ create().catch((err) => {
365
+ // we want to break here
366
+ // we can't do much without a temp dir anyway
367
+ handleError(err);
368
+ });
310
369
  return () => {
311
- dir.cleanup();
370
+ dir.cleanup().catch(() => {
371
+ // extremely unlikely,
372
+ // but it's 2021 after all
373
+ console.error("failed to cleanup tmp dir");
374
+ });
312
375
  };
313
- }, []);
376
+ }, [handleError]);
314
377
  return directory?.path;
315
378
  }
316
379
 
380
+ function useCustomBuild(
381
+ expectedEntry: string,
382
+ props: {
383
+ command?: undefined | string;
384
+ cwd?: undefined | string;
385
+ watch_dir?: undefined | string;
386
+ }
387
+ ): void | string {
388
+ const [entry, setEntry] = useState<string | void>(
389
+ // if there's no build command, just return the expected entry
390
+ props.command ? null : expectedEntry
391
+ );
392
+ const { command, cwd, watch_dir } = props;
393
+ useEffect(() => {
394
+ if (!command) return;
395
+ let cmd, interval;
396
+ console.log("running:", command);
397
+ const commandPieces = command.split(" ");
398
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
399
+ ...(cwd && { cwd }),
400
+ stderr: "inherit",
401
+ stdout: "inherit",
402
+ });
403
+ if (watch_dir) {
404
+ watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
405
+ "all",
406
+ (_event, _path) => {
407
+ console.log(`The file ${path} changed, restarting build...`);
408
+ cmd.kill();
409
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
410
+ ...(cwd && { cwd }),
411
+ stderr: "inherit",
412
+ stdout: "inherit",
413
+ });
414
+ }
415
+ );
416
+ }
417
+
418
+ // check every so often whether `expectedEntry` exists
419
+ // if it does, we're done
420
+ const startedAt = Date.now();
421
+ interval = setInterval(() => {
422
+ if (existsSync(expectedEntry)) {
423
+ clearInterval(interval);
424
+ setEntry(expectedEntry);
425
+ } else {
426
+ const elapsed = Date.now() - startedAt;
427
+ // timeout after 30 seconds of waiting
428
+ if (elapsed > 1000 * 60 * 30) {
429
+ console.error("⎔ Build timed out.");
430
+ clearInterval(interval);
431
+ cmd.kill();
432
+ }
433
+ }
434
+ }, 200);
435
+ // TODO: we could probably timeout here after a while
436
+
437
+ return () => {
438
+ if (cmd) {
439
+ cmd.kill();
440
+ cmd = undefined;
441
+ }
442
+ clearInterval(interval);
443
+ interval = undefined;
444
+ };
445
+ }, [command, cwd, expectedEntry, watch_dir]);
446
+ return entry;
447
+ }
448
+
317
449
  type EsbuildBundle = {
318
450
  id: number;
319
451
  path: string;
320
452
  entry: string;
321
453
  type: "esm" | "commonjs";
322
454
  exports: string[];
455
+ modules: CfModule[];
323
456
  };
324
457
 
325
- function useEsbuild(
326
- entry: string,
327
- destination: string | void,
328
- staticRoot: void | string
329
- ): EsbuildBundle | void {
458
+ function useEsbuild(props: {
459
+ entry: void | string;
460
+ destination: string | void;
461
+ staticRoot: void | string;
462
+ jsxFactory: string | void;
463
+ jsxFragment: string | void;
464
+ }): EsbuildBundle | void {
465
+ const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
330
466
  const [bundle, setBundle] = useState<EsbuildBundle>();
331
467
  useEffect(() => {
332
468
  let result: esbuild.BuildResult;
333
469
  async function build() {
334
- if (!destination) return;
470
+ if (!destination || !entry) return;
471
+ const moduleCollector = makeModuleCollector();
335
472
  result = await esbuild.build({
336
473
  entryPoints: [entry],
337
474
  bundle: true,
@@ -339,7 +476,14 @@ function useEsbuild(
339
476
  metafile: true,
340
477
  format: "esm",
341
478
  sourcemap: true,
479
+ loader: {
480
+ ".js": "jsx",
481
+ },
482
+ ...(jsxFactory && { jsxFactory }),
483
+ ...(jsxFragment && { jsxFragment }),
342
484
  external: ["__STATIC_CONTENT_MANIFEST"],
485
+ conditions: ["worker", "browser"],
486
+ plugins: [moduleCollector.plugin],
343
487
  // TODO: import.meta.url
344
488
  watch: {
345
489
  async onRebuild(error) {
@@ -347,7 +491,10 @@ function useEsbuild(
347
491
  else {
348
492
  // nothing really changes here, so let's increment the id
349
493
  // to change the return object's identity
350
- setBundle((bundle) => ({ ...bundle, id: bundle.id + 1 }));
494
+ setBundle((previousBundle) => ({
495
+ ...previousBundle,
496
+ id: previousBundle.id + 1,
497
+ }));
351
498
  }
352
499
  },
353
500
  },
@@ -363,13 +510,19 @@ function useEsbuild(
363
510
  path: chunks[0],
364
511
  type: chunks[1].exports.length > 0 ? "esm" : "commonjs",
365
512
  exports: chunks[1].exports,
513
+ modules: moduleCollector.modules,
366
514
  });
367
515
  }
368
- build();
516
+ build().catch((_err) => {
517
+ // esbuild already logs errors to stderr
518
+ // and we don't want to end the process
519
+ // on build errors anyway
520
+ // so this is a no-op error handler
521
+ });
369
522
  return () => {
370
523
  result?.stop();
371
524
  };
372
- }, [entry, destination, staticRoot]);
525
+ }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
373
526
  return bundle;
374
527
  }
375
528
 
@@ -380,9 +533,12 @@ function useWorker(props: {
380
533
  modules: CfModule[];
381
534
  accountId: string;
382
535
  apiToken: string;
383
- variables: { [name: string]: CfVariable };
536
+ bindings: CfWorkerInit["bindings"];
384
537
  sitesFolder: void | string;
385
538
  port: number;
539
+ compatibilityDate: string | void;
540
+ compatibilityFlags: string[] | void;
541
+ usageModel: void | "bundled" | "unbound";
386
542
  }): CfPreviewToken | void {
387
543
  const {
388
544
  name,
@@ -391,11 +547,20 @@ function useWorker(props: {
391
547
  modules,
392
548
  accountId,
393
549
  apiToken,
394
- variables,
550
+ bindings,
395
551
  sitesFolder,
552
+ compatibilityDate,
553
+ compatibilityFlags,
554
+ usageModel,
396
555
  port,
397
556
  } = props;
398
557
  const [token, setToken] = useState<CfPreviewToken>();
558
+
559
+ // This is the most reliable way to detect whether
560
+ // something's "happened" in our system; We make a ref and
561
+ // mark it once we log our initial message. Refs are vars!
562
+ const startedRef = useRef(false);
563
+
399
564
  useEffect(() => {
400
565
  async function start() {
401
566
  if (!bundle) return;
@@ -410,17 +575,17 @@ function useWorker(props: {
410
575
  return;
411
576
  }
412
577
 
413
- if (token) {
414
- console.log("⎔ Detected changes, restarting server...");
415
- } else {
578
+ if (!startedRef.current) {
416
579
  console.log("⎔ Starting server...");
580
+ startedRef.current = true;
581
+ } else {
582
+ console.log("⎔ Detected changes, restarting server...");
417
583
  }
418
- const scriptName = path.basename(bundle.path);
419
584
 
420
585
  const assets = sitesFolder
421
586
  ? await syncAssets(
422
587
  accountId,
423
- scriptName,
588
+ path.basename(bundle.path),
424
589
  sitesFolder,
425
590
  true,
426
591
  undefined // TODO: env
@@ -432,28 +597,33 @@ function useWorker(props: {
432
597
 
433
598
  const content = await readFile(bundle.path, "utf-8");
434
599
  const init: CfWorkerInit = {
600
+ name,
435
601
  main: {
436
- name: name || path.basename(bundle.path),
437
- type:
438
- format ||
439
- (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
440
- ? "esm"
441
- : "commonjs",
602
+ name: path.basename(bundle.path),
603
+ type: format || bundle.type === "esm" ? "esm" : "commonjs",
442
604
  content,
443
605
  },
444
- modules: assets.manifest
445
- ? modules.concat({
446
- name: "__STATIC_CONTENT_MANIFEST",
447
- content: JSON.stringify(assets.manifest),
448
- type: "text",
449
- })
450
- : modules,
451
- variables: assets.namespace
452
- ? {
453
- ...variables,
454
- __STATIC_CONTENT: { namespaceId: assets.namespace },
455
- }
456
- : variables,
606
+ modules: modules.concat(
607
+ assets.manifest
608
+ ? {
609
+ name: "__STATIC_CONTENT_MANIFEST",
610
+ content: JSON.stringify(assets.manifest),
611
+ type: "text",
612
+ }
613
+ : []
614
+ ),
615
+ bindings: {
616
+ ...bindings,
617
+ kv_namespaces: (bindings.kv_namespaces || []).concat(
618
+ assets.namespace
619
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
620
+ : []
621
+ ),
622
+ },
623
+ migrations: undefined, // no migrations in dev
624
+ compatibility_date: compatibilityDate,
625
+ compatibility_flags: compatibilityFlags,
626
+ usage_model: usageModel,
457
627
  };
458
628
  setToken(
459
629
  await createWorker(init, {
@@ -461,10 +631,26 @@ function useWorker(props: {
461
631
  apiToken,
462
632
  })
463
633
  );
464
- console.log(`⬣ Listening at http://localhost:${port}`);
465
634
  }
466
- start();
467
- }, [name, bundle, format, accountId, apiToken, port, sitesFolder]);
635
+ start().catch((err) => {
636
+ // we want to log the error, but not end the process
637
+ // since it could recover after the developer fixes whatever's wrong
638
+ console.error("remote worker:", err);
639
+ });
640
+ }, [
641
+ name,
642
+ bundle,
643
+ format,
644
+ accountId,
645
+ apiToken,
646
+ port,
647
+ sitesFolder,
648
+ compatibilityDate,
649
+ compatibilityFlags,
650
+ usageModel,
651
+ bindings,
652
+ modules,
653
+ ]);
468
654
  return token;
469
655
  }
470
656
 
@@ -479,43 +665,53 @@ function useProxy({
479
665
  }) {
480
666
  useEffect(() => {
481
667
  if (!token) return;
482
- const proxy = httpProxy.createProxyServer({
483
- secure: false,
484
- changeOrigin: true,
485
- headers: {
486
- "cf-workers-preview-token": token.value,
668
+ // TODO(soon): since headers are added in callbacks, the server
669
+ // does not need to restart when changes are made.
670
+ const host = token.host;
671
+ const proxy = createHttpProxy({
672
+ host,
673
+ assetPath: typeof publicRoot === "string" ? publicRoot : null,
674
+ onRequest: (headers) => {
675
+ headers["cf-workers-preview-token"] = token.value;
676
+ },
677
+ onResponse: (headers) => {
678
+ for (const [name, value] of Object.entries(headers)) {
679
+ // Rewrite the remote host to the local host.
680
+ if (typeof value === "string" && value.includes(host)) {
681
+ headers[name] = value
682
+ .replaceAll(`https://${host}`, `http://localhost:${port}`)
683
+ .replaceAll(host, `localhost:${port}`);
684
+ }
685
+ }
487
686
  },
488
- target: `https://${token.host}`,
489
- // TODO: log websockets too? validate durables, etc
490
687
  });
491
688
 
492
- const servePublic =
493
- publicRoot &&
494
- serveStatic(publicRoot, {
495
- cacheControl: false,
496
- });
497
- const server = http
498
- .createServer((req, res) => {
499
- if (publicRoot) {
500
- servePublic(req, res, () => {
501
- proxy.web(req, res);
502
- });
503
- } else {
504
- proxy.web(req, res);
505
- }
506
- })
507
- .listen(port); // TODO: custom port
689
+ console.log(`⬣ Listening at http://localhost:${port}`);
690
+
691
+ const server = proxy.listen(port);
508
692
 
509
- proxy.on("proxyRes", function (proxyRes, req, res) {
693
+ // TODO(soon): refactor logging format into its own function
694
+ proxy.on("request", function (req, res) {
510
695
  // log all requests
511
696
  console.log(
512
697
  new Date().toLocaleTimeString(),
513
698
  req.method,
514
699
  req.url,
515
- res.statusCode // TODO add a status message like Ok etc?
700
+ res.statusCode
701
+ );
702
+ });
703
+ proxy.on("upgrade", (req) => {
704
+ console.log(
705
+ new Date().toLocaleTimeString(),
706
+ req.method,
707
+ req.url,
708
+ 101,
709
+ "(WebSocket)"
516
710
  );
517
711
  });
518
- // TODO: log errors?
712
+ proxy.on("error", (err) => {
713
+ console.error(new Date().toLocaleTimeString(), err);
714
+ });
519
715
 
520
716
  return () => {
521
717
  proxy.close();
@@ -589,19 +785,21 @@ function useTunnel(toggle: boolean) {
589
785
  }
590
786
  });
591
787
 
592
- removeSignalExitListener.current = onExit((code, signal) => {
788
+ removeSignalExitListener.current = onExit((_code, _signal) => {
593
789
  console.log("⎔ Shutting down local tunnel.");
594
790
  tunnel.current?.kill();
595
791
  tunnel.current = undefined;
596
792
  });
597
793
 
598
794
  const hostName = await findTunnelHostname();
599
- clipboardy.write(hostName);
795
+ await clipboardy.write(hostName);
600
796
  console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
601
797
  }
602
798
  }
603
799
 
604
- startTunnel();
800
+ startTunnel().catch((err) => {
801
+ console.error("tunnel:", err);
802
+ });
605
803
 
606
804
  return () => {
607
805
  if (tunnel.current) {
@@ -623,38 +821,31 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
623
821
  // UGH, we should put port in context instead
624
822
  const [toggles, setToggles] = useState(initial);
625
823
  useInput(
626
- (
824
+ async (
627
825
  input,
628
826
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
629
827
  key
630
828
  ) => {
631
829
  switch (input) {
632
830
  case "b": // open browser
633
- open(
634
- `http://localhost:${port}/`
635
- // {
636
- // app: {
637
- // name: open.apps.chrome, // TODO: fallback on other browsers
638
- // },
639
- // }
640
- );
831
+ await open(`http://localhost:${port}/`);
641
832
  break;
642
833
  case "d": // toggle inspector
643
- open(
834
+ await open(
644
835
  `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
645
- // {
646
- // app: {
647
- // name: open.apps.chrome,
648
- // // todo - add firefox and edge fallbacks
649
- // },
650
- // }
651
836
  );
652
837
  break;
653
838
  case "s": // toggle tunnel
654
- setToggles((toggles) => ({ ...toggles, tunnel: !toggles.tunnel }));
839
+ setToggles((previousToggles) => ({
840
+ ...previousToggles,
841
+ tunnel: !previousToggles.tunnel,
842
+ }));
655
843
  break;
656
844
  case "l": // toggle local
657
- setToggles((toggles) => ({ ...toggles, local: !toggles.local }));
845
+ setToggles((previousToggles) => ({
846
+ ...previousToggles,
847
+ local: !previousToggles.local,
848
+ }));
658
849
  break;
659
850
  case "q": // shut down
660
851
  case "x": // shut down
@@ -668,3 +859,16 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
668
859
  );
669
860
  return toggles;
670
861
  }
862
+
863
+ function ErrorFallback(props: { error: Error }) {
864
+ const { exit } = useApp();
865
+ useEffect(() => exit(new Error()));
866
+ return (
867
+ <>
868
+ <Text>Something went wrong:</Text>
869
+ <Text>{props.error.message}</Text>
870
+ </>
871
+ );
872
+ }
873
+
874
+ export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback });