wrangler 0.0.0-e3cab74 → 0.0.0-e3e3243

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 (66) hide show
  1. package/README.md +1 -1
  2. package/bin/wrangler.js +10 -1
  3. package/miniflare-dist/index.mjs +79 -0
  4. package/miniflare-dist/index.mjs.map +7 -0
  5. package/package.json +10 -7
  6. package/pages/functions/filepath-routing.test.ts +125 -25
  7. package/pages/functions/filepath-routing.ts +6 -3
  8. package/src/__tests__/configuration.test.ts +959 -324
  9. package/src/__tests__/dev.test.tsx +512 -84
  10. package/src/__tests__/guess-worker-format.test.ts +19 -17
  11. package/src/__tests__/helpers/mock-account-id.ts +22 -4
  12. package/src/__tests__/helpers/mock-cfetch.ts +3 -3
  13. package/src/__tests__/helpers/mock-console.ts +17 -9
  14. package/src/__tests__/helpers/mock-kv.ts +9 -16
  15. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  16. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  17. package/src/__tests__/https-options.test.ts +127 -0
  18. package/src/__tests__/index.test.ts +128 -11
  19. package/src/__tests__/jest.setup.ts +16 -4
  20. package/src/__tests__/kv.test.ts +364 -125
  21. package/src/__tests__/logout.test.ts +15 -13
  22. package/src/__tests__/pages.test.ts +82 -0
  23. package/src/__tests__/publish.test.ts +1838 -735
  24. package/src/__tests__/r2.test.ts +4 -4
  25. package/src/__tests__/route.test.ts +45 -0
  26. package/src/__tests__/secret.test.ts +167 -68
  27. package/src/__tests__/tail.test.ts +2 -4
  28. package/src/__tests__/whoami.test.tsx +21 -7
  29. package/src/bundle.ts +27 -5
  30. package/src/cfetch/internal.ts +11 -5
  31. package/src/config/README.md +6 -6
  32. package/src/config/config.ts +54 -101
  33. package/src/config/environment.ts +67 -1
  34. package/src/config/index.ts +13 -3
  35. package/src/config/validation-helpers.ts +207 -23
  36. package/src/config/validation.ts +344 -129
  37. package/src/{api/preview.ts → create-worker-preview.ts} +39 -20
  38. package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
  39. package/src/dev/dev.tsx +392 -0
  40. package/src/dev/local.tsx +232 -0
  41. package/src/dev/remote.tsx +203 -0
  42. package/src/dev/use-esbuild.ts +104 -0
  43. package/src/entry.ts +192 -9
  44. package/src/https-options.ts +122 -0
  45. package/src/index.tsx +577 -515
  46. package/src/inspect.ts +9 -1
  47. package/src/intl-polyfill.d.ts +139 -0
  48. package/src/{kv.tsx → kv.ts} +97 -53
  49. package/src/miniflare-cli/README.md +30 -0
  50. package/src/miniflare-cli/enum-keys.ts +17 -0
  51. package/src/miniflare-cli/index.ts +37 -0
  52. package/src/module-collection.ts +90 -2
  53. package/src/open-in-browser.ts +14 -2
  54. package/src/pages.tsx +26 -10
  55. package/src/proxy.ts +104 -40
  56. package/src/publish.ts +53 -71
  57. package/src/reporting.ts +25 -9
  58. package/src/selfsigned.d.ts +29 -0
  59. package/src/sites.tsx +68 -29
  60. package/src/user.tsx +163 -85
  61. package/src/{api/worker.ts → worker.ts} +1 -24
  62. package/wrangler-dist/cli.js +5933 -3842
  63. package/wrangler-dist/cli.js.map +3 -3
  64. package/src/__tests__/helpers/mock-user.ts +0 -27
  65. package/src/dev.tsx +0 -961
  66. package/src/guess-worker-format.ts +0 -68
@@ -1,7 +1,7 @@
1
1
  import { URL } from "node:url";
2
2
  import { fetch } from "undici";
3
- import { fetchResult } from "../cfetch";
4
- import { toFormData } from "./form_data";
3
+ import { fetchResult } from "./cfetch";
4
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
5
5
  import type { CfAccount, CfWorkerContext, CfWorkerInit } from "./worker";
6
6
 
