wrangler 0.0.0-e14e589 → 0.0.0-e2a26a0

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 (61) hide show
  1. package/README.md +51 -0
  2. package/bin/wrangler.js +2 -2
  3. package/package.json +49 -30
  4. package/pages/functions/buildWorker.ts +62 -0
  5. package/pages/functions/filepath-routing.test.ts +47 -0
  6. package/pages/functions/filepath-routing.ts +231 -0
  7. package/pages/functions/identifiers.ts +78 -0
  8. package/pages/functions/routes.ts +160 -0
  9. package/pages/functions/template-worker.ts +136 -0
  10. package/src/__tests__/dev.test.tsx +71 -0
  11. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  12. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  13. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  14. package/src/__tests__/helpers/mock-bin.ts +33 -0
  15. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  16. package/src/__tests__/helpers/mock-console.ts +56 -0
  17. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  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/run-in-tmp.ts +29 -0
  21. package/src/__tests__/helpers/run-wrangler.ts +14 -0
  22. package/src/__tests__/index.test.ts +509 -119
  23. package/src/__tests__/jest.setup.ts +37 -0
  24. package/src/__tests__/kv.test.ts +1139 -0
  25. package/src/__tests__/logout.test.ts +50 -0
  26. package/src/__tests__/package-manager.test.ts +206 -0
  27. package/src/__tests__/publish.test.ts +1222 -0
  28. package/src/__tests__/secret.test.ts +210 -0
  29. package/src/__tests__/whoami.test.tsx +86 -0
  30. package/src/api/form_data.ts +85 -76
  31. package/src/api/preview.ts +29 -24
  32. package/src/api/worker.ts +50 -35
  33. package/src/cfetch/index.ts +102 -0
  34. package/src/cfetch/internal.ts +101 -0
  35. package/src/cli.ts +6 -7
  36. package/src/config.ts +630 -85
  37. package/src/dev.tsx +446 -282
  38. package/src/dialogs.tsx +6 -14
  39. package/src/index.tsx +773 -426
  40. package/src/inspect.ts +596 -0
  41. package/src/kv.tsx +118 -45
  42. package/src/module-collection.ts +63 -0
  43. package/src/package-manager.ts +120 -0
  44. package/src/pages.tsx +917 -211
  45. package/src/paths.ts +26 -0
  46. package/src/proxy.ts +370 -0
  47. package/src/publish.ts +220 -88
  48. package/src/sites.tsx +180 -55
  49. package/src/tail.tsx +6 -4
  50. package/src/user.tsx +77 -87
  51. package/src/whoami.tsx +69 -0
  52. package/templates/new-worker.js +15 -0
  53. package/templates/new-worker.ts +15 -0
  54. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  55. package/wrangler-dist/cli.js +125330 -557
  56. package/wrangler-dist/cli.js.map +3 -3
  57. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  58. package/src/__tests__/mock-cfetch.js +0 -46
  59. package/src/api/inspect.ts +0 -441
  60. package/src/cfetch.ts +0 -72
  61. package/src/util/fetch.ts +0 -74
package/src/dev.tsx CHANGED
@@ -1,62 +1,87 @@
1
- import esbuild from "esbuild";
2
- import httpProxy from "http-proxy";
3
- import { readFile } from "fs/promises";
4
- import type { DirectoryResult } from "tmp-promise";
5
- import tmp from "tmp-promise";
6
- import type { CfPreviewToken } from "./api/preview";
7
- import { Box, Text, useInput } from "ink";
8
- import React, { useState, useEffect, useRef } from "react";
9
- import path from "path";
1
+ import assert from "node:assert";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { watch } from "chokidar";
7
+ import clipboardy from "clipboardy";
8
+ import commandExists from "command-exists";
9
+ import * as esbuild from "esbuild";
10
+ import { execaCommand } from "execa";
11
+ import { Box, Text, useApp, useInput } from "ink";
10
12
  import open from "open";
11
- import { DtInspector } from "./api/inspect";
12
- import type { CfModule, CfVariable } from "./api/worker";
13
- import { createWorker } from "./api/worker";
14
- import type { CfWorkerInit } from "./api/worker";
15
- import { spawn } from "child_process";
13
+ import React, { useState, useEffect, useRef } from "react";
14
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
16
15
  import onExit from "signal-exit";
