wrangler 2.0.25 → 2.0.28

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/bin/wrangler.js +1 -1
  2. package/miniflare-dist/index.mjs +15 -3
  3. package/package.json +8 -6
  4. package/src/__tests__/configuration.test.ts +33 -29
  5. package/src/__tests__/dev.test.tsx +8 -6
  6. package/src/__tests__/generate.test.ts +2 -4
  7. package/src/__tests__/helpers/mock-cfetch.ts +33 -0
  8. package/src/__tests__/helpers/mock-get-zone-from-host.ts +8 -0
  9. package/src/__tests__/helpers/mock-known-routes.ts +7 -0
  10. package/src/__tests__/index.test.ts +30 -30
  11. package/src/__tests__/init.test.ts +537 -359
  12. package/src/__tests__/jest.setup.ts +7 -0
  13. package/src/__tests__/metrics.test.ts +1 -1
  14. package/src/__tests__/pages.test.ts +14 -0
  15. package/src/__tests__/publish.test.ts +59 -18
  16. package/src/__tests__/r2.test.ts +4 -3
  17. package/src/__tests__/tail.test.ts +53 -3
  18. package/src/__tests__/test-old-node-version.js +3 -3
  19. package/src/__tests__/user.test.ts +11 -0
  20. package/src/__tests__/worker-namespace.test.ts +37 -35
  21. package/src/api/dev.ts +1 -0
  22. package/src/bundle.ts +1 -1
  23. package/src/cfetch/internal.ts +118 -1
  24. package/src/config/environment.ts +1 -1
  25. package/src/config/index.ts +4 -4
  26. package/src/config/validation-helpers.ts +19 -6
  27. package/src/config/validation.ts +11 -5
  28. package/src/config-cache.ts +2 -1
  29. package/src/create-worker-upload-form.ts +29 -26
  30. package/src/dev/dev.tsx +4 -0
  31. package/src/dev/remote.tsx +10 -1
  32. package/src/dev.tsx +36 -8
  33. package/src/{worker-namespace.ts → dispatch-namespace.ts} +18 -18
  34. package/src/generate.ts +1 -1
  35. package/src/index.tsx +54 -8
  36. package/src/init.ts +111 -38
  37. package/src/{metrics/is-ci.ts → is-ci.ts} +0 -0
  38. package/src/metrics/metrics-config.ts +1 -1
  39. package/src/metrics/send-event.ts +5 -5
  40. package/src/miniflare-cli/assets.ts +8 -0
  41. package/src/miniflare-cli/index.ts +6 -3
  42. package/src/pages/build.tsx +41 -15
  43. package/src/pages/constants.ts +1 -0
  44. package/src/pages/dev.tsx +93 -37
  45. package/src/pages/errors.ts +22 -0
  46. package/src/pages/functions/routes-consolidation.test.ts +185 -1
  47. package/src/pages/functions/routes-consolidation.ts +46 -2
  48. package/src/pages/functions/routes-transformation.ts +0 -3
  49. package/src/pages/functions.tsx +96 -0
  50. package/src/pages/index.tsx +65 -55
  51. package/src/pages/publish.tsx +27 -16
  52. package/src/proxy.ts +10 -0
  53. package/src/publish.ts +19 -4
  54. package/src/r2.ts +4 -4
  55. package/src/tail/filters.ts +3 -1
  56. package/src/tail/printing.ts +2 -0
  57. package/src/user/user.tsx +6 -4
  58. package/src/whoami.tsx +5 -5
  59. package/src/worker.ts +3 -2
  60. package/src/zones.ts +91 -0
  61. package/templates/pages-template-plugin.ts +16 -4
  62. package/templates/pages-template-worker.ts +16 -5
  63. package/templates/service-bindings-module-facade.js +10 -7
  64. package/templates/service-bindings-sw-facade.js +10 -7
  65. package/wrangler-dist/cli.d.ts +8 -3
  66. package/wrangler-dist/cli.js +6757 -1639