7
7
  /**
@@ -61,7 +61,7 @@ async function sessionToken(
61
61
  ): Promise<CfPreviewToken> {
62
62
  const { accountId } = account;
63
63
  const initUrl = ctx.zone
64
- ? `/zones/${ctx.zone}/workers/edge-preview`
64
+ ? `/zones/${ctx.zone.id}/workers/edge-preview`
65
65
  : `/accounts/${accountId}/workers/subdomain/edge-preview`;
66
66
 
67
67
  const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl);
@@ -93,7 +93,7 @@ function randomId(): string {
93
93
  /**
94
94
  * Creates a preview token.
95
95
  */
96
- export async function previewToken(
96
+ async function createPreviewToken(
97
97
  account: CfAccount,
98
98
  worker: CfWorkerInit,
99
99
  ctx: CfWorkerContext
@@ -111,10 +111,10 @@ export async function previewToken(
111
111
  : `/accounts/${accountId}/workers/scripts/${scriptId}/edge-preview`;
112
112
 
113
113
  const mode: CfPreviewMode = ctx.zone
114
- ? { routes: ["*/*"] }
114
+ ? { routes: ["*/*"] } // TODO: should we support routes here? how?
115
115
  : { workers_dev: true };
116
116
 
117
- const formData = toFormData(worker);
117
+ const formData = createWorkerUploadForm(worker);
118
118
  formData.set("wrangler-session-config", JSON.stringify(mode));
119
119
 
120
120
  const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
@@ -127,21 +127,40 @@ export async function previewToken(
127
127
 
128
128
  return {
129
129
  value: preview_token,
130
- // TODO: verify this works with zoned workers
131
- host:
132
- worker.name && !ctx.zone
133
- ? // hrmm this might need change as well
134
- `${
135
- worker.name
136
- // TODO: this should also probably have the env prefix
137
- // but it doesn't appear to work yet, instead giving us the
138
- // "There is nothing here yet" screen
139
- // ctx.env && !ctx.legacyEnv
140
- // ? `${ctx.env}.${worker.name}`
141
- // : worker.name
142
- }.${host.split(".").slice(1).join(".")}`
143
- : host,
130
+ host: ctx.zone
131
+ ? ctx.zone.host
132
+ : worker.name
133
+ ? `${
134
+ worker.name
135
+ // TODO: this should also probably have the env prefix
136
+ // but it doesn't appear to work yet, instead giving us the
137
+ // "There is nothing here yet" screen
138
+ // ctx.env && !ctx.legacyEnv
139
+ // ? `${ctx.env}.${worker.name}`
140
+ // : worker.name
141
+ }.${host.split(".").slice(1).join(".")}`
142
+ : host,
143
+
144
144
  inspectorUrl,
145
145
  prewarmUrl,
146
146
  };
147
147
  }
148
+
149
+ /**
150
+ * A stub to create a Cloudflare Worker preview.
151
+ *
152
+ * @example
153
+ * const {value, host} = await createWorker(init, acct);
154
+ */
155
+ export async function createWorkerPreview(
156
+ init: CfWorkerInit,
157
+ account: CfAccount,
158
+ ctx: CfWorkerContext
159
+ ): Promise<CfPreviewToken> {
160
+ const token = await createPreviewToken(account, init, ctx);
161
+ const response = await fetch(token.prewarmUrl.href, { method: "POST" });
162
+ if (!response.ok) {
163
+ // console.error("worker failed to prewarm: ", response.statusText);
164
+ }
165
+ return token;
166
+ }
@@ -51,7 +51,7 @@ export interface WorkerMetadata {
51
51
  /**
52
52
  * Creates a `FormData` upload from a `CfWorkerInit`.
53
53
  */
54
- export function toFormData(worker: CfWorkerInit): FormData {
54
+ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
55
55
  const formData = new FormData();
56
56
  const {
57
57
  main,
@@ -0,0 +1,392 @@
1
+ import { spawn } from "node:child_process";
2
+ import { watch } from "chokidar";
3
+ import clipboardy from "clipboardy";
4
+ import commandExists from "command-exists";
5
+ import { Box, Text, useApp, useInput, useStdin } from "ink";
6
+ import React, { useState, useEffect, useRef } from "react";
7
+ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
8
+ import onExit from "signal-exit";
9
+ import tmp from "tmp-promise";
10
+ import { fetch } from "undici";
11
+ import { runCustomBuild } from "../entry";
12
+ import openInBrowser from "../open-in-browser";
13
+ import { reportError } from "../reporting";
14
+ import { getAPIToken } from "../user";
15
+ import { Local } from "./local";
16
+ import { Remote } from "./remote";
17
+ import { useEsbuild } from "./use-esbuild";
18
+ import type { Config } from "../config";
19
+ import type { Entry } from "../entry";
20
+ import type { AssetPaths } from "../sites";
21
+ import type { CfWorkerInit } from "../worker";
22
+ import type { EsbuildBundle } from "./use-esbuild";
23
+
24
+ export type DevProps = {
25
+ name?: string;
26
+ entry: Entry;
27
+ port: number;
28
+ ip: string;
29
+ inspectorPort: number;
30
+ rules: Config["rules"];
31
+ accountId: undefined | string;
32
+ initialMode: "local" | "remote";
33
+ jsxFactory: undefined | string;
34
+ jsxFragment: undefined | string;
35
+ tsconfig: string | undefined;
36
+ upstreamProtocol: "https" | "http";
37
+ localProtocol: "https" | "http";
38
+ enableLocalPersistence: boolean;
39
+ bindings: CfWorkerInit["bindings"];
40
+ public: undefined | string;
41
+ assetPaths: undefined | AssetPaths;
42
+ compatibilityDate: undefined | string;
43
+ compatibilityFlags: undefined | string[];
44
+ usageModel: undefined | "bundled" | "unbound";
45
+ build: {
46
+ command?: undefined | string;
47
+ cwd?: undefined | string;
48
+ watch_dir?: undefined | string;
49
+ };
50
+ env: string | undefined;
51
+ legacyEnv: boolean;
52
+ zone:
53
+ | {
54
+ id: string;
55
+ host: string;
56
+ }
57
+ | undefined;
58
+ };
59
+
60
+ export function DevImplementation(props: DevProps): JSX.Element {
61
+ const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
62
+ const directory = useTmpDir();
63
+
64
+ useCustomBuild(props.entry, props.build);
65
+
66
+ if (props.public && props.entry.format === "service-worker") {
67
+ throw new Error(
68
+ "You cannot use the service worker format with a `public` directory."
69
+ );
70
+ }
71
+
72
+ if (props.bindings.wasm_modules && props.entry.format === "modules") {
73
+ throw new Error(
74
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
75
+ );
76
+ }
77
+
78
+ if (props.bindings.text_blobs && props.entry.format === "modules") {
79
+ throw new Error(
80
+ "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"
81
+ );
82
+ }
83
+
84
+ const bundle = useEsbuild({
85
+ entry: props.entry,
86
+ destination: directory,
87
+ staticRoot: props.public,
88
+ jsxFactory: props.jsxFactory,
89
+ rules: props.rules,
90
+ jsxFragment: props.jsxFragment,
91
+ serveAssetsFromWorker: !!props.public,
92
+ tsconfig: props.tsconfig,
93
+ });
94
+
95
+ // only load the UI if we're running in a supported environment
96
+ const { isRawModeSupported } = useStdin();
97
+ return isRawModeSupported ? (
98
+ <InteractiveDevSession {...props} bundle={bundle} apiToken={apiToken} />
99
+ ) : (
100
+ <DevSession
101
+ {...props}
102
+ bundle={bundle}
103
+ apiToken={apiToken}
104
+ local={props.initialMode === "local"}
105
+ />
106
+ );
107
+ }
108
+
109
+ type InteractiveDevSessionProps = DevProps & {
110
+ apiToken: string | undefined;
111
+ bundle: EsbuildBundle | undefined;
112
+ };
113
+
114
+ function InteractiveDevSession(props: InteractiveDevSessionProps) {
115
+ const toggles = useHotkeys(
116
+ {
117
+ local: props.initialMode === "local",
118
+ tunnel: false,
119
+ },
120
+ props.port,
121
+ props.ip,
122
+ props.inspectorPort,
123
+ props.localProtocol
124
+ );
125
+
126
+ useTunnel(toggles.tunnel);
127
+
128
+ return (
129
+ <>
130
+ <DevSession {...props} local={toggles.local} />
131
+ <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
132
+ <Text>
133
+ {`B to open a browser, D to open Devtools, L to ${
134
+ toggles.local ? "turn off" : "turn on"
135
+ } local mode, X to exit`}
136
+ </Text>
137
+ </Box>
138
+ </>
139
+ );
140
+ }
141
+
142
+ type DevSessionProps = InteractiveDevSessionProps & { local: boolean };
143
+
144
+ function DevSession(props: DevSessionProps) {
145
+ return props.local ? (
146
+ <Local
147
+ name={props.name}
148
+ bundle={props.bundle}
149
+ format={props.entry.format}
150
+ compatibilityDate={props.compatibilityDate}
151
+ compatibilityFlags={props.compatibilityFlags}
152
+ bindings={props.bindings}
153
+ assetPaths={props.assetPaths}
154
+ public={props.public}
155
+ port={props.port}
156
+ ip={props.ip}
157
+ rules={props.rules}
158
+ inspectorPort={props.inspectorPort}
159
+ enableLocalPersistence={props.enableLocalPersistence}
160
+ />
161
+ ) : (
162
+ <Remote
163
+ name={props.name}
164
+ bundle={props.bundle}
165
+ format={props.entry.format}
166
+ accountId={props.accountId}
167
+ apiToken={props.apiToken}
168
+ bindings={props.bindings}
169
+ assetPaths={props.assetPaths}
170
+ public={props.public}
171
+ port={props.port}
172
+ ip={props.ip}
173
+ localProtocol={props.localProtocol}
174
+ inspectorPort={props.inspectorPort}
175
+ compatibilityDate={props.compatibilityDate}
176
+ compatibilityFlags={props.compatibilityFlags}
177
+ usageModel={props.usageModel}
178
+ env={props.env}
179
+ legacyEnv={props.legacyEnv}
180
+ zone={props.zone}
181
+ />
182
+ );
183
+ }
184
+
185
+ export interface DirectorySyncResult {
186
+ name: string;
187
+ removeCallback: () => void;
188
+ }
189
+
190
+ function useTmpDir(): string | undefined {
191
+ const [directory, setDirectory] = useState<DirectorySyncResult>();
192
+ const handleError = useErrorHandler();
193
+ useEffect(() => {
194
+ let dir: DirectorySyncResult | undefined;
195
+ try {
196
+ dir = tmp.dirSync({ unsafeCleanup: true });
197
+ setDirectory(dir);
198
+ return;
199
+ } catch (err) {
200
+ console.error("failed to create tmp dir");
201
+ handleError(err);
202
+ }
203
+ return () => {
204
+ dir?.removeCallback();
205
+ };
206
+ }, [handleError]);
207
+ return directory?.name;
208
+ }
209
+
210
+ function useCustomBuild(
211
+ expectedEntry: Entry,
212
+ build: {
213
+ command?: undefined | string;
214
+ cwd?: undefined | string;
215
+ watch_dir?: undefined | string;
216
+ }
217
+ ): void {
218
+ useEffect(() => {
219
+ if (!build.command) return;
220
+ let watcher: ReturnType<typeof watch> | undefined;
221
+ if (build.watch_dir) {
222
+ watcher = watch(build.watch_dir, {
223
+ persistent: true,
224
+ ignoreInitial: true,
225
+ }).on("all", (_event, filePath) => {
226
+ //TODO: we should buffer requests to the proxy until this completes
227
+ console.log(`The file ${filePath} changed, restarting build...`);
228
+ runCustomBuild(expectedEntry.file, build).catch((err) => {
229
+ console.error("Custom build failed:", err);
230
+ });
231
+ });
232
+ }
233
+
234
+ return () => {
235
+ watcher?.close();
236
+ };
237
+ }, [build, expectedEntry.file]);
238
+ }
239
+
240
+ function sleep(period: number) {
241
+ return new Promise((resolve) => setTimeout(resolve, period));
242
+ }
243
+ const SLEEP_DURATION = 2000;
244
+ // really need a first class api for this
245
+ const hostNameRegex = /userHostname="(.*)"/g;
246
+ async function findTunnelHostname() {
247
+ let hostName: string | undefined;
248
+ while (!hostName) {
249
+ try {
250
+ const resp = await fetch("http://localhost:8789/metrics");
251
+ const data = await resp.text();
252
+ const matches = Array.from(data.matchAll(hostNameRegex));
253
+ hostName = matches[0][1];
254
+ } catch (err) {
255
+ await sleep(SLEEP_DURATION);
256
+ }
257
+ }
258
+ return hostName;
259
+ }
260
+
261
+ /**
262
+ * Create a tunnel to the remote worker.
263
+ * We've disabled this for now until we figure out a better user experience.
264
+ */
265
+ function useTunnel(toggle: boolean) {
266
+ const tunnel = useRef<ReturnType<typeof spawn>>();
267
+ const removeSignalExitListener = useRef<() => void>();
268
+ // TODO: test if cloudflared is available, if not
269
+ // point them to a url where they can get docs to install it
270
+ useEffect(() => {
271
+ async function startTunnel() {
272
+ if (toggle) {
273
+ try {
274
+ await commandExists("cloudflared");
275
+ } catch (e) {
276
+ console.error(
277
+ "To share your worker on the Internet, please install `cloudflared` from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
278
+ );
279
+ return;
280
+ }
281
+ console.log("⎔ Starting a tunnel...");
282
+ tunnel.current = spawn("cloudflared", [
283
+ "tunnel",
284
+ "--url",
285
+ "http://localhost:8787",
286
+ "--metrics",
287
+ "localhost:8789",
288
+ ]);
289
+
290
+ tunnel.current.on("close", (code) => {
291
+ if (code) {
292
+ console.log(`Tunnel process exited with code ${code}`);
293
+ }
294
+ });
295
+
296
+ removeSignalExitListener.current = onExit((_code, _signal) => {
297
+ console.log("⎔ Shutting down local tunnel.");
298
+ tunnel.current?.kill();
299
+ tunnel.current = undefined;
300
+ });
301
+
302
+ const hostName = await findTunnelHostname();
303
+ await clipboardy.write(hostName);
304
+ console.log(`⬣ Sharing at ${hostName}, copied to clipboard.`);
305
+ }
306
+ }
307
+
308
+ startTunnel().catch(async (err) => {
309
+ console.error("tunnel:", err);
310
+ await reportError(err);
311
+ });
312
+
313
+ return () => {
314
+ if (tunnel.current) {
315
+ console.log("⎔ Shutting down tunnel.");
316
+ tunnel.current?.kill();
317
+ tunnel.current = undefined;
318
+ removeSignalExitListener.current && removeSignalExitListener.current();
319
+ removeSignalExitListener.current = undefined;
320
+ }
321
+ };
322
+ }, [toggle]);
323
+ }
324
+
325
+ type useHotkeysInitialState = {
326
+ local: boolean;
327
+ tunnel: boolean;
328
+ };
329
+ function useHotkeys(
330
+ initial: useHotkeysInitialState,
331
+ port: number,
332
+ ip: string,
333
+ inspectorPort: number,
334
+ localProtocol: "http" | "https"
335
+ ) {
336
+ // UGH, we should put port in context instead
337
+ const [toggles, setToggles] = useState(initial);
338
+ useInput(
339
+ async (
340
+ input,
341
+ // eslint-disable-next-line unused-imports/no-unused-vars
342
+ key
343
+ ) => {
344
+ switch (input.toLowerCase()) {
345
+ // open browser
346
+ case "b": {
347
+ await openInBrowser(`${localProtocol}://${ip}:${port}`);
348
+ break;
349
+ }
350
+ // toggle inspector
351
+ case "d": {
352
+ await openInBrowser(
353
+ `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`,
354
+ { forceChromium: true }
355
+ );
356
+ break;
357
+ }
358
+ // toggle local
359
+ case "l":
360
+ setToggles((previousToggles) => ({
361
+ ...previousToggles,
362
+ local: !previousToggles.local,
363
+ }));
364
+ break;
365
+ // shut down
366
+ case "q":
367
+ case "x":
368
+ process.exit(0);
369
+ break;
370
+ default:
371
+ // nothing?
372
+ break;
373
+ }
374
+ }
375
+ );
376
+ return toggles;
377
+ }
378
+
379
+ function ErrorFallback(props: { error: Error }) {
380
+ const { exit } = useApp();
381
+ useEffect(() => exit(props.error));
382
+ return (
383
+ <>
384
+ <Text>Something went wrong:</Text>
385
+ <Text>{props.error.stack}</Text>
386
+ </>
387
+ );
388
+ }
389
+
390
+ export default withErrorBoundary(DevImplementation, {
391
+ FallbackComponent: ErrorFallback,
392
+ });