wrangler 0.0.0-ece06ea → 0.0.0-edc4b53

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/package.json +24 -15
  2. package/pages/functions/buildWorker.ts +62 -0
  3. package/pages/functions/filepath-routing.test.ts +39 -0
  4. package/pages/functions/filepath-routing.ts +221 -0
  5. package/pages/functions/identifiers.ts +78 -0
  6. package/pages/functions/routes.ts +158 -0
  7. package/pages/functions/template-worker.ts +144 -0
  8. package/src/__tests__/dev.test.tsx +66 -0
  9. package/src/__tests__/index.test.ts +254 -120
  10. package/src/__tests__/jest.setup.ts +22 -0
  11. package/src/__tests__/kv.test.ts +1106 -0
  12. package/src/__tests__/mock-cfetch.ts +171 -0
  13. package/src/__tests__/mock-dialogs.ts +65 -0
  14. package/src/__tests__/run-in-tmp.ts +19 -0
  15. package/src/__tests__/run-wrangler.ts +32 -0
  16. package/src/api/form_data.ts +71 -98
  17. package/src/api/preview.ts +29 -24
  18. package/src/api/worker.ts +38 -44
  19. package/src/cfetch/index.ts +102 -0
  20. package/src/cfetch/internal.ts +65 -0
  21. package/src/cli.ts +6 -7
  22. package/src/config.ts +476 -113
  23. package/src/dev.tsx +291 -256
  24. package/src/dialogs.tsx +4 -12
  25. package/src/index.tsx +381 -252
  26. package/src/inspect.ts +524 -0
  27. package/src/kv.tsx +110 -45
  28. package/src/module-collection.ts +64 -0
  29. package/src/pages.tsx +900 -213
  30. package/src/proxy.ts +294 -0
  31. package/src/publish.ts +67 -63
  32. package/src/sites.tsx +10 -7
  33. package/src/tail.tsx +5 -3
  34. package/src/user.tsx +31 -33
  35. package/wrangler-dist/cli.js +43444 -56347
  36. package/wrangler-dist/cli.js.map +3 -3
  37. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  38. package/src/__tests__/mock-cfetch.js +0 -46
  39. package/src/api/inspect.ts +0 -441
  40. package/src/cfetch.ts +0 -72
  41. package/src/util/fetch.ts +0 -74
package/src/dev.tsx CHANGED
@@ -1,51 +1,56 @@
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
- import { DtInspector } from "./api/inspect";
12
- import type { CfModule, CfVariable } from "./api/worker";
11
+ import useInspector from "./inspect";
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";
16
16
  import onExit from "signal-exit";
17
17
  import { syncAssets } from "./sites";
18
18
  import clipboardy from "clipboardy";
19
- import http from "node:http";
20
- import serveStatic from "serve-static";
21
19
  import commandExists from "command-exists";
22
20
  import assert from "assert";
23
21
  import { getAPIToken } from "./user";
24
22
  import fetch from "node-fetch";
25
- import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
26
- import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
23
+ import makeModuleCollector from "./module-collection";
24
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
25
+ import { usePreviewServer } from "./proxy";
26
+ import { execa } from "execa";
27
+ import { watch } from "chokidar";
27
28
 
28
- type CfScriptFormat = void | "modules" | "service-worker";
29
+ type CfScriptFormat = undefined | "modules" | "service-worker";
29
30
 
