wrangler 2.0.26 → 2.0.29

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 (62) hide show
  1. package/bin/wrangler.js +1 -1
  2. package/miniflare-dist/index.mjs +54 -46
  3. package/package.json +6 -4
  4. package/src/__tests__/api-dev.test.ts +19 -0
  5. package/src/__tests__/configuration.test.ts +33 -29
  6. package/src/__tests__/dev.test.tsx +8 -6
  7. package/src/__tests__/generate.test.ts +2 -4
  8. package/src/__tests__/helpers/hello-world-worker.js +5 -0
  9. package/src/__tests__/helpers/mock-get-zone-from-host.ts +8 -0
  10. package/src/__tests__/helpers/mock-known-routes.ts +7 -0
  11. package/src/__tests__/index.test.ts +30 -30
  12. package/src/__tests__/jest.setup.ts +17 -0
  13. package/src/__tests__/metrics.test.ts +1 -1
  14. package/src/__tests__/pages.test.ts +829 -103
  15. package/src/__tests__/paths.test.ts +17 -0
  16. package/src/__tests__/publish.test.ts +59 -18
  17. package/src/__tests__/r2.test.ts +4 -3
  18. package/src/__tests__/tail.test.ts +34 -0
  19. package/src/__tests__/test-old-node-version.js +3 -3
  20. package/src/__tests__/user.test.ts +11 -0
  21. package/src/__tests__/worker-namespace.test.ts +37 -35
  22. package/src/api/dev.ts +74 -28
  23. package/src/bundle.ts +14 -11
  24. package/src/cfetch/internal.ts +81 -3
  25. package/src/cli.ts +1 -1
  26. package/src/config/environment.ts +1 -1
  27. package/src/config/index.ts +4 -4
  28. package/src/config/validation-helpers.ts +19 -6
  29. package/src/config/validation.ts +11 -5
  30. package/src/config-cache.ts +2 -1
  31. package/src/create-worker-upload-form.ts +29 -26
  32. package/src/dev/local.tsx +317 -169
  33. package/src/dev/remote.tsx +10 -1
  34. package/src/dev/start-server.ts +412 -0
  35. package/src/dev.tsx +341 -157
  36. package/src/{worker-namespace.ts → dispatch-namespace.ts} +18 -18
  37. package/src/entry.ts +2 -1
  38. package/src/generate.ts +1 -1
  39. package/src/index.tsx +54 -8
  40. package/src/init.ts +5 -5
  41. package/src/{metrics/is-ci.ts → is-ci.ts} +0 -0
  42. package/src/metrics/metrics-config.ts +1 -1
  43. package/src/metrics/send-event.ts +6 -5
  44. package/src/miniflare-cli/assets.ts +4 -65
  45. package/src/miniflare-cli/index.ts +36 -32
  46. package/src/pages/constants.ts +3 -0
  47. package/src/pages/dev.tsx +10 -15
  48. package/src/pages/functions/buildPlugin.ts +2 -1
  49. package/src/pages/functions/buildWorker.ts +2 -1
  50. package/src/pages/functions/routes-transformation.test.ts +12 -1
  51. package/src/pages/functions/routes-transformation.ts +7 -1
  52. package/src/pages/publish.tsx +82 -38
  53. package/src/paths.ts +20 -1
  54. package/src/proxy.ts +10 -0
  55. package/src/publish.ts +19 -4
  56. package/src/r2.ts +4 -4
  57. package/src/user/user.tsx +6 -4
  58. package/src/whoami.tsx +5 -5
  59. package/src/worker.ts +10 -9
  60. package/src/zones.ts +91 -0
  61. package/wrangler-dist/cli.d.ts +22 -8
  62. package/wrangler-dist/cli.js +7757 -2315
@@ -1,5 +1,9 @@
1
1
  import { toUrlPath } from "../../paths";
2
- import { MAX_FUNCTIONS_ROUTES_RULES, ROUTES_SPEC_VERSION } from "../constants";
2
+ import {
3
+ MAX_FUNCTIONS_ROUTES_RULES,
4
+ ROUTES_SPEC_VERSION,
5
+ ROUTES_SPEC_DESCRIPTION,
6
+ } from "../constants";
3
7
  import {
4
8
  compareRoutes,
5
9
  convertRoutesToGlobPatterns,
@@ -164,6 +168,7 @@ describe("route-paths-to-glob-patterns", () => {
164
168
  ])
165
169
  ).toEqual({
166
170
  version: ROUTES_SPEC_VERSION,
171
+ description: ROUTES_SPEC_DESCRIPTION,
167
172
  include: ["/middleware/*", "/foo/*", "/api/foo/bar"],
168
173
  exclude: [],
169
174
  });
@@ -176,6 +181,7 @@ describe("route-paths-to-glob-patterns", () => {
176
181
  }
177
182
  expect(convertRoutesToRoutesJSONSpec(routes)).toEqual({
178
183
  version: ROUTES_SPEC_VERSION,
184
+ description: ROUTES_SPEC_DESCRIPTION,
179
185
  include: ["/*"],
180
186
  exclude: [],
181
187
  });
@@ -197,6 +203,7 @@ describe("route-paths-to-glob-patterns", () => {
197
203
  expect(
198
204
  optimizeRoutesJSONSpec({
199
205
  version: ROUTES_SPEC_VERSION,
206
+ description: ROUTES_SPEC_DESCRIPTION,
200
207
  exclude: [],
201
208
  include: [
202
209
  "/api/foo/bar",
@@ -208,6 +215,7 @@ describe("route-paths-to-glob-patterns", () => {
208
215
  })
209
216
  ).toEqual({
210
217
  version: ROUTES_SPEC_VERSION,
218
+ description: ROUTES_SPEC_DESCRIPTION,
211
219
  include: ["/middleware/*", "/foo/*", "/api/foo/bar"],
212
220
  exclude: [],
213
221
  });
@@ -221,11 +229,13 @@ describe("route-paths-to-glob-patterns", () => {
221
229
  expect(
222
230
  optimizeRoutesJSONSpec({
223
231
  version: ROUTES_SPEC_VERSION,
232
+ description: ROUTES_SPEC_DESCRIPTION,
224
233
  include,
225
234
  exclude: [],
226
235
  })
227
236
  ).toEqual({
228
237
  version: ROUTES_SPEC_VERSION,
238
+ description: ROUTES_SPEC_DESCRIPTION,
229
239
  include: ["/*"],
230
240
  exclude: [],
231
241
  });
@@ -239,6 +249,7 @@ describe("route-paths-to-glob-patterns", () => {
239
249
  expect(
240
250
  optimizeRoutesJSONSpec({
241
251
  version: ROUTES_SPEC_VERSION,
252
+ description: ROUTES_SPEC_DESCRIPTION,
242
253
  include,
243
254
  exclude: [],
244
255
  }).include.length
@@ -1,12 +1,17 @@
1
1
  import { join as pathJoin } from "node:path";
2
2
  import { toUrlPath } from "../../paths";
3
- import { MAX_FUNCTIONS_ROUTES_RULES, ROUTES_SPEC_VERSION } from "../constants";
3
+ import {
4
+ MAX_FUNCTIONS_ROUTES_RULES,
5
+ ROUTES_SPEC_DESCRIPTION,
6
+ ROUTES_SPEC_VERSION,
7
+ } from "../constants";
4
8
  import { consolidateRoutes } from "./routes-consolidation";
5
9
  import type { RouteConfig } from "./routes";
6
10
 
7
11
  /** Interface for _routes.json */
8
12
  interface RoutesJSONSpec {
9
13
  version: typeof ROUTES_SPEC_VERSION;
14
+ description?: string;
10
15
  include: string[];
11
16
  exclude: string[];
12
17
  }
@@ -53,6 +58,7 @@ export function convertRoutesToRoutesJSONSpec(
53
58
  const include = convertRoutesToGlobPatterns(reversedRoutes);
54
59
  return optimizeRoutesJSONSpec({
55
60
  version: ROUTES_SPEC_VERSION,
61
+ description: ROUTES_SPEC_DESCRIPTION,
56
62
  include,
57
63
  exclude: [],
58
64
  });
@@ -15,12 +15,9 @@ import { logger } from "../logger";
15
15
  import * as metrics from "../metrics";
16
16
  import { requireAuth } from "../user";
17
17
  import { buildFunctions } from "./build";
18
- import { PAGES_CONFIG_CACHE_FILENAME } from "./constants";
18
+ import { PAGES_CONFIG_CACHE_FILENAME, ROUTES_SPEC_VERSION } from "./constants";
19
19
  import { FunctionsNoRoutesError, getFunctionsNoRoutesWarning } from "./errors";
20
- import {
21
- isRoutesJSONSpec,
22
- optimizeRoutesJSONSpec,
23
- } from "./functions/routes-transformation";
20
+ import { isRoutesJSONSpec } from "./functions/routes-transformation";
24
21
  import { listProjects } from "./projects";
25
22
  import { upload } from "./upload";
26
23
  import { pagesBetaWarning } from "./utils";
@@ -254,7 +251,10 @@ export const Handler = async ({
254
251
 
255
252
  let builtFunctions: string | undefined = undefined;
256
253
  const functionsDirectory = join(cwd(), "functions");
257
- const routesOutputPath = join(tmpdir(), `_routes-${Math.random()}.json`);
254
+ const routesOutputPath = !existsSync(join(directory, "_routes.json"))
255
+ ? join(tmpdir(), `_routes-${Math.random()}.json`)
256
+ : undefined;
257
+
258
258
  if (existsSync(functionsDirectory)) {
259
259
  const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
260
260
  try {
@@ -265,6 +265,7 @@ export const Handler = async ({
265
265
  buildOutputDirectory: dirname(outfile),
266
266
  routesOutputPath,
267
267
  });
268
+
268
269
  builtFunctions = readFileSync(outfile, "utf-8");
269
270
  } catch (e) {
270
271
  if (e instanceof FunctionsNoRoutesError) {
@@ -301,7 +302,8 @@ export const Handler = async ({
301
302
 
302
303
  let _headers: string | undefined,
303
304
  _redirects: string | undefined,
304
- _routes: string | undefined,
305
+ _routesGenerated: string | undefined,
306
+ _routesCustom: string | undefined,
305
307
  _workerJS: string | undefined;
306
308
 
307
309
  try {
@@ -312,6 +314,12 @@ export const Handler = async ({
312
314
  _redirects = readFileSync(join(directory, "_redirects"), "utf-8");
313
315
  } catch {}
314
316
 
317
+ try {
318
+ // Developers can specify a custom _routes.json file, for projects with Pages
319
+ // Functions or projects in Advanced Mode
320
+ _routesCustom = readFileSync(join(directory, "_routes.json"), "utf-8");
321
+ } catch {}
322
+
315
323
  try {
316
324
  _workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
317
325
  } catch {}
@@ -327,47 +335,65 @@ export const Handler = async ({
327
335
  }
328
336
 
329
337
  if (builtFunctions) {
338
+ // with Pages Functions
339
+ // https://developers.cloudflare.com/pages/platform/functions/
330
340
  formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
331
341
  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"));
342
+
343
+ if (_routesCustom) {
344
+ // user provided a custom _routes.json file
345
+ try {
346
+ validateRoutesFile(_routesCustom, join(directory, "_routes.json"));
347
+
348
+ formData.append(
349
+ "_routes.json",
350
+ new File([_routesCustom], "_routes.json")
351
+ );
352
+ logger.log(`✨ Uploading _routes.json`);
353
+ logger.warn(
354
+ `_routes.json is an experimental feature and is subject to change. Please use with care.`
355
+ );
356
+ } catch (err) {
357
+ if (err instanceof FatalError) {
358
+ throw err;
359
+ }
336
360
  }
337
- } catch {}
361
+ } else if (routesOutputPath) {
362
+ // no custom _routes.json file found, so fallback to the generated one
363
+ try {
364
+ _routesGenerated = readFileSync(routesOutputPath, "utf-8");
365
+
366
+ if (_routesGenerated) {
367
+ formData.append(
368
+ "_routes.json",
369
+ new File([_routesGenerated], "_routes.json")
370
+ );
371
+ }
372
+ } catch {}
373
+ }
338
374
  } else if (_workerJS) {
339
375
  // Advanced Mode
340
376
  // https://developers.cloudflare.com/pages/platform/functions/#advanced-mode
341
377
  formData.append("_worker.js", new File([_workerJS], "_worker.js"));
342
378
  logger.log(`✨ Uploading _worker.js`);
343
379
 
344
- try {
345
- // In advanced mode, developers can specify a custom _routes.json
346
- // file. In which case, we need to run it through optimization
347
- // to potentially reduce the overall worker pipeline size
348
- const routesPath = join(directory, "_routes.json");
349
- const advancedModeRoutesString = readFileSync(routesPath, "utf-8");
350
- const advancedModeRoutes = JSON.parse(advancedModeRoutesString);
351
-
352
- if (!isRoutesJSONSpec(advancedModeRoutes)) {
353
- throw new FatalError(
354
- "Invalid _routes.json file found at:" + routesPath,
355
- 1
356
- );
357
- }
380
+ if (_routesCustom) {
381
+ // user provided a custom _routes.json file
382
+ try {
383
+ validateRoutesFile(_routesCustom, join(directory, "_routes.json"));
358
384
 
359
- _routes = JSON.stringify(optimizeRoutesJSONSpec(advancedModeRoutes));
360
- formData.append("_routes.json", new File([_routes], "_routes.json"));
361
- logger.log(`✨ Uploading _routes.json`);
362
-
363
- logger.warn(
364
- `🚨 _routes.json is an experimental feature and is subject to change. Don't use unless you really must!`
365
- );
366
- } catch (e) {
367
- // Ignore file not existing errors for _routes.json but forward the potential
368
- // FatalError from an invalid spec
369
- if (e instanceof FatalError) {
370
- throw e;
385
+ formData.append(
386
+ "_routes.json",
387
+ new File([_routesCustom], "_routes.json")
388
+ );
389
+ logger.log(`✨ Uploading _routes.json`);
390
+ logger.warn(
391
+ `_routes.json is an experimental feature and is subject to change. Please use with care.`
392
+ );
393
+ } catch (err) {
394
+ if (err instanceof FatalError) {
395
+ throw err;
396
+ }
371
397
  }
372
398
  }
373
399
  }
@@ -389,3 +415,21 @@ export const Handler = async ({
389
415
  );
390
416
  await metrics.sendMetricsEvent("create pages deployment");
391
417
  };
418
+
419
+ function validateRoutesFile(_routes: string, routesPath: string) {
420
+ const routes = JSON.parse(_routes);
421
+
422
+ if (!isRoutesJSONSpec(routes)) {
423
+ throw new FatalError(
424
+ `Invalid _routes.json file found at: ${routesPath}. Please make sure the JSON object has the following format:
425
+ {
426
+ version: ${ROUTES_SPEC_VERSION};
427
+ include: string[];
428
+ exclude: string[];
429
+ }
430
+ and that at least one include rule is provided.
431
+ `,
432
+ 1
433
+ );
434
+ }
435
+ }
package/src/paths.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { assert } from "console";
1
+ import { assert } from "node:console";
2
+ import { resolve } from "node:path";
2
3
 
3
4
  type DiscriminatedPath<Discriminator extends string> = string & {
4
5
  _discriminator: Discriminator;
@@ -24,3 +25,21 @@ export function toUrlPath(path: string): UrlPath {
24
25
  );
25
26
  return path.replace(/\\/g, "/") as UrlPath;
26
27
  }
28
+
29
+ /**
30
+ * The __RELATIVE_PACKAGE_PATH__ is defined either in the esbuild config (for production)
31
+ * or the jest.setup.ts (for unit testing).
32
+ */
33
+ declare const __RELATIVE_PACKAGE_PATH__: string;
34
+
35
+ /**
36
+ * Use this function (rather than node.js constants like `__dirname`) to specify
37
+ * paths that are relative to the base path of the Wrangler package.
38
+ *
39
+ * It is important to use this function because it reliably maps to the root of the package
40
+ * no matter whether the code has been bundled or not.
41
+ */
42
+ export function getBasePath(): string {
43
+ // eslint-disable-next-line no-restricted-globals
44
+ return resolve(__dirname, __RELATIVE_PACKAGE_PATH__);
45
+ }
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
  /**
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
@@ -65,14 +65,14 @@ export interface CfModule {
65
65
  /**
66
66
  * A map of variable names to values.
67
67
  */
68
- interface CfVars {
68
+ export interface CfVars {
69
69
  [key: string]: unknown;
70
70
  }
71
71
 
72
72
  /**
73
73
  * A KV namespace.
74
74
  */
75
- interface CfKvNamespace {
75
+ export interface CfKvNamespace {
76
76
  binding: string;
77
77
  id: string;
78
78
  }
@@ -81,7 +81,7 @@ interface CfKvNamespace {
81
81
  * A binding to a wasm module (in service-worker format)
82
82
  */
83
83
 
84
- interface CfWasmModuleBindings {
84
+ export interface CfWasmModuleBindings {
85
85
  [key: string]: string;
86
86
  }
87
87
 
@@ -89,7 +89,7 @@ interface CfWasmModuleBindings {
89
89
  * A binding to a text blob (in service-worker format)
90
90
  */
91
91
 
92
- interface CfTextBlobBindings {
92
+ export interface CfTextBlobBindings {
93
93
  [key: string]: string;
94
94
  }
95
95
 
@@ -97,21 +97,21 @@ interface CfTextBlobBindings {
97
97
  * A binding to a data blob (in service-worker format)
98
98
  */
99
99
 
100
- interface CfDataBlobBindings {
100
+ export interface CfDataBlobBindings {
101
101
  [key: string]: string;
102
102
  }
103
103
 
104
104
  /**
105
105
  * A Durable Object.
106
106
  */
107
- interface CfDurableObject {
107
+ export interface CfDurableObject {
108
108
  name: string;
109
109
  class_name: string;
110
110
  script_name?: string;
111
111
  environment?: string;
112
112
  }
113
113
 
114
- interface CfR2Bucket {
114
+ export interface CfR2Bucket {
115
115
  binding: string;
116
116
  bucket_name: string;
117
117
  }
@@ -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
+ }