@@ -3,6 +3,7 @@
3
3
  import * as Build from "./build";
4
4
  import * as Deployments from "./deployments";
5
5
  import * as Dev from "./dev";
6
+ import * as Functions from "./functions";
6
7
  import * as Projects from "./projects";
7
8
  import * as Publish from "./publish";
8
9
  import * as Upload from "./upload";
@@ -19,66 +20,75 @@ process.on("SIGTERM", () => {
19
20
  });
20
21
 
21
22
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
22
- return yargs
23
- .command(
24
- "dev [directory] [-- command..]",
25
- "🧑‍💻 Develop your full-stack Pages application locally",
26
- Dev.Options,
27
- Dev.Handler
28
- )
29
- .command("functions", false, (yargs) =>
30
- // we hide this command from help output because
31
- // it's not meant to be used directly right now
32
- {
33
- return yargs.command(
34
- "build [directory]",
35
- "Compile a folder of Cloudflare Pages Functions into a single Worker",
36
- Build.Options,
37
- Build.Handler
38
- );
39
- }
40
- )
41
- .command("project", "⚡️ Interact with your Pages projects", (yargs) =>
42
- yargs
43
- .command(
44
- "list",
45
- "List your Cloudflare Pages projects",
46
- Projects.ListOptions,
47
- Projects.ListHandler
48
- )
49
- .command(
50
- "create [project-name]",
51
- "Create a new Cloudflare Pages project",
52
- Projects.CreateOptions,
53
- Projects.CreateHandler
54
- )
55
- .command("upload [directory]", false, Upload.Options, Upload.Handler)
56
- .epilogue(pagesBetaWarning)
57
- )
58
- .command(
59
- "deployment",
60
- "🚀 Interact with the deployments of a project",
61
- (yargs) =>
23
+ return (
24
+ yargs
25
+ .command(
26
+ "dev [directory] [-- command..]",
27
+ "🧑‍💻 Develop your full-stack Pages application locally",
28
+ Dev.Options,
29
+ Dev.Handler
30
+ )
31
+ /**
32
+ * `wrangler pages functions` is meant for internal use only for now,
33
+ * so let's hide this command from the help output
34
+ */
35
+ .command("functions", false, (yargs) =>
36
+ yargs
37
+ .command(
38
+ "build [directory]",
39
+ "Compile a folder of Cloudflare Pages Functions into a single Worker",
40
+ Build.Options,
41
+ Build.Handler
42
+ )
43
+ .command(
44
+ "optimize-routes [routesPath] [outputRoutesPath]",
45
+ "Consolidate and optimize the route paths declared in _routes.json",
46
+ Functions.OptimizeRoutesOptions,
47
+ Functions.OptimizeRoutesHandler
48
+ )
49
+ )
50
+ .command("project", "⚡️ Interact with your Pages projects", (yargs) =>
62
51
  yargs
63
52
  .command(
64
53
  "list",
65
- "List deployments in your Cloudflare Pages project",
66
- Deployments.ListOptions,
67
- Deployments.ListHandler
54
+ "List your Cloudflare Pages projects",
55
+ Projects.ListOptions,
56
+ Projects.ListHandler
68
57
  )
69
58
  .command(
70
- "create [directory]",
71
- "🆙 Publish a directory of static assets as a Pages deployment",
72
- Publish.Options,
73
- Publish.Handler
59
+ "create [project-name]",
60
+ "Create a new Cloudflare Pages project",
61
+ Projects.CreateOptions,
62
+ Projects.CreateHandler
74
63
  )
64
+ .command("upload [directory]", false, Upload.Options, Upload.Handler)
75
65
  .epilogue(pagesBetaWarning)
76
- )
77
- .command(
78
- "publish [directory]",
79
- "🆙 Publish a directory of static assets as a Pages deployment",
80
- Publish.Options,
81
- Publish.Handler
82
- )
83
- .epilogue(pagesBetaWarning);
66
+ )
67
+ .command(
68
+ "deployment",
69
+ "🚀 Interact with the deployments of a project",
70
+ (yargs) =>
71
+ yargs
72
+ .command(
73
+ "list",
74
+ "List deployments in your Cloudflare Pages project",
75
+ Deployments.ListOptions,
76
+ Deployments.ListHandler
77
+ )
78
+ .command(
79
+ "create [directory]",
80
+ "🆙 Publish a directory of static assets as a Pages deployment",
81
+ Publish.Options,
82
+ Publish.Handler
83
+ )
84
+ .epilogue(pagesBetaWarning)
85
+ )
86
+ .command(
87
+ "publish [directory]",
88
+ "🆙 Publish a directory of static assets as a Pages deployment",
89
+ Publish.Options,
90
+ Publish.Handler
91
+ )
92
+ .epilogue(pagesBetaWarning)
93
+ );
84
94
  };
@@ -16,6 +16,7 @@ import * as metrics from "../metrics";
16
16
  import { requireAuth } from "../user";
17
17
  import { buildFunctions } from "./build";
18
18
  import { PAGES_CONFIG_CACHE_FILENAME } from "./constants";
19
+ import { FunctionsNoRoutesError, getFunctionsNoRoutesWarning } from "./errors";
19
20
  import {
20
21
  isRoutesJSONSpec,
21
22
  optimizeRoutesJSONSpec,
@@ -256,18 +257,24 @@ export const Handler = async ({
256
257
  const routesOutputPath = join(tmpdir(), `_routes-${Math.random()}.json`);
257
258
  if (existsSync(functionsDirectory)) {
258
259
  const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
259
-
260
- await new Promise((resolve) =>
261
- buildFunctions({
260
+ try {
261
+ await buildFunctions({
262
262
  outfile,
263
263
  functionsDirectory,
264
- onEnd: () => resolve(null),
264
+ onEnd: () => {},
265
265
  buildOutputDirectory: dirname(outfile),
266
266
  routesOutputPath,
267
- })
268
- );
269
-
270
- builtFunctions = readFileSync(outfile, "utf-8");
267
+ });
268
+ builtFunctions = readFileSync(outfile, "utf-8");
269
+ } catch (e) {
270
+ if (e instanceof FunctionsNoRoutesError) {
271
+ logger.warn(
272
+ getFunctionsNoRoutesWarning(functionsDirectory, "skipping")
273
+ );
274
+ } else {
275
+ throw e;
276
+ }
277
+ }
271
278
  }
272
279
 
273
280
  const manifest = await upload({ directory, accountId, projectName });
@@ -305,32 +312,34 @@ export const Handler = async ({
305
312
  _redirects = readFileSync(join(directory, "_redirects"), "utf-8");
306
313
  } catch {}
307
314
 
308
- try {
309
- _routes = readFileSync(routesOutputPath, "utf-8");
310
- } catch {}
311
-
312
315
  try {
313
316
  _workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
314
317
  } catch {}
315
318
 
316
319
  if (_headers) {
317
320
  formData.append("_headers", new File([_headers], "_headers"));
321
+ logger.log(`✨ Uploading _headers`);
318
322
  }
319
323
 
320
324
  if (_redirects) {
321
325
  formData.append("_redirects", new File([_redirects], "_redirects"));
322
- }
323
-
324
- if (_routes) {
325
- formData.append("_routes.json", new File([_routes], "_routes.json"));
326
+ logger.log(`✨ Uploading _redirects`);
326
327
  }
327
328
 
328
329
  if (builtFunctions) {
329
330
  formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
331
+ logger.log(`✨ Uploading Functions`);
332
+ try {
333
+ _routes = readFileSync(routesOutputPath, "utf-8");
334
+ if (_routes) {
335
+ formData.append("_routes.json", new File([_routes], "_routes.json"));
336
+ }
337
+ } catch {}
330
338
  } else if (_workerJS) {
331
339
  // Advanced Mode
332
340
  // https://developers.cloudflare.com/pages/platform/functions/#advanced-mode
333
341
  formData.append("_worker.js", new File([_workerJS], "_worker.js"));
342
+ logger.log(`✨ Uploading _worker.js`);
334
343
 
335
344
  try {
336
345
  // In advanced mode, developers can specify a custom _routes.json
@@ -348,6 +357,8 @@ export const Handler = async ({
348
357
  }
349
358
 
350
359
  _routes = JSON.stringify(optimizeRoutesJSONSpec(advancedModeRoutes));
360
+ formData.append("_routes.json", new File([_routes], "_routes.json"));
361
+ logger.log(`✨ Uploading _routes.json`);
351
362
 
352
363
  logger.warn(
353
364
  `🚨 _routes.json is an experimental feature and is subject to change. Don't use unless you really must!`
package/src/proxy.ts CHANGED
@@ -177,6 +177,7 @@ export function usePreviewServer({
177
177
  const cleanupListeners: (() => void)[] = [];
178
178
 
179
179
  // create a ClientHttp2Session
180
+ logger.debug("PREVIEW URL:", `https://${previewToken.host}`);
180
181
  const remote = connect(`https://${previewToken.host}`);
181
182
  cleanupListeners.push(() => remote.destroy());
182
183
 
@@ -221,6 +222,15 @@ export function usePreviewServer({
221
222
  }
222
223
  }
223
224
  const request = message.pipe(remote.request(headers));
225
+ logger.debug(
226
+ "WORKER REQUEST",
227
+ new Date().toLocaleTimeString(),
228
+ method,
229
+ url
230
+ );
231
+ logger.debug("HEADERS", JSON.stringify(headers, null, 2));
232
+ logger.debug("PREVIEW TOKEN", previewToken);
233
+
224
234
  request.on("response", (responseHeaders) => {
225
235
  const status = responseHeaders[":status"] ?? 500;
226
236
 
package/src/publish.ts CHANGED
@@ -36,6 +36,8 @@ type Props = {
36
36
  compatibilityDate: string | undefined;
37
37
  compatibilityFlags: string[] | undefined;
38
38
  assetPaths: AssetPaths | undefined;
39
+ vars: Record<string, string> | undefined;
40
+ defines: Record<string, string> | undefined;
39
41
  triggers: string[] | undefined;
40
42
  routes: string[] | undefined;
41
43
  legacyEnv: boolean | undefined;
@@ -382,7 +384,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
382
384
  tsconfig: props.tsconfig ?? config.tsconfig,
383
385
  minify,
384
386
  nodeCompat,
385
- define: config.define,
387
+ define: { ...config.define, ...props.defines },
386
388
  checkFetch: false,
387
389
  assets: config.assets && {
388
390
  ...config.assets,
@@ -430,7 +432,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
430
432
  ? { binding: "__STATIC_CONTENT", id: assets.namespace }
431
433
  : []
432
434
  ),
433
- vars: config.vars,
435
+ vars: { ...config.vars, ...props.vars },
434
436
  wasm_modules: config.wasm_modules,
435
437
  text_blobs: {
436
438
  ...config.text_blobs,
@@ -443,7 +445,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
443
445
  durable_objects: config.durable_objects,
444
446
  r2_buckets: config.r2_buckets,
445
447
  services: config.services,
446
- worker_namespaces: config.worker_namespaces,
448
+ dispatch_namespaces: config.dispatch_namespaces,
447
449
  logfwdr: config.logfwdr,
448
450
  unsafe: config.unsafe?.bindings,
449
451
  };
@@ -470,6 +472,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
470
472
  compatibility_flags:
471
473
  props.compatibilityFlags ?? config.compatibility_flags,
472
474
  usage_model: config.usage_model,
475
+ keep_bindings: true,
473
476
  };
474
477
 
475
478
  void printBundleSize(
@@ -482,7 +485,19 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
482
485
  kv_namespaces: config.kv_namespaces,
483
486
  text_blobs: config.text_blobs,
484
487
  };
485
- printBindings(withoutStaticAssets);
488
+
489
+ // mask anything that was overridden in cli args
490
+ // so that we don't log potential secrets into the terminal
491
+ const maskedVars = { ...withoutStaticAssets.vars };
492
+ for (const key of Object.keys(maskedVars)) {
493
+ if (maskedVars[key] !== config.vars[key]) {
494
+ // This means it was overridden in cli args
495
+ // so let's mask it
496
+ maskedVars[key] = "(hidden)";
497
+ }
498
+ }
499
+
500
+ printBindings({ ...withoutStaticAssets, vars: maskedVars });
486
501
 
487
502
  if (!props.dryRun) {
488
503
  // Upload the script so it has time to propagate.
package/src/r2.ts CHANGED
@@ -33,10 +33,10 @@ export async function createR2Bucket(
33
33
  accountId: string,
34
34
  bucketName: string
35
35
  ): Promise<void> {
36
- return await fetchResult<void>(
37
- `/accounts/${accountId}/r2/buckets/${bucketName}`,
38
- { method: "PUT" }
39
- );
36
+ return await fetchResult<void>(`/accounts/${accountId}/r2/buckets`, {
37
+ method: "POST",
38
+ body: JSON.stringify({ name: bucketName }),
39
+ });
40
40
  }
41
41
 
42
42
  /**
@@ -58,13 +58,14 @@ type OutcomeFilter = {
58
58
 
59
59
  /**
60
60
  * There are five possible outcomes we can get, three of which
61
- * (exception, exceededCpu, and unknown) are considered errors
61
+ * (exception, exceededCpu, exceededMemory, and unknown) are considered errors
62
62
  */
63
63
  export type Outcome =
64
64
  | "ok"
65
65
  | "canceled"
66
66
  | "exception"
67
67
  | "exceededCpu"
68
+ | "exceededMemory"
68
69
  | "unknown";
69
70
 
70
71
  /**
@@ -210,6 +211,7 @@ function parseOutcome(
210
211
  case "error":
211
212
  outcomes.add("exception");
212
213
  outcomes.add("exceededCpu");
214
+ outcomes.add("exceededMemory");
213
215
  outcomes.add("unknown");
214
216
  break;
215
217
 
@@ -96,6 +96,8 @@ function prettifyOutcome(outcome: Outcome): string {
96
96
  return "Canceled";
97
97
  case "exceededCpu":
98
98
  return "Exceeded CPU Limit";
99
+ case "exceededMemory":
100
+ return "Exceeded Memory Limit";
99
101
  case "exception":
100
102
  return "Exception Thrown";
101
103
  case "unknown":
package/src/user/user.tsx CHANGED
@@ -224,6 +224,7 @@ import {
224
224
  saveToConfigCache,
225
225
  } from "../config-cache";
226
226
  import { getGlobalWranglerConfigPath } from "../global-wrangler-config-path";
227
+ import { CI } from "../is-ci";
227
228
  import isInteractive from "../is-interactive";
228
229
  import { logger } from "../logger";
229
230
  import openInBrowser from "../open-in-browser";
@@ -878,10 +879,11 @@ type LoginProps = {
878
879
  export async function loginOrRefreshIfRequired(): Promise<boolean> {
879
880
  // TODO: if there already is a token, then try refreshing
880
881
  // TODO: ask permission before opening browser
882
+ const { isCI } = CI;
881
883
  if (!getAPIToken()) {
882
884
  // Not logged in.
883
885
  // If we are not interactive, we cannot ask the user to login
884
- return isInteractive() && (await login());
886
+ return isInteractive() && !isCI() && (await login());
885
887
  } else if (isAccessTokenExpired()) {
886
888
  // We're logged in, but the refresh token seems to have expired,
887
889
  // so let's try to refresh it
@@ -891,7 +893,7 @@ export async function loginOrRefreshIfRequired(): Promise<boolean> {
891
893
  return true;
892
894
  } else {
893
895
  // If the refresh token isn't valid, then we ask the user to login again
894
- return isInteractive() && (await login());
896
+ return isInteractive() && !isCI() && (await login());
895
897
  }
896
898
  } else {
897
899
  return true;
@@ -1088,7 +1090,7 @@ export async function getAccountId(): Promise<string | undefined> {
1088
1090
  return accounts[0].id;
1089
1091
  }
1090
1092
 
1091
- if (isInteractive()) {
1093
+ if (isInteractive() && !CI.isCI()) {
1092
1094
  const account = await new Promise<{ id: string; name: string }>(
1093
1095
  (resolve, reject) => {
1094
1096
  const { unmount } = render(
@@ -1128,7 +1130,7 @@ export async function requireAuth(config: {
1128
1130
  }): Promise<string> {
1129
1131
  const loggedIn = await loginOrRefreshIfRequired();
1130
1132
  if (!loggedIn) {
1131
- if (!isInteractive()) {
1133
+ if (!isInteractive() || CI.isCI()) {
1132
1134
  throw new Error(
1133
1135
  "In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN environment variable for wrangler to work. Please go to https://developers.cloudflare.com/api/tokens/create/ for instructions on how to create an api token, and assign its value to CLOUDFLARE_API_TOKEN."
1134
1136
  );
package/src/whoami.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Text, render } from "ink";
2
2
  import Table from "ink-table";
3
- import React from "react";
3
+ import React, { Fragment } from "react";
4
4
  import { fetchListResult, fetchResult } from "./cfetch";
5
5
  import { logger } from "./logger";
6
6
  import { getAPIToken, getAuthFromEnv, getScopes } from "./user";
@@ -64,12 +64,12 @@ function Permissions(props: {
64
64
  and re-login.
65
65
  </Text>
66
66
  <Text>Scope (Access)</Text>
67
- {permissions.map(([type, name]) => (
68
- <>
67
+ {permissions.map(([scope, access], index) => (
68
+ <Fragment key={`${scope}${index}`}>
69
69
  <Text>
70
- - {type} {name && `(${name})`}
70
+ - {scope} {access && `(${access})`}
71
71
  </Text>
72
- </>
72
+ </Fragment>
73
73
  ))}
74
74
  </>
75
75
  ) : null
package/src/worker.ts CHANGED
@@ -122,7 +122,7 @@ interface CfService {
122
122
  environment?: string;
123
123
  }
124
124
 
125
- interface CfWorkerNamespace {
125
+ interface CfDispatchNamespace {
126
126
  binding: string;
127
127
  namespace: string;
128
128
  }
@@ -183,7 +183,7 @@ export interface CfWorkerInit {
183
183
  durable_objects: { bindings: CfDurableObject[] } | undefined;
184
184
  r2_buckets: CfR2Bucket[] | undefined;
185
185
  services: CfService[] | undefined;
186
- worker_namespaces: CfWorkerNamespace[] | undefined;
186
+ dispatch_namespaces: CfDispatchNamespace[] | undefined;
187
187
  logfwdr: CfLogfwdr | undefined;
188
188
  unsafe: CfUnsafeBinding[] | undefined;
189
189
  };
@@ -191,6 +191,7 @@ export interface CfWorkerInit {
191
191
  compatibility_date: string | undefined;
192
192
  compatibility_flags: string[] | undefined;
193
193
  usage_model: "bundled" | "unbound" | undefined;
194
+ keep_bindings: boolean | undefined;
194
195
  }
195
196
 
196
197
  export interface CfWorkerContext {
package/src/zones.ts CHANGED
@@ -74,3 +74,94 @@ export async function getZoneIdFromHost(host: string): Promise<string> {
74
74
 
75
75
  throw new Error(`Could not find zone for ${host}`);
76
76
  }
77
+
78
+ /**
79
+ * An object holding information about an assigned worker route, returned from the API
80
+ */
81
+ interface WorkerRoute {
82
+ id: string;
83
+ pattern: string;
84
+ script: string;
85
+ }
86
+
87
+ /**
88
+ * Given a zone within the user's account, return a list of all assigned worker routes
89
+ */
90
+ export async function getRoutesForZone(zone: string): Promise<WorkerRoute[]> {
91
+ const routes = await fetchListResult<WorkerRoute>(
92
+ `/zones/${zone}/workers/routes`
93
+ );
94
+ return routes;
95
+ }
96
+
97
+ /**
98
+ * Given two strings, return the levenshtein distance between them as a simple text match heuristic
99
+ */
100
+ function distanceBetween(a: string, b: string, cache = new Map()): number {
101
+ if (cache.has(`${a}|${b}`)) {
102
+ return cache.get(`${a}|${b}`);
103
+ }
104
+ let result;
105
+ if (b == "") {
106
+ result = a.length;
107
+ } else if (a == "") {
108
+ result = b.length;
109
+ } else if (a[0] === b[0]) {
110
+ result = distanceBetween(a.slice(1), b.slice(1), cache);
111
+ } else {
112
+ result =
113
+ 1 +
114
+ Math.min(
115
+ distanceBetween(a.slice(1), b, cache),
116
+ distanceBetween(a, b.slice(1), cache),
117
+ distanceBetween(a.slice(1), b.slice(1), cache)
118
+ );
119
+ }
120
+ cache.set(`${a}|${b}`, result);
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Given an invalid route, sort the valid routes by closeness to the invalid route (levenstein distance)
126
+ */
127
+ export function findClosestRoute(
128
+ providedRoute: string,
129
+ assignedRoutes: WorkerRoute[]
130
+ ): WorkerRoute[] {
131
+ return assignedRoutes.sort((a, b) => {
132
+ const distanceA = distanceBetween(providedRoute, a.pattern);
133
+ const distanceB = distanceBetween(providedRoute, b.pattern);
134
+ return distanceA - distanceB;
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Given a route (must be assigned and within the correct zone), return the name of the worker assigned to it
140
+ */
141
+ export async function getWorkerForZone(worker: string) {
142
+ const zone = await getZoneForRoute(worker);
143
+ if (!zone) {
144
+ throw new Error(
145
+ `The route '${worker}' is not part of one of your zones. Either add this zone from the Cloudflare dashboard, or try using a route within one of your existing zones.`
146
+ );
147
+ }
148
+ const routes = await getRoutesForZone(zone.id);
149
+
150
+ const scriptName = routes.find((route) => route.pattern === worker)?.script;
151
+
152
+ if (!scriptName) {
153
+ const closestRoute = findClosestRoute(worker, routes)?.[0];
154
+
155
+ if (!closestRoute) {
156
+ throw new Error(
157
+ `The route '${worker}' has no workers assigned. You can assign a worker to it from wrangler.toml or the Cloudflare dashboard`
158
+ );
159
+ } else {
160
+ throw new Error(
161
+ `The route '${worker}' has no workers assigned. Did you mean to tail the route '${closestRoute.pattern}'?`
162
+ );
163
+ }
164
+ }
165
+
166
+ return scriptName;
167
+ }
@@ -1,5 +1,8 @@
1
1
  import { match } from "path-to-regexp";
2
2
 
3
+ //note: this explicitly does not include the * character, as pages requires this
4
+ const escapeRegex = /[.+?^${}()|[\]\\]/g;
5
+
3
6
  type HTTPMethod =
4
7
  | "HEAD"
5
8
  | "OPTIONS"
@@ -67,8 +70,13 @@ function* executeRequest(request: Request, relativePathname: string) {
67
70
  continue;
68
71
  }
69
72
 
70
- const routeMatcher = match(route.routePath, { end: false });
71
- const mountMatcher = match(route.mountPath, { end: false });
73
+ // replaces with "\\$&", this prepends a backslash to the matched string, e.g. "[" becomes "\["
74
+ const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
75
+ end: false,
76
+ });
77
+ const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
78
+ end: false,
79
+ });
72
80
  const matchResult = routeMatcher(relativePathname);
73
81
  const mountMatchResult = mountMatcher(relativePathname);
74
82
  if (matchResult && mountMatchResult) {
@@ -88,8 +96,12 @@ function* executeRequest(request: Request, relativePathname: string) {
88
96
  continue;
89
97
  }
90
98
 
91
- const routeMatcher = match(route.routePath, { end: true });
92
- const mountMatcher = match(route.mountPath, { end: false });
99
+ const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
100
+ end: true,
101
+ });
102
+ const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
103
+ end: false,
104
+ });
93
105
  const matchResult = routeMatcher(relativePathname);
94
106
  const mountMatchResult = mountMatcher(relativePathname);
95
107
  if (matchResult && mountMatchResult && route.modules.length) {
@@ -1,5 +1,8 @@
1
1
  import { match } from "path-to-regexp";
2
2
 
3
+ //note: this explicitly does not include the * character, as pages requires this
4
+ const escapeRegex = /[.+?^${}()|[\]\\]/g;
5
+
3
6
  type HTTPMethod =
4
7
  | "HEAD"
5
8
  | "OPTIONS"
@@ -61,8 +64,13 @@ function* executeRequest(request: Request) {
61
64
  continue;
62
65
  }
63
66
 
64
- const routeMatcher = match(route.routePath, { end: false });
65
- const mountMatcher = match(route.mountPath, { end: false });
67
+ // replaces with "\\$&", this prepends a backslash to the matched string, e.g. "[" becomes "\["
68
+ const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
69
+ end: false,
70
+ });
71
+ const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
72
+ end: false,
73
+ });
66
74
  const matchResult = routeMatcher(requestPath);
67
75
  const mountMatchResult = mountMatcher(requestPath);
68
76
  if (matchResult && mountMatchResult) {
@@ -81,9 +89,12 @@ function* executeRequest(request: Request) {
81
89
  if (route.method && route.method !== request.method) {
82
90
  continue;
83
91
  }
84
-
85
- const routeMatcher = match(route.routePath, { end: true });
86
- const mountMatcher = match(route.mountPath, { end: false });
92
+ const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
93
+ end: true,
94
+ });
95
+ const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
96
+ end: false,
97
+ });
87
98
  const matchResult = routeMatcher(requestPath);
88
99
  const mountMatchResult = mountMatcher(requestPath);
89
100
  if (matchResult && mountMatchResult && route.modules.length) {
@@ -16,20 +16,23 @@ export default {
16
16
  if (details) {
17
17
  facadeEnv[name] = {
18
18
  async fetch(...reqArgs) {
19
+ const reqFromArgs = new Request(...reqArgs);
19
20
  if (details.headers) {
20
- const req = new Request(...reqArgs);
21
21
  for (const [key, value] of Object.entries(details.headers)) {
22
22
  // In remote mode, you need to add a couple of headers
23
23
  // to make sure it's talking to the 'dev' preview session
24
24
  // (much like wrangler dev already does via proxy.ts)
25
- req.headers.set(key, value);
25
+ reqFromArgs.headers.set(key, value);
26
26
  }
27
- return env[name].fetch(req);
27
+ return env[name].fetch(reqFromArgs);
28
28
  }
29
- const url = `${details.protocol}://${details.host}${
30
- details.port ? `:${details.port}` : ""
31
- }`;
32
- const request = new Request(url, ...reqArgs);
29
+
30
+ const url = new URL(reqFromArgs.url);
31
+ url.protocol = details.protocol;
32
+ url.host = details.host;
33
+ if (details.port !== undefined) url.port = details.port;
34
+
35
+ const request = new Request(url.toString(), reqFromArgs);
33
36
  return fetch(request);
34
37
  },
35
38
  };