wrangler 2.0.25 → 2.0.26

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.
@@ -1,5 +1,6 @@
1
1
  import fetchMock from "jest-fetch-mock";
2
2
  import {
3
+ fetchDashboardScript,
3
4
  fetchInternal,
4
5
  fetchKVGetValue,
5
6
  fetchR2Objects,
@@ -7,6 +8,7 @@ import {
7
8
  } from "../cfetch/internal";
8
9
  import { confirm, prompt } from "../dialogs";
9
10
  import {
11
+ mockFetchDashScript,
10
12
  mockFetchInternal,
11
13
  mockFetchKVGetValue,
12
14
  mockFetchR2Objects,
@@ -49,6 +51,7 @@ jest.mock("../cfetch/internal");
49
51
  "https://api.cloudflare.com/client/v4"
50
52
  );
51
53
  (fetchR2Objects as jest.Mock).mockImplementation(mockFetchR2Objects);
54
+ (fetchDashboardScript as jest.Mock).mockImplementation(mockFetchDashScript);
52
55
 
53
56
  jest.mock("../dialogs");
54
57
 
@@ -97,6 +97,20 @@ describe("pages", () => {
97
97
  expect(std.out).toMatchInlineSnapshot(`
98
98
  "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
99
99
 
100
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
101
+ `);
102
+ });
103
+
104
+ it("should display for pages:functions:optimize-routes", async () => {
105
+ await expect(
106
+ runWrangler(
107
+ 'pages functions optimize-routes --routes-path="/build/_routes.json" --output-routes-path="/build/_optimized-routes.json"'
108
+ )
109
+ ).rejects.toThrowError();
110
+
111
+ expect(std.out).toMatchInlineSnapshot(`
112
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
113
+
100
114
  If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
101
115
  `);
102
116
  });
@@ -118,7 +118,7 @@ describe("tail", () => {
118
118
  const api = mockWebsocketAPIs();
119
119
  await runWrangler("tail test-worker --status error");
120
120
  await expect(api.nextMessageJson()).resolves.toHaveProperty("filters", [
121
- { outcome: ["exception", "exceededCpu", "unknown"] },
121
+ { outcome: ["exception", "exceededCpu", "exceededMemory", "unknown"] },
122
122
  ]);
123
123
  });
124
124
 
@@ -126,7 +126,15 @@ describe("tail", () => {
126
126
  const api = mockWebsocketAPIs();
127
127
  await runWrangler("tail test-worker --status error --status canceled");
128
128
  await expect(api.nextMessageJson()).resolves.toHaveProperty("filters", [
129
- { outcome: ["exception", "exceededCpu", "unknown", "canceled"] },
129
+ {
130
+ outcome: [
131
+ "exception",
132
+ "exceededCpu",
133
+ "exceededMemory",
134
+ "unknown",
135
+ "canceled",
136
+ ],
137
+ },
130
138
  ]);
131
139
  });
132
140
 
@@ -213,7 +221,15 @@ describe("tail", () => {
213
221
  const expectedWebsocketMessage = {
214
222
  filters: [
215
223
  { sampling_rate },
216
- { outcome: ["ok", "exception", "exceededCpu", "unknown"] },
224
+ {
225
+ outcome: [
226
+ "ok",
227
+ "exception",
228
+ "exceededCpu",
229
+ "exceededMemory",
230
+ "unknown",
231
+ ],
232
+ },
217
233
  { method },
218
234
  { header: { key: "X-HELLO", query: "world" } },
219
235
  { client_ip },
package/src/api/dev.ts CHANGED
@@ -16,6 +16,7 @@ interface DevOptions {
16
16
  siteExclude?: string[];
17
17
  nodeCompat?: boolean;
18
18
  compatibilityDate?: string;
19
+ compatibilityFlags?: string[];
19
20
  experimentalEnableLocalPersistence?: boolean;
20
21
  liveReload?: boolean;
21
22
  watch?: boolean;
@@ -179,3 +179,42 @@ export async function fetchR2Objects(
179
179
  );
180
180
  }
181
181
  }
182
+
183
+ /**
184
+ * This is a wrapper STOPGAP for getting the script which returns a raw text response.
185
+ */
186
+ export async function fetchDashboardScript(
187
+ resource: string,
188
+ bodyInit: RequestInit = {}
189
+ ): Promise<string> {
190
+ await requireLoggedIn();
191
+ const auth = requireApiToken();
192
+ const headers = cloneHeaders(bodyInit.headers);
193
+ addAuthorizationHeaderIfUnspecified(headers, auth);
194
+ addUserAgent(headers);
195
+
196
+ const response = await fetch(`${getCloudflareAPIBaseURL()}${resource}`, {
197
+ ...bodyInit,
198
+ headers,
199
+ });
200
+
201
+ if (!response.ok || !response.body) {
202
+ throw new Error(
203
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText});`
204
+ );
205
+ }
206
+
207
+ const usesModules = response.headers
208
+ .get("content-type")
209
+ ?.startsWith("multipart");
210
+
211
+ if (usesModules) {
212
+ const file = await response.text();
213
+
214
+ // Follow up on issue in Undici about multipart/form-data support & replace the workaround: https://github.com/nodejs/undici/issues/974
215
+ // This should be using a builtin formData() parser pattern.
216
+ return file.split("\n").slice(4, -4).join("\n");
217
+ } else {
218
+ return response.text();
219
+ }
220
+ }
package/src/dev/dev.tsx CHANGED
@@ -493,6 +493,10 @@ function useHotkeys(props: {
493
493
  break;
494
494
  // open browser
495
495
  case "b": {
496
+ if (ip === "0.0.0.0") {
497
+ await openInBrowser(`${localProtocol}://127.0.0.1:${port}`);
498
+ return;
499
+ }
496
500
  await openInBrowser(`${localProtocol}://${ip}:${port}`);
497
501
  break;
498
502
  }
package/src/dev.tsx CHANGED
@@ -502,10 +502,10 @@ export async function startDev(args: StartDevOptions) {
502
502
  isWorkersSite={Boolean(args.site || config.site)}
503
503
  compatibilityDate={getDevCompatibilityDate(
504
504
  config,
505
- args["compatibility-date"]
505
+ args.compatibilityDate
506
506
  )}
507
507
  compatibilityFlags={
508
- args["compatibility-flags"] || config.compatibility_flags
508
+ args.compatibilityFlags || config.compatibility_flags
509
509
  }
510
510
  usageModel={config.usage_model}
511
511
  bindings={bindings}
package/src/init.ts CHANGED
@@ -5,12 +5,16 @@ import TOML from "@iarna/toml";
5
5
  import { findUp } from "find-up";
6
6
  import { version as wranglerVersion } from "../package.json";
7
7
 
8
+ import { fetchDashboardScript } from "./cfetch/internal";
9
+ import { readConfig } from "./config";
8
10
  import { confirm, select } from "./dialogs";
9
11
  import { initializeGit, isGitInstalled, isInsideGitRepo } from "./git-client";
10
12
  import { logger } from "./logger";
11
13
  import { getPackageManager } from "./package-manager";
12
14
  import { parsePackageJSON, parseTOML, readFileSync } from "./parse";
15
+ import { requireAuth } from "./user";
13
16
  import { CommandLineArgsError, printWranglerBanner } from "./index";
17
+ import type { ConfigPath } from "./index";
14
18
 
15
19
  import type { Argv, ArgumentsCamelCase } from "yargs";
16
20
 
@@ -36,6 +40,12 @@ export async function initOptions(yargs: Argv) {
36
40
  describe: 'Answer "yes" to any prompts for new projects',
37
41
  type: "boolean",
38
42
  alias: "y",
43
+ })
44
+ .option("from-dash", {
45
+ describe: "Download script from the dashboard for local development",
46
+ type: "string",
47
+ requiresArg: true,
48
+ hidden: true,
39
49
  });
40
50
  }
41
51
 
@@ -61,7 +71,11 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
61
71
  const devDepsToInstall: string[] = [];
62
72
  const instructions: string[] = [];
63
73
  let shouldRunPackageManagerInstall = false;
64
- const creationDirectory = path.resolve(process.cwd(), args.name ?? "");
74
+ const fromDashScriptName = args["from-dash"] as string;
75
+ const creationDirectory = path.resolve(
76
+ process.cwd(),
77
+ (args.name ? args.name : fromDashScriptName) ?? ""
78
+ );
65
79
 
66
80
  if (args.site) {
67
81
  const gitDirectory =
@@ -90,6 +104,7 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
90
104
 
91
105
  // TODO: ask which directory to make the worker in (defaults to args.name)
92
106
  // TODO: if args.name isn't provided, ask what to name the worker
107
+ // Note: `--from-dash` will be a fallback creationDir/Worker name if none is provided.
93
108
 
94
109
  const wranglerTomlDestination = path.join(
95
110
  creationDirectory,
@@ -98,12 +113,15 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
98
113
  let justCreatedWranglerToml = false;
99
114
 
100
115
  if (fs.existsSync(wranglerTomlDestination)) {
116
+ let shouldContinue = false;
101
117
  logger.warn(
102
118
  `${path.relative(process.cwd(), wranglerTomlDestination)} already exists!`
103
119
  );
104
- const shouldContinue = await confirm(
105
- "Do you want to continue initializing this project?"
106
- );
120
+ if (!fromDashScriptName) {
121
+ shouldContinue = await confirm(
122
+ "Do you want to continue initializing this project?"
123
+ );
124
+ }
107
125
  if (!shouldContinue) {
108
126
  return;
109
127
  }
@@ -438,27 +456,23 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
438
456
  process.cwd(),
439
457
  path.join(creationDirectory, "./src/index.ts")
440
458
  );
441
-
442
- const newWorkerType = yesFlag
443
- ? "fetch"
444
- : await getNewWorkerType(newWorkerFilename);
445
-
446
- if (newWorkerType !== "none") {
447
- const template = getNewWorkerTemplate("ts", newWorkerType);
448
-
459
+ if (fromDashScriptName) {
460
+ const config = readConfig(args.config as ConfigPath, args);
461
+ const accountId = await requireAuth(config);
449
462
  await mkdir(path.join(creationDirectory, "./src"), {
450
463
  recursive: true,
451
464
  });
452
- await writeFile(
453
- path.join(creationDirectory, "./src/index.ts"),
454
- readFileSync(path.join(__dirname, `../templates/${template}`))
465
+
466
+ const dashScript = await fetchDashboardScript(
467
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
468
+ {
469
+ method: "GET",
470
+ }
455
471
  );
456
472
 
457
- logger.log(
458
- `✨ Created ${path.relative(
459
- process.cwd(),
460
- path.join(creationDirectory, "./src/index.ts")
461
- )}`
473
+ await writeFile(
474
+ path.join(creationDirectory, "./src/index.ts"),
475
+ dashScript
462
476
  );
463
477
 
464
478
  await writePackageJsonScriptsAndUpdateWranglerToml(
@@ -466,8 +480,39 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
466
480
  justCreatedWranglerToml,
467
481
  pathToPackageJson,
468
482
  "src/index.ts",
469
- getNewWorkerToml(newWorkerType)
483
+ {}
470
484
  );
485
+ } else {
486
+ const newWorkerType = yesFlag
487
+ ? "fetch"
488
+ : await getNewWorkerType(newWorkerFilename);
489
+
490
+ if (newWorkerType !== "none") {
491
+ const template = getNewWorkerTemplate("ts", newWorkerType);
492
+
493
+ await mkdir(path.join(creationDirectory, "./src"), {
494
+ recursive: true,
495
+ });
496
+ await writeFile(
497
+ path.join(creationDirectory, "./src/index.ts"),
498
+ readFileSync(path.join(__dirname, `../templates/${template}`))
499
+ );
500
+
501
+ logger.log(
502
+ `✨ Created ${path.relative(
503
+ process.cwd(),
504
+ path.join(creationDirectory, "./src/index.ts")
505
+ )}`
506
+ );
507
+
508
+ await writePackageJsonScriptsAndUpdateWranglerToml(
509
+ shouldWritePackageJsonScripts,
510
+ justCreatedWranglerToml,
511
+ pathToPackageJson,
512
+ "src/index.ts",
513
+ getNewWorkerToml(newWorkerType)
514
+ );
515
+ }
471
516
  }
472
517
  }
473
518
  } else {
@@ -477,35 +522,63 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
477
522
  path.join(creationDirectory, "./src/index.js")
478
523
  );
479
524
 
480
- const newWorkerType = yesFlag
481
- ? "fetch"
482
- : await getNewWorkerType(newWorkerFilename);
483
-
484
- if (newWorkerType !== "none") {
485
- const template = getNewWorkerTemplate("js", newWorkerType);
486
-
525
+ if (fromDashScriptName) {
526
+ const config = readConfig(args.config as ConfigPath, args);
527
+ const accountId = await requireAuth(config);
487
528
  await mkdir(path.join(creationDirectory, "./src"), {
488
529
  recursive: true,
489
530
  });
490
- await writeFile(
491
- path.join(creationDirectory, "./src/index.js"),
492
- readFileSync(path.join(__dirname, `../templates/${template}`))
531
+
532
+ const dashScript = await fetchDashboardScript(
533
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
534
+ {
535
+ method: "GET",
536
+ }
493
537
  );
494
538
 
495
- logger.log(
496
- `✨ Created ${path.relative(
497
- process.cwd(),
498
- path.join(creationDirectory, "./src/index.js")
499
- )}`
539
+ await writeFile(
540
+ path.join(creationDirectory, "./src/index.js"),
541
+ dashScript
500
542
  );
501
543
 
502
544
  await writePackageJsonScriptsAndUpdateWranglerToml(
503
545
  shouldWritePackageJsonScripts,
504
546
  justCreatedWranglerToml,
505
547
  pathToPackageJson,
506
- "src/index.js",
507
- getNewWorkerToml(newWorkerType)
548
+ "src/index.ts",
549
+ {}
508
550
  );
551
+ } else {
552
+ const newWorkerType = yesFlag
553
+ ? "fetch"
554
+ : await getNewWorkerType(newWorkerFilename);
555
+
556
+ if (newWorkerType !== "none") {
557
+ const template = getNewWorkerTemplate("js", newWorkerType);
558
+
559
+ await mkdir(path.join(creationDirectory, "./src"), {
560
+ recursive: true,
561
+ });
562
+ await writeFile(
563
+ path.join(creationDirectory, "./src/index.js"),
564
+ readFileSync(path.join(__dirname, `../templates/${template}`))
565
+ );
566
+
567
+ logger.log(
568
+ `✨ Created ${path.relative(
569
+ process.cwd(),
570
+ path.join(creationDirectory, "./src/index.js")
571
+ )}`
572
+ );
573
+
574
+ await writePackageJsonScriptsAndUpdateWranglerToml(
575
+ shouldWritePackageJsonScripts,
576
+ justCreatedWranglerToml,
577
+ pathToPackageJson,
578
+ "src/index.js",
579
+ getNewWorkerToml(newWorkerType)
580
+ );
581
+ }
509
582
  }
510
583
  }
511
584
  }
@@ -466,6 +466,14 @@ async function generateAssetsFetch(
466
466
  return deconstructedResponse;
467
467
  }
468
468
  } else if (hasFileExtension(assetName)) {
469
+ if ((asset = getAsset(assetName + ".html"))) {
470
+ deconstructedResponse.body = serveAsset(asset);
471
+ deconstructedResponse.headers.set(
472
+ "Content-Type",
473
+ getType(asset) || "application/octet-stream"
474
+ );
475
+ return deconstructedResponse;
476
+ }
469
477
  notFound();
470
478
  return deconstructedResponse;
471
479
  }
@@ -76,10 +76,13 @@ async function main() {
76
76
  namespace.get = (id) => {
77
77
  const stub = new DurableObjectStub(factory, id);
78
78
  stub.fetch = (...reqArgs) => {
79
- const url = `http://${host}${port ? `:${port}` : ""}`;
79
+ const requestFromArgs = new MiniflareRequest(...reqArgs);
80
+ const url = new URL(requestFromArgs.url);
81
+ url.host = host;
82
+ if (port !== undefined) url.port = port.toString();
80
83
  const request = new MiniflareRequest(
81
- url,
82
- new MiniflareRequest(...reqArgs)
84
+ url.toString(),
85
+ requestFromArgs
83
86
  );
84
87
  request.headers.set("x-miniflare-durable-object-name", name);
85
88
  request.headers.set("x-miniflare-durable-object-id", id.toString());
@@ -1,10 +1,16 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
+ import { FatalError } from "../errors";
4
5
  import { logger } from "../logger";
5
6
  import * as metrics from "../metrics";
6
7
  import { toUrlPath } from "../paths";
7
8
  import { isInPagesCI } from "./constants";
9
+ import {
10
+ EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR,
11
+ FunctionsNoRoutesError,
12
+ getFunctionsNoRoutesWarning,
13
+ } from "./errors";
8
14
  import { buildPlugin } from "./functions/buildPlugin";
9
15
  import { buildWorker } from "./functions/buildWorker";
10
16
  import { generateConfigFromFileTree } from "./functions/filepath-routing";
@@ -104,23 +110,37 @@ export const Handler = async ({
104
110
  }
105
111
 
106
112
  buildOutputDirectory ??= dirname(outfile);
107
-
108
- await buildFunctions({
109
- outfile,
110
- outputConfigPath,
111
- functionsDirectory: directory,
112
- minify,
113
- sourcemap,
114
- fallbackService,
115
- watch,
116
- plugin,
117
- buildOutputDirectory,
118
- nodeCompat,
119
- routesOutputPath,
120
- });
113
+ try {
114
+ await buildFunctions({
115
+ outfile,
116
+ outputConfigPath,
117
+ functionsDirectory: directory,
118
+ minify,
119
+ sourcemap,
120
+ fallbackService,
121
+ watch,
122
+ plugin,
123
+ buildOutputDirectory,
124
+ nodeCompat,
125
+ routesOutputPath,
126
+ });
127
+ } catch (e) {
128
+ if (e instanceof FunctionsNoRoutesError) {
129
+ throw new FatalError(
130
+ getFunctionsNoRoutesWarning(directory),
131
+ EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR
132
+ );
133
+ } else {
134
+ throw e;
135
+ }
136
+ }
121
137
  await metrics.sendMetricsEvent("build pages functions");
122
138
  };
123
139
 
140
+ /**
141
+ * Builds a Functions worker based on the functions directory, with filepath and handler based routing.
142
+ * @throws FunctionsNoRoutesError when there are no routes found in the functions directory
143
+ */
124
144
  export async function buildFunctions({
125
145
  outfile,
126
146
  outputConfigPath,
@@ -164,7 +184,13 @@ export async function buildFunctions({
164
184
  baseURL,
165
185
  });
166
186
 
167
- if (config.routes && routesOutputPath) {
187
+ if (!config.routes || config.routes.length === 0) {
188
+ throw new FunctionsNoRoutesError(
189
+ `Failed to find any routes while compiling Functions in: ${functionsDirectory}`
190
+ );
191
+ }
192
+
193
+ if (routesOutputPath) {
168
194
  const routesJSON = convertRoutesToRoutesJSONSpec(config.routes);
169
195
  writeFileSync(routesOutputPath, JSON.stringify(routesJSON, null, 2));
170
196
  }
@@ -8,4 +8,5 @@ export const SECONDS_TO_WAIT_FOR_PROXY = 5;
8
8
  export const isInPagesCI = !!process.env.CF_PAGES;
9
9
  /** The max number of rules in _routes.json */
10
10
  export const MAX_FUNCTIONS_ROUTES_RULES = 100;
11
+ export const MAX_FUNCTIONS_ROUTES_RULE_LENGTH = 100;
11
12
  export const ROUTES_SPEC_VERSION = 1;