16
+ import tmp from "tmp-promise";
17
+ import { fetch } from "undici";
18
+
19
+ import { createWorker } from "./api/worker";
20
+
21
+ import useInspector from "./inspect";
22
+ import makeModuleCollector from "./module-collection";
23
+ import { usePreviewServer, waitForPortToBeAvailable } from "./proxy";
17
24
  import { syncAssets } from "./sites";
18
- import clipboardy from "clipboardy";
19
- import http from "node:http";
20
- import serveStatic from "serve-static";
21
- import commandExists from "command-exists";
22
- import assert from "assert";
23
25
  import { getAPIToken } from "./user";
24
- import fetch from "node-fetch";
26
+ import type { CfPreviewToken } from "./api/preview";
27
+ import type { CfModule, CfWorkerInit } from "./api/worker";
28
+ import type { AssetPaths } from "./sites";
29
+ import type { DirectoryResult } from "tmp-promise";
25
30
 
26
- type CfScriptFormat = void | "modules" | "service-worker";
31
+ type CfScriptFormat = undefined | "modules" | "service-worker";
27
32
 
28
- type Props = {
33
+ export type DevProps = {
29
34
  name?: string;
30
35
  entry: string;
31
36
  port?: number;
32
37
  format: CfScriptFormat;
33
- accountId: void | string;
38
+ accountId: undefined | string;
34
39
  initialMode: "local" | "remote";
35
- variables?: { [name: string]: CfVariable };
36
- public?: void | string;
37
- site?: void | string;
40
+ jsxFactory: undefined | string;
41
+ jsxFragment: undefined | string;
42
+ enableLocalPersistence: boolean;
43
+ bindings: CfWorkerInit["bindings"];
44
+ public: undefined | string;
45
+ assetPaths: undefined | AssetPaths;
46
+ compatibilityDate: undefined | string;
47
+ compatibilityFlags: undefined | string[];
48
+ usageModel: undefined | "bundled" | "unbound";
49
+ buildCommand: {
50
+ command?: undefined | string;
51
+ cwd?: undefined | string;
52
+ watch_dir?: undefined | string;
53
+ };
54
+ env: string | undefined;
38
55
  };
39
56
 
40
- export function Dev(props: Props): JSX.Element {
57
+ function Dev(props: DevProps): JSX.Element {
41
58
  if (props.public && props.format === "service-worker") {
42
59
  throw new Error(
43
60
  "You cannot use the service worker format with a `public` directory."
44
61
  );
45
62
  }
46
- const port = props.port || 8787;
63
+ const port = props.port ?? 8787;
47
64
  const apiToken = getAPIToken();
48
65
  const directory = useTmpDir();
49
66
 
50
- const bundle = useEsbuild(props.entry, directory, props.public);
67
+ // if there isn't a build command, we just return the entry immediately
68
+ // ideally there would be a conditional here, but the rules of hooks
69
+ // kinda forbid that, so we thread the entry through useCustomBuild
70
+ const entry = useCustomBuild(props.entry, props.buildCommand);
71
+
72
+ const bundle = useEsbuild({
73
+ entry,
74
+ destination: directory,
75
+ staticRoot: props.public,
76
+ jsxFactory: props.jsxFactory,
77
+ jsxFragment: props.jsxFragment,
78
+ });
51
79
  if (bundle && bundle.type === "commonjs" && !props.format && props.public) {
52
80
  throw new Error(
53
81
  "You cannot use the service worker format with a `public` directory."
54
82
  );
55
83
  }
56
84
 
57
- // @ts-expect-error whack
58
- useDevtoolsRefresh(bundle?.id ?? 0);
59
-
60
85
  const toggles = useHotkeys(
61
86
  {
62
87
  local: props.initialMode === "local",
@@ -74,10 +99,11 @@ 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}
78
- site={props.site}
102
+ bindings={props.bindings}
103
+ assetPaths={props.assetPaths}
79
104
  public={props.public}
80
- port={props.port}
105
+ port={port}
106
+ enableLocalPersistence={props.enableLocalPersistence}
81
107
  />
82
108
  ) : (
83
109
  <Remote
@@ -86,10 +112,14 @@ export function Dev(props: Props): JSX.Element {
86
112
  format={props.format}
87
113
  accountId={props.accountId}
88
114
  apiToken={apiToken}
89
- variables={props.variables}
90
- site={props.site}
115
+ bindings={props.bindings}
116
+ assetPaths={props.assetPaths}
91
117
  public={props.public}
92
- port={props.port}
118
+ port={port}
119
+ compatibilityDate={props.compatibilityDate}
120
+ compatibilityFlags={props.compatibilityFlags}
121
+ usageModel={props.usageModel}
122
+ env={props.env}
93
123
  />
94
124
  )}
95
125
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
@@ -105,98 +135,91 @@ export function Dev(props: Props): JSX.Element {
105
135
  );
106
136
  }
107
137
 
108
- function useDevtoolsRefresh(bundleId: number) {
109
- // TODO: this is a hack while we figure out
110
- // a better cleaner solution to get devtools to reconnect
111
- // without having to do a full refresh
112
- const ref = useRef();
113
- // @ts-expect-error whack
114
- ref.current = bundleId;
115
-
116
- useEffect(() => {
117
- const server = http.createServer((req, res) => {
118
- res.setHeader("Access-Control-Allow-Origin", "*");
119
- res.setHeader("Access-Control-Request-Method", "*");
120
- res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET");
121
- res.setHeader("Access-Control-Allow-Headers", "*");
122
- if (req.method === "OPTIONS") {
123
- res.writeHead(200);
124
- res.end();
125
- return;
126
- }
127
- res.writeHead(200, { "Content-Type": "application/json" });
128
- res.end(JSON.stringify({ value: ref.current }));
129
- });
130
-
131
- server.listen(3142);
132
- return () => {
133
- server.close();
134
- };
135
- }, []);
136
- }
137
-
138
138
  function Remote(props: {
139
- name: void | string;
140
- bundle: EsbuildBundle | void;
139
+ name: undefined | string;
140
+ bundle: EsbuildBundle | undefined;
141
141
  format: CfScriptFormat;
142
- public: void | string;
143
- site: void | string;
142
+ public: undefined | string;
143
+ assetPaths: undefined | AssetPaths;
144
144
  port: number;
145
- accountId: void | string;
146
- apiToken: void | string;
147
- variables: { [name: string]: CfVariable };
145
+ accountId: undefined | string;
146
+ apiToken: undefined | string;
147
+ bindings: CfWorkerInit["bindings"];
148
+ compatibilityDate: string | undefined;
149
+ compatibilityFlags: undefined | string[];
150
+ usageModel: undefined | "bundled" | "unbound";
151
+ env: string | undefined;
148
152
  }) {
149
153
  assert(props.accountId, "accountId is required");
150
154
  assert(props.apiToken, "apiToken is required");
151
- const token = useWorker({
155
+ const previewToken = useWorker({
152
156
  name: props.name,
153
157
  bundle: props.bundle,
154
158
  format: props.format,
155
- modules: [],
159
+ modules: props.bundle ? props.bundle.modules : [],
156
160
  accountId: props.accountId,
157
161
  apiToken: props.apiToken,
158
- variables: props.variables,
159
- sitesFolder: props.site,
162
+ bindings: props.bindings,
163
+ assetPaths: props.assetPaths,
160
164
  port: props.port,
165
+ compatibilityDate: props.compatibilityDate,
166
+ compatibilityFlags: props.compatibilityFlags,
167
+ usageModel: props.usageModel,
168
+ env: props.env,
161
169
  });
162
170
 
163
- useProxy({ token, publicRoot: props.public, port: props.port });
171
+ usePreviewServer({
172
+ previewToken,
173
+ publicRoot: props.public,
174
+ port: props.port,
175
+ });
164
176
 
165
- useInspector(token ? token.inspectorUrl.href : undefined);
177
+ useInspector({
178
+ inspectorUrl: previewToken ? previewToken.inspectorUrl.href : undefined,
179
+ port: 9229,
180
+ logToTerminal: true,
181
+ });
166
182
  return null;
167
183
  }
168
184
  function Local(props: {
169
- name: void | string;
170
- bundle: EsbuildBundle | void;
185
+ name: undefined | string;
186
+ bundle: EsbuildBundle | undefined;
171
187
  format: CfScriptFormat;
172
- variables: { [name: string]: CfVariable };
173
- public: void | string;
174
- site: void | string;
188
+ bindings: CfWorkerInit["bindings"];
189
+ assetPaths: undefined | AssetPaths;
190
+ public: undefined | string;
175
191
  port: number;
192
+ enableLocalPersistence: boolean;
176
193
  }) {
177
194
  const { inspectorUrl } = useLocalWorker({
178
195
  name: props.name,
179
196
  bundle: props.bundle,
180
197
  format: props.format,
181
- variables: props.variables,
198
+ bindings: props.bindings,
199
+ assetPaths: props.assetPaths,
200
+ public: props.public,
182
201
  port: props.port,
202
+ enableLocalPersistence: props.enableLocalPersistence,
183
203
  });
184
- useInspector(inspectorUrl);
204
+ useInspector({ inspectorUrl, port: 9229, logToTerminal: false });
185
205
  return null;
186
206
  }
187
207
 
188
208
  function useLocalWorker(props: {
189
- name: void | string;
190
- bundle: EsbuildBundle | void;
209
+ name: undefined | string;
210
+ bundle: EsbuildBundle | undefined;
191
211
  format: CfScriptFormat;
192
- variables: { [name: string]: CfVariable };
212
+ bindings: CfWorkerInit["bindings"];
213
+ assetPaths: undefined | AssetPaths;
214
+ public: undefined | string;
193
215
  port: number;
216
+ enableLocalPersistence: boolean;
194
217
  }) {
195
218
  // TODO: pass vars via command line
196
- const { bundle, format, variables, port } = props;
219
+ const { bundle, format, bindings, port, assetPaths } = props;
197
220
  const local = useRef<ReturnType<typeof spawn>>();
198
221
  const removeSignalExitListener = useRef<() => void>();
199
- const [inspectorUrl, setInspectorUrl] = useState<string | void>();
222
+ const [inspectorUrl, setInspectorUrl] = useState<string | undefined>();
200
223
  useEffect(() => {
201
224
  async function startLocalWorker() {
202
225
  if (!bundle) return;
@@ -211,7 +234,15 @@ function useLocalWorker(props: {
211
234
  return;
212
235
  }
213
236
 
237
+ await waitForPortToBeAvailable(port, { retryPeriod: 200, timeout: 2000 });
238
+ if (props.public) {
239
+ throw new Error(
240
+ '⎔ A "public" folder is not yet supported in local mode.'
241
+ );
242
+ }
243
+
214
244
  console.log("⎔ Starting a local server...");
245
+ // TODO: just use execa for this
215
246
  local.current = spawn("node", [
216
247
  "--experimental-vm-modules",
217
248
  "--inspect",
@@ -226,20 +257,34 @@ function useLocalWorker(props: {
226
257
  path.join(__dirname, "../miniflare-config-stubs/package.empty.json"),
227
258
  "--port",
228
259
  port.toString(),
229
- "--kv-persist",
230
- "--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),
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
+ ),
243
288
  "--modules",
244
289
  format ||
245
290
  (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
@@ -254,29 +299,42 @@ function useLocalWorker(props: {
254
299
  }
255
300
  });
256
301
 
257
- local.current.stdout.on("data", (data: string) => {
258
- // console.log(`stdout: ${data}`);
302
+ local.current.stdout?.on("data", (data: Buffer) => {
303
+ console.log(`${data.toString()}`);
259
304
  });
260
305
 
261
- local.current.stderr.on("data", (data: string) => {
262
- // console.error(`stderr: ${data}`);
306
+ local.current.stderr?.on("data", (data: Buffer) => {
307
+ console.error(`${data.toString()}`);
263
308
  const matches =
264
309
  /Debugger listening on (ws:\/\/127\.0\.0\.1:9229\/[A-Za-z0-9-]+)/.exec(
265
- data
310
+ data.toString()
266
311
  );
267
312
  if (matches) {
268
313
  setInspectorUrl(matches[1]);
269
314
  }
270
315
  });
271
316
 
272
- removeSignalExitListener.current = onExit((code, signal) => {
317
+ local.current.on("exit", (code) => {
318
+ if (code !== 0) {
319
+ console.error(`miniflare process exited with code ${code}`);
320
+ }
321
+ });
322
+
323
+ local.current.on("error", (error: Error) => {
324
+ console.error(`miniflare process failed to spawn`);
325
+ console.error(error);
326
+ });
327
+
328
+ removeSignalExitListener.current = onExit((_code, _signal) => {
273
329
  console.log("⎔ Shutting down local server.");
274
330
  local.current?.kill();
275
331
  local.current = undefined;
276
332
  });
277
333
  }
278
334
 
279
- startLocalWorker();
335
+ startLocalWorker().catch((err) => {
336
+ console.error("local worker:", err);
337
+ });
280
338
 
281
339
  return () => {
282
340
  if (local.current) {
@@ -287,12 +345,23 @@ function useLocalWorker(props: {
287
345
  removeSignalExitListener.current = undefined;
288
346
  }
289
347
  };
290
- }, [bundle, format, port]);
348
+ }, [
349
+ bundle,
350
+ format,
351
+ port,
352
+ bindings.durable_objects?.bindings,
353
+ bindings.kv_namespaces,
354
+ bindings.vars,
355
+ props.enableLocalPersistence,
356
+ assetPaths,
357
+ props.public,
358
+ ]);
291
359
  return { inspectorUrl };
292
360
  }
293
361
 
294
- function useTmpDir(): string | void {
362
+ function useTmpDir(): string | undefined {
295
363
  const [directory, setDirectory] = useState<DirectoryResult>();
364
+ const handleError = useErrorHandler();
296
365
  useEffect(() => {
297
366
  let dir: DirectoryResult;
298
367
  async function create() {
@@ -302,36 +371,118 @@ function useTmpDir(): string | void {
302
371
  return;
303
372
  } catch (err) {
304
373
  console.error("failed to create tmp dir");
305
- console.error(err);
306
374
  throw err;
307
375
  }
308
376
  }
309
- create();
377
+ create().catch((err) => {
378
+ // we want to break here
379
+ // we can't do much without a temp dir anyway
380
+ handleError(err);
381
+ });
310
382
  return () => {
311
- dir.cleanup();
383
+ dir.cleanup().catch(() => {
384
+ // extremely unlikely,
385
+ // but it's 2022 after all
386
+ console.error("failed to cleanup tmp dir");
387
+ });
312
388
  };
313
- }, []);
389
+ }, [handleError]);
314
390
  return directory?.path;
315
391
  }
316
392
 
393
+ function useCustomBuild(
394
+ expectedEntry: string,
395
+ props: {
396
+ command?: undefined | string;
397
+ cwd?: undefined | string;
398
+ watch_dir?: undefined | string;
399
+ }
400
+ ): undefined | string {
401
+ const [entry, setEntry] = useState<string | undefined>(
402
+ // if there's no build command, just return the expected entry
403
+ props.command || expectedEntry
404
+ );
405
+ const { command, cwd, watch_dir } = props;
406
+ useEffect(() => {
407
+ if (!command) return;
408
+ let cmd, interval;
409
+ console.log("running:", command);
410
+ cmd = execaCommand(command, {
411
+ ...(cwd && { cwd }),
412
+ shell: true,
413
+ stderr: "inherit",
414
+ stdout: "inherit",
415
+ });
416
+ if (watch_dir) {
417
+ watch(watch_dir, { persistent: true, ignoreInitial: true }).on(
418
+ "all",
419
+ (_event, filePath) => {
420
+ console.log(`The file ${filePath} changed, restarting build...`);
421
+ cmd.kill();
422
+ cmd = execaCommand(command, {
423
+ ...(cwd && { cwd }),
424
+ shell: true,
425
+ stderr: "inherit",
426
+ stdout: "inherit",
427
+ });
428
+ }
429
+ );
430
+ }
431
+
432
+ // check every so often whether `expectedEntry` exists
433
+ // if it does, we're done
434
+ const startedAt = Date.now();
435
+ interval = setInterval(() => {
436
+ if (existsSync(expectedEntry)) {
437
+ clearInterval(interval);
438
+ setEntry(expectedEntry);
439
+ } else {
440
+ const elapsed = Date.now() - startedAt;
441
+ // timeout after 30 seconds of waiting
442
+ if (elapsed > 1000 * 60 * 30) {
443
+ console.error("⎔ Build timed out.");
444
+ clearInterval(interval);
445
+ cmd.kill();
446
+ }
447
+ }
448
+ }, 200);
449
+ // TODO: we could probably timeout here after a while
450
+
451
+ return () => {
452
+ if (cmd) {
453
+ cmd.kill();
454
+ cmd = undefined;
455
+ }
456
+ clearInterval(interval);
457
+ interval = undefined;
458
+ };
459
+ }, [command, cwd, expectedEntry, watch_dir]);
460
+ return entry;
461
+ }
462
+
317
463
  type EsbuildBundle = {
318
464
  id: number;
319
465
  path: string;
320
466
  entry: string;
321
467
  type: "esm" | "commonjs";
322
468
  exports: string[];
469
+ modules: CfModule[];
323
470
  };
324
471
 
325
- function useEsbuild(
326
- entry: string,
327
- destination: string | void,
328
- staticRoot: void | string
329
- ): EsbuildBundle | void {
472
+ function useEsbuild(props: {
473
+ entry: undefined | string;
474
+ destination: string | undefined;
475
+ staticRoot: undefined | string;
476
+ jsxFactory: string | undefined;
477
+ jsxFragment: string | undefined;
478
+ }): EsbuildBundle | undefined {
479
+ const { entry, destination, staticRoot, jsxFactory, jsxFragment } = props;
330
480
  const [bundle, setBundle] = useState<EsbuildBundle>();
331
481
  useEffect(() => {
332
- let result: esbuild.BuildResult;
482
+ let result: esbuild.BuildResult | undefined;
333
483
  async function build() {
334
- if (!destination) return;
484
+ if (!destination || !entry) return;
485
+ const moduleCollector = makeModuleCollector();
335
486
  result = await esbuild.build({
336
487
  entryPoints: [entry],
337
488
  bundle: true,
@@ -339,7 +490,17 @@ function useEsbuild(
339
490
  metafile: true,
340
491
  format: "esm",
341
492
  sourcemap: true,
493
+ loader: {
494
+ ".js": "jsx",
495
+ ".html": "text",
496
+ ".pem": "text",
497
+ ".txt": "text",
498
+ },
499
+ ...(jsxFactory && { jsxFactory }),
500
+ ...(jsxFragment && { jsxFragment }),
342
501
  external: ["__STATIC_CONTENT_MANIFEST"],
502
+ conditions: ["worker", "browser"],
503
+ plugins: [moduleCollector.plugin],
343
504
  // TODO: import.meta.url
344
505
  watch: {
345
506
  async onRebuild(error) {
@@ -347,43 +508,68 @@ function useEsbuild(
347
508
  else {
348
509
  // nothing really changes here, so let's increment the id
349
510
  // to change the return object's identity
350
- setBundle((bundle) => ({ ...bundle, id: bundle.id + 1 }));
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
+ });
351
519
  }
352
520
  },
353
521
  },
354
522
  });
355
523
 
356
- const chunks = Object.entries(result.metafile.outputs).find(
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(
357
528
  ([_path, { entryPoint }]) => entryPoint === entry
358
529
  ); // assumedly only one entry point
359
530
 
531
+ if (outputEntry === undefined) {
532
+ throw new Error(
533
+ `Cannot find entry-point "${entry}" in generated bundle.`
534
+ );
535
+ }
360
536
  setBundle({
361
537
  id: 0,
362
538
  entry,
363
- path: chunks[0],
364
- type: chunks[1].exports.length > 0 ? "esm" : "commonjs",
365
- exports: chunks[1].exports,
539
+ path: outputEntry[0],
540
+ type: outputEntry[1].exports.length > 0 ? "esm" : "commonjs",
541
+ exports: outputEntry[1].exports,
542
+ modules: moduleCollector.modules,
366
543
  });
367
544
  }
368
- build();
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
550
+ });
369
551
  return () => {
370
- result?.stop();
552
+ result?.stop?.();
371
553
  };
372
- }, [entry, destination, staticRoot]);
554
+ }, [entry, destination, staticRoot, jsxFactory, jsxFragment]);
373
555
  return bundle;
374
556
  }
375
557
 
376
558
  function useWorker(props: {
377
- name: void | string;
378
- bundle: EsbuildBundle | void;
559
+ name: undefined | string;
560
+ bundle: EsbuildBundle | undefined;
379
561
  format: CfScriptFormat;
380
562
  modules: CfModule[];
381
563
  accountId: string;
382
564
  apiToken: string;
383
- variables: { [name: string]: CfVariable };
384
- sitesFolder: void | string;
565
+ bindings: CfWorkerInit["bindings"];
566
+ assetPaths: undefined | AssetPaths;
385
567
  port: number;
386
- }): CfPreviewToken | void {
568
+ compatibilityDate: string | undefined;
569
+ compatibilityFlags: string[] | undefined;
570
+ usageModel: undefined | "bundled" | "unbound";
571
+ env: string | undefined;
572
+ }): CfPreviewToken | undefined {
387
573
  const {
388
574
  name,
389
575
  bundle,
@@ -391,13 +577,24 @@ function useWorker(props: {
391
577
  modules,
392
578
  accountId,
393
579
  apiToken,
394
- variables,
395
- sitesFolder,
580
+ bindings,
581
+ assetPaths,
582
+ compatibilityDate,
583
+ compatibilityFlags,
584
+ usageModel,
396
585
  port,
397
586
  } = props;
398
- const [token, setToken] = useState<CfPreviewToken>();
587
+ const [token, setToken] = useState<CfPreviewToken | undefined>();
588
+
589
+ // This is the most reliable way to detect whether
590
+ // something's "happened" in our system; We make a ref and
591
+ // mark it once we log our initial message. Refs are vars!
592
+ const startedRef = useRef(false);
593
+
399
594
  useEffect(() => {
400
595
  async function start() {
596
+ setToken(undefined); // reset token in case we're re-running
597
+
401
598
  if (!bundle) return;
402
599
  if (format === "modules" && bundle.type === "commonjs") {
403
600
  console.error("⎔ Cannot use modules with a commonjs bundle.");
@@ -410,50 +607,56 @@ function useWorker(props: {
410
607
  return;
411
608
  }
412
609
 
413
- if (token) {
414
- console.log("⎔ Detected changes, restarting server...");
610
+ if (!startedRef.current) {
611
+ startedRef.current = true;
415
612
  } else {
416
- console.log("⎔ Starting server...");
613
+ console.log("⎔ Detected changes, restarted server.");
614
+ }
615
+
616
+ const assets = await syncAssets(
617
+ accountId,
618
+ name || path.basename(bundle.path),
619
+ assetPaths,
620
+ true,
621
+ props.env
622
+ ); // TODO: cancellable?
623
+
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}`;
417
630
  }
418
- const scriptName = path.basename(bundle.path);
419
-
420
- const assets = sitesFolder
421
- ? await syncAssets(
422
- accountId,
423
- scriptName,
424
- sitesFolder,
425
- true,
426
- undefined // TODO: env
427
- )
428
- : {
429
- manifest: undefined,
430
- namespace: undefined,
431
- }; // TODO: cancellable?
432
-
433
- const content = await readFile(bundle.path, "utf-8");
631
+
434
632
  const init: CfWorkerInit = {
633
+ name,
435
634
  main: {
436
- name: name || path.basename(bundle.path),
437
- type:
438
- format ||
439
- (bundle.type === "esm" ? "modules" : "service-worker") === "modules"
440
- ? "esm"
441
- : "commonjs",
635
+ name: path.basename(bundle.path),
636
+ type: workerType,
442
637
  content,
443
638
  },
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,
639
+ modules: modules.concat(
640
+ assets.manifest && workerType === "esm"
641
+ ? {
642
+ name: "__STATIC_CONTENT_MANIFEST",
643
+ content: JSON.stringify(assets.manifest),
644
+ type: "text",
645
+ }
646
+ : []
647
+ ),
648
+ bindings: {
649
+ ...bindings,
650
+ kv_namespaces: (bindings.kv_namespaces || []).concat(
651
+ assets.namespace
652
+ ? { binding: "__STATIC_CONTENT", id: assets.namespace }
653
+ : []
654
+ ),
655
+ },
656
+ migrations: undefined, // no migrations in dev
657
+ compatibility_date: compatibilityDate,
658
+ compatibility_flags: compatibilityFlags,
659
+ usage_model: usageModel,
457
660
  };
458
661
  setToken(
459
662
  await createWorker(init, {
@@ -461,80 +664,28 @@ function useWorker(props: {
461
664
  apiToken,
462
665
  })
463
666
  );
464
- console.log(`⬣ Listening at http://localhost:${port}`);
465
667
  }
466
- start();
467
- }, [name, bundle, format, accountId, apiToken, port, sitesFolder]);
468
- return token;
469
- }
470
-
471
- function useProxy({
472
- token,
473
- publicRoot,
474
- port,
475
- }: {
476
- token: CfPreviewToken | void;
477
- publicRoot: void | string;
478
- port: number;
479
- }) {
480
- useEffect(() => {
481
- if (!token) return;
482
- const proxy = httpProxy.createProxyServer({
483
- secure: false,
484
- changeOrigin: true,
485
- headers: {
486
- "cf-workers-preview-token": token.value,
487
- },
488
- target: `https://${token.host}`,
489
- // TODO: log websockets too? validate durables, etc
668
+ start().catch((err) => {
669
+ // we want to log the error, but not end the process
670
+ // since it could recover after the developer fixes whatever's wrong
671
+ console.error("remote worker:", err);
490
672
  });
491
-
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
508
-
509
- proxy.on("proxyRes", function (proxyRes, req, res) {
510
- // log all requests
511
- console.log(
512
- new Date().toLocaleTimeString(),
513
- req.method,
514
- req.url,
515
- res.statusCode // TODO add a status message like Ok etc?
516
- );
517
- });
518
- // TODO: log errors?
519
-
520
- return () => {
521
- proxy.close();
522
- server.close();
523
- };
524
- }, [token, publicRoot, port]);
525
- }
526
-
527
- function useInspector(inspectorUrl: string | void) {
528
- useEffect(() => {
529
- if (!inspectorUrl) return;
530
-
531
- const inspector = new DtInspector(inspectorUrl);
532
- const abortController = inspector.proxyTo(9229);
533
- return () => {
534
- inspector.close();
535
- abortController.abort();
536
- };
537
- }, [inspectorUrl]);
673
+ }, [
674
+ name,
675
+ bundle,
676
+ format,
677
+ accountId,
678
+ apiToken,
679
+ port,
680
+ assetPaths,
681
+ compatibilityDate,
682
+ compatibilityFlags,
683
+ usageModel,
684
+ bindings,
685
+ modules,
686
+ props.env,
687
+ ]);
688
+ return token;
538
689
  }
539
690
 
540
691
  function sleep(period: number) {
@@ -544,7 +695,7 @@ const SLEEP_DURATION = 2000;
544
695
  // really need a first class api for this
545
696
  const hostNameRegex = /userHostname="(.*)"/g;
546
697
  async function findTunnelHostname() {
547
- let hostName: string;
698
+ let hostName: string | undefined;
548
699
  while (!hostName) {
549
700
  try {
550
701
  const resp = await fetch("http://localhost:8789/metrics");
@@ -589,19 +740,21 @@ function useTunnel(toggle: boolean) {
589
740
  }
590
741
  });
591
742
 
592
- removeSignalExitListener.current = onExit((code, signal) => {
743
+ removeSignalExitListener.current = onExit((_code, _signal) => {
593
744
  console.log("⎔ Shutting down local tunnel.");
594
745
  tunnel.current?.kill();
595
746
  tunnel.current = undefined;
596
747
  });
597
748
 
598
749
  const hostName = await findTunnelHostname();
599
- clipboardy.write(hostName);
750
+ await clipboardy.write(hostName);
600
751
  console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
601
752
  }
602
753
  }
603
754
 
604
- startTunnel();
755
+ startTunnel().catch((err) => {
756
+ console.error("tunnel:", err);
757
+ });
605
758
 
606
759
  return () => {
607
760
  if (tunnel.current) {
@@ -623,41 +776,39 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
623
776
  // UGH, we should put port in context instead
624
777
  const [toggles, setToggles] = useState(initial);
625
778
  useInput(
626
- (
779
+ async (
627
780
  input,
628
781
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
629
782
  key
630
783
  ) => {
631
- switch (input) {
632
- 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
- );
784
+ switch (input.toLowerCase()) {
785
+ // open browser
786
+ case "b":
787
+ await open(`http://localhost:${port}/`);
641
788
  break;
642
- case "d": // toggle inspector
643
- open(
789
+ // toggle inspector
790
+ case "d":
791
+ await open(
644
792
  `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
793
  );
652
794
  break;
653
- case "s": // toggle tunnel
654
- setToggles((toggles) => ({ ...toggles, tunnel: !toggles.tunnel }));
795
+ // toggle tunnel
796
+ case "s":
797
+ setToggles((previousToggles) => ({
798
+ ...previousToggles,
799
+ tunnel: !previousToggles.tunnel,
800
+ }));
655
801
  break;
656
- case "l": // toggle local
657
- setToggles((toggles) => ({ ...toggles, local: !toggles.local }));
802
+ // toggle local
803
+ case "l":
804
+ setToggles((previousToggles) => ({
805
+ ...previousToggles,
806
+ local: !previousToggles.local,
807
+ }));
658
808
  break;
659
- case "q": // shut down
660
- case "x": // shut down
809
+ // shut down
810
+ case "q":
811
+ case "x":
661
812
  process.exit(0);
662
813
  break;
663
814
  default:
@@ -668,3 +819,16 @@ function useHotkeys(initial: useHotkeysInitialState, port: number) {
668
819
  );
669
820
  return toggles;
670
821
  }
822
+
823
+ function ErrorFallback(props: { error: Error }) {
824
+ const { exit } = useApp();
825
+ useEffect(() => exit(new Error()));
826
+ return (
827
+ <>
828
+ <Text>Something went wrong:</Text>
829
+ <Text>{props.error.stack}</Text>
830
+ </>
831
+ );
832
+ }
833
+
834
+ export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback });