wrangler 0.0.0-e1c615f → 0.0.0-e3cab74

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