30
- type Props = {
31
+ export type DevProps = {
31
32
  name?: string;
32
33
  entry: string;
33
34
  port?: number;
34
35
  format: CfScriptFormat;
35
- accountId: void | string;
36
+ accountId: undefined | string;
36
37
  initialMode: "local" | "remote";
37
- jsxFactory: void | string;
38
- jsxFragment: void | string;
39
- variables: { [name: string]: CfVariable };
40
- public: void | string;
41
- site: void | string;
42
- compatibilityDate: void | string;
43
- compatibilityFlags: void | string[];
44
- usageModel: void | "bundled" | "unbound";
45
- polyfillNode: void | boolean;
38
+ jsxFactory: undefined | string;
39
+ jsxFragment: undefined | string;
40
+ bindings: CfWorkerInit["bindings"];
41
+ public: undefined | string;
42
+ site: undefined | string;
43
+ compatibilityDate: undefined | string;
44
+ compatibilityFlags: undefined | string[];
45
+ usageModel: undefined | "bundled" | "unbound";
46
+ buildCommand: {
47
+ command?: undefined | string;
48
+ cwd?: undefined | string;
49
+ watch_dir?: undefined | string;
50
+ };
46
51
  };
47
52
 
48
- export function Dev(props: Props): JSX.Element {
53
+ function Dev(props: DevProps): JSX.Element {
49
54
  if (props.public && props.format === "service-worker") {
50
55
  throw new Error(
51
56
  "You cannot use the service worker format with a `public` directory."
@@ -55,13 +60,17 @@ export function Dev(props: Props): JSX.Element {
55
60
  const apiToken = getAPIToken();
56
61
  const directory = useTmpDir();
57
62
 
63
+ // if there isn't a build command, we just return the entry immediately
64
+ // ideally there would be a conditional here, but the rules of hooks
65
+ // kinda forbid that, so we thread the entry through useCustomBuild
66
+ const entry = useCustomBuild(props.entry, props.buildCommand);
67
+
58
68
  const bundle = useEsbuild({
59
- entry: props.entry,
69
+ entry,
60
70
  destination: directory,
61
71
  staticRoot: props.public,
62
72
  jsxFactory: props.jsxFactory,
63
73
  jsxFragment: props.jsxFragment,
64
- polyfillNode: props.polyfillNode,
65
74
  });
66
75
  if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
67
76
  throw new Error(
@@ -69,9 +78,6 @@ export function Dev(props: Props): JSX.Element {
69
78
  );
70
79
  }
71
80
 
72
- // @ts-expect-error whack
73
- useDevtoolsRefresh(bundle?.id ?? 0);
74
-
75
81
  const toggles = useHotkeys(
76
82
  {
77
83
  local: props.initialMode === "local",
@@ -89,7 +95,7 @@ export function Dev(props: Props): JSX.Element {
89
95
  name={props.name}
90
96
  bundle={bundle}
91
97
  format={props.format}
92
- variables={props.variables}
98
+ bindings={props.bindings}
93
99
  site={props.site}
94
100
  public={props.public}
95
101
  port={props.port}
@@ -101,7 +107,7 @@ export function Dev(props: Props): JSX.Element {
101
107
  format={props.format}
102
108
  accountId={props.accountId}
103
109
  apiToken={apiToken}
104
- variables={props.variables}
110
+ bindings={props.bindings}
105
111
  site={props.site}
106
112
  public={props.public}
107
113
  port={props.port}
@@ -123,60 +129,30 @@ export function Dev(props: Props): JSX.Element {
123
129
  );
124
130
  }
125
131
 
126
- function useDevtoolsRefresh(bundleId: number) {
127
- // TODO: this is a hack while we figure out
128
- // a better cleaner solution to get devtools to reconnect
129
- // without having to do a full refresh
130
- const ref = useRef();
131
- // @ts-expect-error whack
132
- ref.current = bundleId;
133
-
134
- useEffect(() => {
135
- const server = http.createServer((req, res) => {
136
- res.setHeader("Access-Control-Allow-Origin", "*");
137
- res.setHeader("Access-Control-Request-Method", "*");
138
- res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET");
139
- res.setHeader("Access-Control-Allow-Headers", "*");
140
- if (req.method === "OPTIONS") {
141
- res.writeHead(200);
142
- res.end();
143
- return;
144
- }
145
- res.writeHead(200, { "Content-Type": "application/json" });
146
- res.end(JSON.stringify({ value: ref.current }));
147
- });
148
-
149
- server.listen(3142);
150
- return () => {
151
- server.close();
152
- };
153
- }, []);
154
- }
155
-
156
132
  function Remote(props: {
157
- name: void | string;
158
- bundle: EsbuildBundle | void;
133
+ name: undefined | string;
134
+ bundle: EsbuildBundle | undefined;
159
135
  format: CfScriptFormat;
160
- public: void | string;
161
- site: void | string;
136
+ public: undefined | string;
137
+ site: undefined | string;
162
138
  port: number;
163
- accountId: void | string;
164
- apiToken: void | string;
165
- variables: { [name: string]: CfVariable };
166
- compatibilityDate: string | void;
167
- compatibilityFlags: void | string[];
168
- usageModel: void | "bundled" | "unbound";
139
+ accountId: undefined | string;
140
+ apiToken: undefined | string;
141
+ bindings: CfWorkerInit["bindings"];
142
+ compatibilityDate: string | undefined;
143
+ compatibilityFlags: undefined | string[];
144
+ usageModel: undefined | "bundled" | "unbound";
169
145
  }) {
170
146
  assert(props.accountId, "accountId is required");
171
147
  assert(props.apiToken, "apiToken is required");
172
- const token = useWorker({
148
+ const previewToken = useWorker({
173
149
  name: props.name,
174
150
  bundle: props.bundle,
175
151
  format: props.format,
176
- modules: [],
152
+ modules: props.bundle ? props.bundle.modules : [],
177
153
  accountId: props.accountId,
178
154
  apiToken: props.apiToken,
179
- variables: props.variables,
155
+ bindings: props.bindings,
180
156
  sitesFolder: props.site,
181
157
  port: props.port,
182
158
  compatibilityDate: props.compatibilityDate,
@@ -184,43 +160,51 @@ function Remote(props: {
184
160
  usageModel: props.usageModel,
185
161
  });
186
162
 
187
- useProxy({ token, publicRoot: props.public, port: props.port });
163
+ usePreviewServer({
164
+ previewToken,
165
+ publicRoot: props.public,
166
+ port: props.port,
167
+ });
188
168
 
189
- useInspector(token ? token.inspectorUrl.href : undefined);
169
+ useInspector({
170
+ inspectorUrl: previewToken ? previewToken.inspectorUrl.href : undefined,
171
+ port: 9229,
172
+ logToTerminal: true,
173
+ });
190
174
  return null;
191
175
  }
192
176
  function Local(props: {
193
- name: void | string;
194
- bundle: EsbuildBundle | void;
177
+ name: undefined | string;
178
+ bundle: EsbuildBundle | undefined;
195
179
  format: CfScriptFormat;
196
- variables: { [name: string]: CfVariable };
197
- public: void | string;
198
- site: void | string;
180
+ bindings: CfWorkerInit["bindings"];
181
+ public: undefined | string;
182
+ site: undefined | string;
199
183
  port: number;
200
184
  }) {
201
185
  const { inspectorUrl } = useLocalWorker({
202
186
  name: props.name,
203
187
  bundle: props.bundle,
204
188
  format: props.format,
205
- variables: props.variables,
189
+ bindings: props.bindings,
206
190
  port: props.port,
207
191
  });
208
- useInspector(inspectorUrl);
192
+ useInspector({ inspectorUrl, port: 9229, logToTerminal: false });
209
193
  return null;
210
194
  }
211
195
 
212
196
  function useLocalWorker(props: {
213
- name: void | string;
214
- bundle: EsbuildBundle | void;
197
+ name: undefined | string;
198
+ bundle: EsbuildBundle | undefined;
215
199
  format: CfScriptFormat;
216
- variables: { [name: string]: CfVariable };
200
+ bindings: CfWorkerInit["bindings"];
217
201
  port: number;
218
202
  }) {
219
203
  // TODO: pass vars via command line
220
- const { bundle, format, variables, port } = props;
204
+ const { bundle, format, bindings, port } = props;
221
205
  const local = useRef<ReturnType<typeof spawn>>();
222
206
  const removeSignalExitListener = useRef<() => void>();
223
- const [inspectorUrl, setInspectorUrl] = useState<string | void>();
207
+ const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
224
208
  useEffect(() => {
225
209
  async function startLocalWorker() {
226
210
  if (!bundle) return;
@@ -236,6 +220,7 @@ function useLocalWorker(props: {
236
220
  }
237
221
 
238
222
  console.log("⎔ Starting a local server...");
223
+ // TODO: just use execa for this
239
224
  local.current = spawn("node", [
240
225
  "--experimental-vm-modules",
241
226
  "--inspect",
@@ -253,20 +238,17 @@ function useLocalWorker(props: {
253
238
  "--kv-persist",
254
239
  "--cache-persist",
255
240
  "--do-persist",
256
- ...Object.entries(variables)
257
- .map(([varKey, varVal]) => {
258
- if (typeof varVal === "string") {
259
- return `--binding ${varKey}=${varVal}`;
260
- } else if (
261
- "namespaceId" in varVal &&
262
- typeof varVal.namespaceId === "string"
263
- ) {
264
- return `--kv ${varKey}`;
265
- } else if ("class_name" in varVal) {
266
- return `--do ${varKey}=${varVal.class_name}`;
267
- }
268
- })
269
- .filter(Boolean),
241
+ ...Object.entries(bindings.vars || {}).flatMap(([key, value]) => {
242
+ return ["--binding", `${key}=${value}`];
243
+ }),
244
+ ...(bindings.kv_namespaces || []).flatMap(({ binding }) => {
245
+ return ["--kv", binding];
246
+ }),
247
+ ...(bindings.durable_objects?.bindings || []).flatMap(
248
+ ({ name, class_name }) => {
249
+ return ["--do", `${name}=${class_name}`];
250
+ }
251
+ ),
270
252
  "--modules",
271
253
  format ||
272
254
  (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
@@ -281,21 +263,32 @@ function useLocalWorker(props: {
281
263
  }
282
264
  });
283
265
 
284
- local.current.stdout.on("data", (_data: string) => {
285
- // console.log(`stdout: ${data}`);
266
+ local.current.stdout.on("data", (data: Buffer) => {
267
+ console.log(`${data.toString()}`);
286
268
  });
287
269
 
288
- local.current.stderr.on("data", (data: string) => {
289
- // console.error(`stderr: ${data}`);
270
+ local.current.stderr.on("data", (data: Buffer) => {
271
+ console.error(`${data.toString()}`);
290
272
  const matches =
291
273
  /Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
292
- data
274
+ data.toString()
293
275
  );
294
276
  if (matches) {
295
277
  setInspectorUrl(matches[1]);
296
278
  }
297
279
  });
298
280
 
281
+ local.current.on("exit", (code) => {
282
+ if (code !== 0) {
283
+ console.error(`miniflare process exited with code ${code}`);
284
+ }
285
+ });
286
+
287
+ local.current.on("error", (error: Error) => {
288
+ console.error(`miniflare process failed to spawn`);
289
+ console.error(error);
290
+ });
291
+
299
292
  removeSignalExitListener.current = onExit((_code, _signal) => {
300
293
  console.log("⎔ Shutting down local server.");
301
294
  local.current?.kill();
@@ -303,7 +296,9 @@ function useLocalWorker(props: {
303
296
  });
304
297
  }
305
298
 
306
- startLocalWorker();
299
+ startLocalWorker().catch((err) => {
300
+ console.error("local worker:", err);
301
+ });
307
302
 
308
303
  return () => {
309
304
  if (local.current) {
@@ -314,12 +309,20 @@ function useLocalWorker(props: {
314
309
  removeSignalExitListener.current = undefined;
315
310
  }
316
311
  };
317
- }, [bundle, format, port]);
312
+ }, [
313
+ bundle,
314
+ format,
315
+ port,
316
+ bindings.durable_objects?.bindings,
317
+ bindings.kv_namespaces,
318
+ bindings.vars,
319
+ ]);
318
320
  return { inspectorUrl };
319
321
  }
320
322
 
321
- function useTmpDir(): string | void {
323
+ function useTmpDir(): string | undefined {
322
324
  const [directory, setDirectory] = useState<DirectoryResult>();
325
+ const handleError = useErrorHandler();
323
326
  useEffect(() => {
324
327
  let dir: DirectoryResult;
325
328
  async function create() {
@@ -329,52 +332,119 @@ function useTmpDir(): string | void {
329
332
  return;
330
333
  } catch (err) {
331
334
  console.error("failed to create tmp dir");
332
- console.error(err);
333
335
  throw err;
334
336
  }
335
337
  }
336
- create();
338
+ create().catch((err) => {
339
+ // we want to break here
340
+ // we can't do much without a temp dir anyway
341
+ handleError(err);
342
+ });
337
343
  return () => {
338
- dir.cleanup();
344
+ dir.cleanup().catch(() => {
345
+ // extremely unlikely,
346
+ // but it's 2021 after all
347
+ console.error("failed to cleanup tmp dir");
348
+ });
339
349
  };
340
- }, []);
350
+ }, [handleError]);
341
351
  return directory?.path;
342
352
  }
343
353
 
354
+ function useCustomBuild(
355
+ expectedEntry: string,
356
+ props: {
357
+ command?: undefined | string;
358
+ cwd?: undefined | string;
359
+ watch_dir?: undefined | string;
360
+ }
361
+ ): undefined | string {
362
+ const [entry, setEntry] = useState<string | undefined>(
363
+ // if there's no build command, just return the expected entry
364
+ props.command ? null : expectedEntry
365
+ );
366
+ const { command, cwd, watch_dir } = props;
367
+ useEffect(() => {
368
+ if (!command) return;
369
+ let cmd, interval;
370
+ console.log("running:", command);
371
+ const commandPieces = command.split(" ");
372
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
373
+ ...(cwd && { cwd }),
374
+ stderr: "inherit",
375
+ stdout: "inherit",
376
+ });
377
+ if (watch_dir) {
378
+ watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
379
+ "all",
380
+ (_event, _path) => {
381
+ console.log(`The file ${path} changed, restarting build...`);
382
+ cmd.kill();
383
+ cmd = execa(commandPieces[0], commandPieces.slice(1), {
384
+ ...(cwd && { cwd }),
385
+ stderr: "inherit",
386
+ stdout: "inherit",
387
+ });
388
+ }
389
+ );
390
+ }
391
+
392
+ // check every so often whether `expectedEntry` exists
393
+ // if it does, we're done
394
+ const startedAt = Date.now();
395
+ interval = setInterval(() => {
396
+ if (existsSync(expectedEntry)) {
397
+ clearInterval(interval);
398
+ setEntry(expectedEntry);
399
+ } else {
400
+ const elapsed = Date.now() - startedAt;
401
+ // timeout after 30 seconds of waiting
402
+ if (elapsed > 1000 * 60 * 30) {
403
+ console.error("⎔ Build timed out.");
404
+ clearInterval(interval);
405
+ cmd.kill();
406
+ }
407
+ }
408
+ }, 200);
409
+ // TODO: we could probably timeout here after a while
410
+
411
+ return () => {
412
+ if (cmd) {
413
+ cmd.kill();
414
+ cmd = undefined;
415
+ }
416
+ clearInterval(interval);
417
+ interval = undefined;
418
+ };
419
+ }, [command, cwd, expectedEntry, watch_dir]);
420
+ return entry;
421
+ }
422
+
344
423
  type EsbuildBundle = {
345
424
  id: number;
346
425
  path: string;
347
426
  entry: string;
348
427
  type: "esm" | "commonjs";
349
428
  exports: string[];
429
+ modules: CfModule[];
350
430
  };
351
431
 
352
432
  function useEsbuild(props: {
353
- entry: string;
354
- destination: string | void;
355
- staticRoot: void | string;
356
- jsxFactory: string | void;
357
- jsxFragment: string | void;
358
- polyfillNode: boolean | void;
359
- }): EsbuildBundle | void {
360
- const {
361
- entry,
362
- destination,
363
- staticRoot,
364
- jsxFactory,
365
- jsxFragment,
366
- polyfillNode,
367
- } = props;
433
+ entry: undefined | string;
434
+ destination: string | undefined;
435
+ staticRoot: undefined | string;
436
+ jsxFactory: string | undefined;
437
+ jsxFragment: string | undefined;
438
+ }): EsbuildBundle | undefined {
439
+ const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
368
440
  const [bundle, setBundle] = useState<EsbuildBundle>();
369
441
  useEffect(() => {
370
442
  let result: esbuild.BuildResult;
371
443
  async function build() {
372
- if (!destination) return;
444
+ if (!destination || !entry) return;
445
+ const moduleCollector = makeModuleCollector();
373
446
  result = await esbuild.build({
374
447
  entryPoints: [entry],
375
- define: {
376
- ...(polyfillNode && { global: "globalThis" }),
377
- },
378
448
  bundle: true,
379
449
  outdir: destination,
380
450
  metafile: true,
@@ -383,12 +453,11 @@ function useEsbuild(props: {
383
453
  loader: {
384
454
  ".js": "jsx",
385
455
  },
386
- plugins: polyfillNode
387
- ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
388
- : undefined,
389
456
  ...(jsxFactory && { jsxFactory }),
390
457
  ...(jsxFragment && { jsxFragment }),
391
458
  external: ["__STATIC_CONTENT_MANIFEST"],
459
+ conditions: ["worker", "browser"],
460
+ plugins: [moduleCollector.plugin],
392
461
  // TODO: import.meta.url
393
462
  watch: {
394
463
  async onRebuild(error) {
@@ -396,14 +465,18 @@ function useEsbuild(props: {
396
465
  else {
397
466
  // nothing really changes here, so let's increment the id
398
467
  // to change the return object's identity
399
- setBundle((bundle) => ({ ...bundle, id: bundle.id + 1 }));
468
+ setBundle((previousBundle) => ({
469
+ ...previousBundle,
470
+ id: previousBundle.id + 1,
471
+ }));
400
472
  }
401
473
  },
402
474
  },
403
475
  });
404
476
 
405
477
  const chunks = Object.entries(result.metafile.outputs).find(
406
- ([_path, { entryPoint }]) => entryPoint === entry
478
+ ([_path, { entryPoint }]) =>
479
+ entryPoint === Object.keys(result.metafile.inputs)[0]
407
480
  ); // assumedly only one entry point
408
481
 
409
482
  setBundle({
@@ -412,30 +485,36 @@ function useEsbuild(props: {
412
485
  path: chunks[0],
413
486
  type: chunks[1].exports.length > 0 ? "esm" : "commonjs",
414
487
  exports: chunks[1].exports,
488
+ modules: moduleCollector.modules,
415
489
  });
416
490
  }
417
- build();
491
+ build().catch((_err) => {
492
+ // esbuild already logs errors to stderr
493
+ // and we don't want to end the process
494
+ // on build errors anyway
495
+ // so this is a no-op error handler
496
+ });
418
497
  return () => {
419
498
  result?.stop();
420
499
  };
421
- }, [entry, destination, staticRoot, jsxFactory, jsxFragment, polyfillNode]);
500
+ }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
422
501
  return bundle;
423
502
  }
424
503
 
425
504
  function useWorker(props: {
426
- name: void | string;
427
- bundle: EsbuildBundle | void;
505
+ name: undefined | string;
506
+ bundle: EsbuildBundle | undefined;
428
507
  format: CfScriptFormat;
429
508
  modules: CfModule[];
430
509
  accountId: string;
431
510
  apiToken: string;
432
- variables: { [name: string]: CfVariable };
433
- sitesFolder: void | string;
511
+ bindings: CfWorkerInit["bindings"];
512
+ sitesFolder: undefined | string;
434
513
  port: number;
435
- compatibilityDate: string | void;
436
- compatibilityFlags: string[] | void;
437
- usageModel: void | "bundled" | "unbound";
438
- }): CfPreviewToken | void {
514
+ compatibilityDate: string | undefined;
515
+ compatibilityFlags: string[] | undefined;
516
+ usageModel: undefined | "bundled" | "unbound";
517
+ }): CfPreviewToken | undefined {
439
518
  const {
440
519
  name,
441
520
  bundle,
@@ -443,16 +522,24 @@ function useWorker(props: {
443
522
  modules,
444
523
  accountId,
445
524
  apiToken,
446
- variables,
525
+ bindings,
447
526
  sitesFolder,
448
527
  compatibilityDate,
449
528
  compatibilityFlags,
450
529
  usageModel,
451
530
  port,
452
531
  } = props;
453
- const [token, setToken] = useState<CfPreviewToken>();
532
+ const [token, setToken] = useState<CfPreviewToken | undefined>();
533
+
534
+ // This is the most reliable way to detect whether
535
+ // something's "happened" in our system; We make a ref and
536
+ // mark it once we log our initial message. Refs are vars!
537
+ const startedRef = useRef(false);
538
+
454
539
  useEffect(() => {
455
540
  async function start() {
541
+ setToken(undefined); // reset token in case we're re-running
542
+
456
543
  if (!bundle) return;
457
544
  if (format === "modules" && bundle.type === "commonjs") {
458
545
  console.error("⎔ Cannot use modules with a commonjs bundle.");
@@ -465,10 +552,10 @@ function useWorker(props: {
465
552
  return;
466
553
  }
467
554
 
468
- if (token) {
469
- console.log("⎔ Detected changes, restarting server...");
555
+ if (!startedRef.current) {
556
+ startedRef.current = true;
470
557
  } else {
471
- console.log("⎔ Starting server...");
558
+ console.log("⎔ Detected changes, restarting server...");
472
559
  }
473
560
 
474
561
  const assets = sitesFolder
@@ -486,25 +573,29 @@ function useWorker(props: {
486
573
 
487
574
  const content = await readFile(bundle.path, "utf-8");
488
575
  const init: CfWorkerInit = {
489
- name: name || path.basename(bundle.path),
576
+ name,
490
577
  main: {
491
578
  name: path.basename(bundle.path),
492
579
  type: format || bundle.type === "esm" ? "esm" : "commonjs",
493
580
  content,
494
581
  },
495
- modules: assets.manifest
496
- ? modules.concat({
497
- name: "__STATIC_CONTENT_MANIFEST",
498
- content: JSON.stringify(assets.manifest),
499
- type: "text",
500
- })
501
- : modules,
502
- variables: assets.namespace
503
- ? {
504
- ...variables,
505
- __STATIC_CONTENT: { namespaceId: assets.namespace },
506
- }
507
- : variables,
582
+ modules: modules.concat(
583
+ assets.manifest
584
+ ? {
585
+ name: "__STATIC_CONTENT_MANIFEST",
586
+ content: JSON.stringify(assets.manifest),
587
+ type: "text",
588
+ }
589
+ : []
590
+ ),
591
+ bindings: {
592
+ ...bindings,
593
+ kv_namespaces: (bindings.kv_namespaces || []).concat(
594
+ assets.namespace
595
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
596
+ : []
597
+ ),
598
+ },
508
599
  migrations: undefined, // no migrations in dev
509
600
  compatibility_date: compatibilityDate,
510
601
  compatibility_flags: compatibilityFlags,
@@ -516,9 +607,12 @@ function useWorker(props: {
516
607
  apiToken,
517
608
  })
518
609
  );
519
- console.log(`⬣ Listening at http://localhost:${port}`);
520
610
  }
521
- start();
611
+ start().catch((err) => {
612
+ // we want to log the error, but not end the process
613
+ // since it could recover after the developer fixes whatever's wrong
614
+ console.error("remote worker:", err);
615
+ });
522
616
  }, [
523
617
  name,
524
618
  bundle,
@@ -530,79 +624,12 @@ function useWorker(props: {
530
624
  compatibilityDate,
531
625
  compatibilityFlags,
532
626
  usageModel,
627
+ bindings,
628
+ modules,
533
629
  ]);
534
630
  return token;
535
631
  }
536
632
 
537
- function useProxy({
538
- token,
539
- publicRoot,
540
- port,
541
- }: {
542
- token: CfPreviewToken | void;
543
- publicRoot: void | string;
544
- port: number;
545
- }) {
546
- useEffect(() => {
547
- if (!token) return;
548
- const proxy = httpProxy.createProxyServer({
549
- secure: false,
550
- changeOrigin: true,
551
- headers: {
552
- "cf-workers-preview-token": token.value,
553
- },
554
- target: `https://${token.host}`,
555
- // TODO: log websockets too? validate durables, etc
556
- });
557
-
558
- const servePublic =
559
- publicRoot &&
560
- serveStatic(publicRoot, {
561
- cacheControl: false,
562
- });
563
- const server = http
564
- .createServer((req, res) => {
565
- if (publicRoot) {
566
- servePublic(req, res, () => {
567
- proxy.web(req, res);
568
- });
569
- } else {
570
- proxy.web(req, res);
571
- }
572
- })
573
- .listen(port); // TODO: custom port
574
-
575
- proxy.on("proxyRes", function (proxyRes, req, res) {
576
- // log all requests
577
- console.log(
578
- new Date().toLocaleTimeString(),
579
- req.method,
580
- req.url,
581
- res.statusCode // TODO add a status message like Ok etc?
582
- );
583
- });
584
- // TODO: log errors?
585
-
586
- return () => {
587
- proxy.close();
588
- server.close();
589
- };
590
- }, [token, publicRoot, port]);
591
- }
592
-
593
- function useInspector(inspectorUrl: string | void) {
594
- useEffect(() => {
595
- if (!inspectorUrl) return;
596
-
597
- const inspector = new DtInspector(inspectorUrl);
598
- const abortController = inspector.proxyTo(9229);
599
- return () => {
600
- inspector.close();
601
- abortController.abort();
602
- };
603
- }, [inspectorUrl]);
604
- }
605
-
606
633
  function sleep(period: number) {
607
634
  return new Promise((resolve) => setTimeout(resolve, period));
608
635
  }
@@ -610,7 +637,7 @@ const SLEEP_DURATION = 2000;
610
637
  // really need a first class api for this
611
638
  const hostNameRegex = /userHostname="(.*)"/g;
612
639
  async function findTunnelHostname() {
613
- let hostName: string;
640
+ let hostName: string | undefined;
614
641
  while (!hostName) {
615
642
  try {
616
643
  const resp = await fetch("http://localhost:8789/metrics");
@@ -662,12 +689,14 @@ function useTunnel(toggle: boolean) {
662
689
  });
663
690
 
664
691
  const hostName = await findTunnelHostname();
665
- clipboardy.write(hostName);
692
+ await clipboardy.write(hostName);
666
693
  console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
667
694
  }
668
695
  }
669
696
 
670
- startTunnel();
697
+ startTunnel().catch((err) => {
698
+ console.error("tunnel:", err);
699
+ });
671
700
 
672
701
  return () => {
673
702
  if (tunnel.current) {
@@ -689,38 +718,31 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
689
718
  // UGH, we should put port in context instead
690
719
  const [toggles, setToggles] = useState(initial);
691
720
  useInput(
692
- (
721
+ async (
693
722
  input,
694
723
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
695
724
  key
696
725
  ) => {
697
726
  switch (input) {
698
727
  case "b": // open browser
699
- open(
700
- `http://localhost:${port}/`
701
- // {
702
- // app: {
703
- // name: open.apps.chrome, // TODO: fallback on other browsers
704
- // },
705
- // }
706
- );
728
+ await open(`http://localhost:${port}/`);
707
729
  break;
708
730
  case "d": // toggle inspector
709
- open(
731
+ await open(
710
732
  `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:9229/ws`
711
- // {
712
- // app: {
713
- // name: open.apps.chrome,
714
- // // todo - add firefox and edge fallbacks
715
- // },
716
- // }
717
733
  );
718
734
  break;
719
735
  case "s": // toggle tunnel
720
- setToggles((toggles) => ({ ...toggles, tunnel: !toggles.tunnel }));
736
+ setToggles((previousToggles) => ({
737
+ ...previousToggles,
738
+ tunnel: !previousToggles.tunnel,
739
+ }));
721
740
  break;
722
741
  case "l": // toggle local
723
- setToggles((toggles) => ({ ...toggles, local: !toggles.local }));
742
+ setToggles((previousToggles) => ({
743
+ ...previousToggles,
744
+ local: !previousToggles.local,
745
+ }));
724
746
  break;
725
747
  case "q": // shut down
726
748
  case "x": // shut down
@@ -734,3 +756,16 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
734
756
  );
735
757
  return toggles;
736
758
  }
759
+
760
+ function ErrorFallback(props: { error: Error }) {
761
+ const { exit } = useApp();
762
+ useEffect(() => exit(new Error()));
763
+ return (
764
+ <>
765
+ <Text>Something went wrong:</Text>
766
+ <Text>{props.error.message}</Text>
767
+ </>
768
+ );
769
+ }
770
+
771
+ export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback });