wrangler 0.0.0-e1c615f → 0.0.0-e5d3690

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "0.0.0-e1c615f",
3
+ "version": "0.0.0-e5d3690",
4
4
  "author": "wrangler@cloudflare.com",
5
5
  "description": "Command-line interface for all things Cloudflare Workers",
6
6
  "bin": {
@@ -37,9 +37,10 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "esbuild": "0.14.1",
40
- "miniflare": "2.0.0-rc.5",
40
+ "miniflare": "2.2.0",
41
41
  "path-to-regexp": "^6.2.0",
42
- "semiver": "^1.1.0"
42
+ "semiver": "^1.1.0",
43
+ "xxhash-addon": "^1.4.0"
43
44
  },
44
45
  "optionalDependencies": {
45
46
  "fsevents": "~2.3.2"
@@ -59,18 +60,20 @@
59
60
  "chokidar": "^3.5.2",
60
61
  "clipboardy": "^3.0.0",
61
62
  "command-exists": "^1.2.9",
63
+ "devtools-protocol": "^0.0.955664",
62
64
  "execa": "^6.0.0",
63
65
  "faye-websocket": "^0.11.4",
64
66
  "finalhandler": "^1.1.2",
65
67
  "find-up": "^6.2.0",
66
68
  "formdata-node": "^4.3.1",
69
+ "ignore": "^5.2.0",
67
70
  "ink": "^3.2.0",
68
71
  "ink-select-input": "^4.2.1",
69
72
  "ink-table": "^3.0.0",
70
73
  "ink-testing-library": "^2.1.0",
71
74
  "ink-text-input": "^4.0.2",
72
75
  "mime": "^3.0.0",
73
- "node-fetch": "^3.1.0",
76
+ "node-fetch": "3.1.1",
74
77
  "open": "^8.4.0",
75
78
  "react": "^17.0.2",
76
79
  "react-error-boundary": "^3.1.4",
@@ -96,7 +99,8 @@
96
99
  "bundle": "node -r esbuild-register scripts/bundle.ts",
97
100
  "build": "npm run clean && npm run bundle",
98
101
  "start": "npm run bundle && NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js",
99
- "test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true"
102
+ "test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true",
103
+ "test-watch": "npm run test -- --runInBand --testTimeout=50000 --watch"
100
104
  },
101
105
  "engines": {
102
106
  "node": ">=16.7.0"
@@ -6,6 +6,7 @@ type Options = {
6
6
  outfile: string;
7
7
  minify?: boolean;
8
8
  sourcemap?: boolean;
9
+ fallbackService?: string;
9
10
  watch?: boolean;
10
11
  onEnd?: () => void;
11
12
  };
@@ -15,6 +16,7 @@ export function buildWorker({
15
16
  outfile = "bundle.js",
16
17
  minify = false,
17
18
  sourcemap = false,
19
+ fallbackService = "ASSETS",
18
20
  watch = false,
19
21
  onEnd = () => {},
20
22
  }: Options) {
@@ -31,6 +33,9 @@ export function buildWorker({
31
33
  sourcemap,
32
34
  watch,
33
35
  allowOverwrite: true,
36
+ define: {
37
+ __FALLBACK_SERVICE__: JSON.stringify(fallbackService),
38
+ },
34
39
  plugins: [
35
40
  {
36
41
  name: "wrangler notifier and monitor",
@@ -52,7 +52,7 @@ export async function generateConfigFromFileTree({
52
52
  const declaration = node.declaration;
53
53
 
54
54
  // `export async function onRequest() {...}`
55
- if (declaration.type === "FunctionDeclaration") {
55
+ if (declaration.type === "FunctionDeclaration" && declaration.id) {
56
56
  exportNames.push(declaration.id.name);
57
57
  }
58
58
 
@@ -155,12 +155,12 @@ export async function generateConfigFromFileTree({
155
155
  // more specific routes aren't occluded from matching due to
156
156
  // less specific routes appearing first in the route list.
157
157
  export function compareRoutes(a: string, b: string) {
158
- function parseRoutePath(routePath: string) {
159
- let [method, segmentedPath] = routePath.split(" ");
160
- if (!segmentedPath) {
161
- segmentedPath = method;
162
- method = null;
163
- }
158
+ function parseRoutePath(routePath: string): [string | null, string[]] {
159
+ const parts = routePath.split(" ", 2);
160
+ // split() will guarantee at least one element.
161
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162
+ const segmentedPath = parts.pop()!;
163
+ const method = parts.pop() ?? null;
164
164
 
165
165
  const segments = segmentedPath.slice(1).split("/").filter(Boolean);
166
166
  return [method, segments];
@@ -204,7 +204,7 @@ async function forEachFile<T>(
204
204
  const searchPaths = [baseDir];
205
205
  const returnValues: T[] = [];
206
206
 
207
- while (searchPaths.length) {
207
+ while (isNotEmpty(searchPaths)) {
208
208
  const cwd = searchPaths.shift();
209
209
  const dir = await fs.readdir(cwd, { withFileTypes: true });
210
210
  for (const entry of dir) {
@@ -219,3 +219,10 @@ async function forEachFile<T>(
219
219
 
220
220
  return returnValues;
221
221
  }
222
+
223
+ interface NonEmptyArray<T> extends Array<T> {
224
+ shift(): T;
225
+ }
226
+ function isNotEmpty<T>(array: T[]): array is NonEmptyArray<T> {
227
+ return array.length > 0;
228
+ }
@@ -114,7 +114,7 @@ export function parseConfig(config: Config, baseDir: string) {
114
114
  });
115
115
  }
116
116
 
117
- for (const [route, props] of Object.entries(config.routes)) {
117
+ for (const [route, props] of Object.entries(config.routes ?? {})) {
118
118
  let [_methods, routePath] = route.split(" ");
119
119
  if (!routePath) {
120
120
  routePath = _methods;
@@ -29,18 +29,20 @@ type RouteHandler = {
29
29
 
30
30
  // inject `routes` via ESBuild
31
31
  declare const routes: RouteHandler[];
32
+ // define `__FALLBACK_SERVICE__` via ESBuild
33
+ declare const __FALLBACK_SERVICE__: string;
32
34
 
33
35
  // expect an ASSETS fetcher binding pointing to the asset-server stage
34
- type Env = {
35
- [name: string]: unknown;
36
- ASSETS: { fetch(url: string, init: RequestInit): Promise<Response> };
36
+ type FetchEnv = {
37
+ [name: string]: { fetch: typeof fetch };
38
+ ASSETS: { fetch: typeof fetch };
37
39
  };
38
40
 
39
41
  type WorkerContext = {
40
42
  waitUntil: (promise: Promise<unknown>) => void;
41
43
  };
42
44
 
43
- function* executeRequest(request: Request, env: Env) {
45
+ function* executeRequest(request: Request, _env: FetchEnv) {
44
46
  const requestPath = new URL(request.url).pathname;
45
47
 
46
48
  // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches
@@ -85,16 +87,10 @@ function* executeRequest(request: Request, env: Env) {
85
87
  break;
86
88
  }
87
89
  }
88
-
89
- // Finally, yield to the asset-server
90
- return {
91
- handler: () => env.ASSETS.fetch(request.url, request),
92
- params: {} as Params,
93
- };
94
90
  }
95
91
 
96
92
  export default {
97
- async fetch(request: Request, env: Env, workerContext: WorkerContext) {
93
+ async fetch(request: Request, env: FetchEnv, workerContext: WorkerContext) {
98
94
  const handlerIterator = executeRequest(request, env);
99
95
  const data = {}; // arbitrary data the user can set between functions
100
96
  const next = async (input?: RequestInfo, init?: RequestInit) => {
@@ -102,14 +98,11 @@ export default {
102
98
  request = new Request(input, init);
103
99
  }
104
100
 
105
- const { value } = handlerIterator.next();
106
- if (value) {
107
- const { handler, params } = value;
108
- const context: EventContext<
109
- unknown,
110
- string,
111
- Record<string, unknown>
112
- > = {
101
+ const result = handlerIterator.next();
102
+ // Note we can't use `!result.done` because this doesn't narrow to the correct type
103
+ if (result.done == false) {
104
+ const { handler, params } = result.value;
105
+ const context = {
113
106
  request: new Request(request.clone()),
114
107
  next,
115
108
  params,
@@ -125,6 +118,12 @@ export default {
125
118
  [101, 204, 205, 304].includes(response.status) ? null : response.body,
126
119
  response
127
120
  );
121
+ } else if (__FALLBACK_SERVICE__) {
122
+ // There are no more handlers so finish with the fallback service (`env.ASSETS.fetch` in Pages' case)
123
+ return env[__FALLBACK_SERVICE__].fetch(request);
124
+ } else {
125
+ // There was not fallback service so actually make the request to the origin.
126
+ return fetch(request);
128
127
  }
129
128
  };
130
129
 
@@ -15,9 +15,10 @@ describe("Dev component", () => {
15
15
  accountId: "some-account-id",
16
16
  public: "some/public/path",
17
17
  });
18
- expect(lastFrame()).toMatchInlineSnapshot(`
18
+ expect(lastFrame()?.split("\n").slice(0, 2).join("\n"))
19
+ .toMatchInlineSnapshot(`
19
20
  "Something went wrong:
20
- You cannot use the service worker format with a \`public\` directory."
21
+ Error: You cannot use the service worker format with a \`public\` directory."
21
22
  `);
22
23
  });
23
24
  });
@@ -38,7 +39,7 @@ function renderDev({
38
39
  jsxFragment,
39
40
  bindings = {},
40
41
  public: publicDir,
41
- site,
42
+ assetPaths,
42
43
  compatibilityDate,
43
44
  compatibilityFlags,
44
45
  usageModel,
@@ -55,7 +56,7 @@ function renderDev({
55
56
  jsxFactory={jsxFactory}
56
57
  jsxFragment={jsxFragment}
57
58
  accountId={accountId}
58
- site={site}
59
+ assetPaths={assetPaths}
59
60
  public={publicDir}
60
61
  compatibilityDate={compatibilityDate}
61
62
  compatibilityFlags={compatibilityFlags}
@@ -17,6 +17,7 @@ describe("wrangler", () => {
17
17
 
18
18
  Commands:
19
19
  wrangler init [name] 📥 Create a wrangler.toml configuration file
20
+ wrangler whoami 🕵️ Retrieve your user info and test your auth config
20
21
  wrangler dev <filename> 👂 Start a local server for developing your worker
21
22
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
22
23
  wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
@@ -49,6 +50,7 @@ describe("wrangler", () => {
49
50
 
50
51
  Commands:
51
52
  wrangler init [name] 📥 Create a wrangler.toml configuration file
53
+ wrangler whoami 🕵️ Retrieve your user info and test your auth config
52
54
  wrangler dev <filename> 👂 Start a local server for developing your worker
53
55
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
54
56
  wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
@@ -1,4 +1,5 @@
1
1
  import { writeFileSync } from "fs";
2
+ import type { KVNamespaceInfo } from "../kv";
2
3
  import {
3
4
  setMockResponse,
4
5
  setMockRawResponse,
@@ -196,30 +197,30 @@ describe("wrangler", () => {
196
197
  }
197
198
 
198
199
  it("should list namespaces", async () => {
199
- const KVNamespaces = [
200
+ const kvNamespaces: KVNamespaceInfo[] = [
200
201
  { title: "title-1", id: "id-1" },
201
202
  { title: "title-2", id: "id-2" },
202
203
  ];
203
- mockListRequest(KVNamespaces);
204
+ mockListRequest(kvNamespaces);
204
205
  const { error, stdout, stderr } = await runWrangler(
205
206
  "kv:namespace list"
206
207
  );
207
208
  expect(error).toMatchInlineSnapshot(`undefined`);
208
209
  expect(stderr).toMatchInlineSnapshot(`""`);
209
210
  const namespaces = JSON.parse(stdout);
210
- expect(namespaces).toEqual(KVNamespaces);
211
+ expect(namespaces).toEqual(kvNamespaces);
211
212
  });
212
213
 
213
214
  it("should make multiple requests for paginated results", async () => {
214
215
  // Create a lot of mock namespaces, so that the fetch requests will be paginated
215
- const KVNamespaces = [];
216
+ const kvNamespaces: KVNamespaceInfo[] = [];
216
217
  for (let i = 0; i < 550; i++) {
217
- KVNamespaces.push({ title: "title-" + i, id: "id-" + i });
218
+ kvNamespaces.push({ title: "title-" + i, id: "id-" + i });
218
219
  }
219
- const requests = mockListRequest(KVNamespaces);
220
+ const requests = mockListRequest(kvNamespaces);
220
221
  const { stdout } = await runWrangler("kv:namespace list");
221
222
  const namespaces = JSON.parse(stdout);
222
- expect(namespaces).toEqual(KVNamespaces);
223
+ expect(namespaces).toEqual(kvNamespaces);
223
224
  expect(requests.count).toEqual(6);
224
225
  });
225
226
  });
@@ -335,8 +336,16 @@ describe("wrangler", () => {
335
336
  expect(namespaceId).toEqual(expectedNamespaceId);
336
337
  expect(key).toEqual(expectedKey);
337
338
  expect(body).toEqual(expectedValue);
338
- expect(query.get("expiration")).toEqual(`${expiration}`);
339
- expect(query.get("expiration_ttl")).toEqual(`${expirationTtl}`);
339
+ if (expiration !== undefined) {
340
+ expect(query.get("expiration")).toEqual(`${expiration}`);
341
+ } else {
342
+ expect(query.has("expiration")).toBe(false);
343
+ }
344
+ if (expirationTtl) {
345
+ expect(query.get("expiration_ttl")).toEqual(`${expirationTtl}`);
346
+ } else {
347
+ expect(query.has("expiration_ttl")).toBe(false);
348
+ }
340
349
  return null;
341
350
  }
342
351
  );
@@ -636,9 +645,12 @@ describe("wrangler", () => {
636
645
  `kv:key put key value --binding otherBinding`
637
646
  );
638
647
  expect(stdout).toMatchInlineSnapshot(`""`);
639
- expect(stderr).toMatchInlineSnapshot(
640
- `"A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\"."`
641
- );
648
+ expect(stderr).toMatchInlineSnapshot(`
649
+ "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\".
650
+
651
+ %s
652
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
653
+ `);
642
654
  expect(error).toMatchInlineSnapshot(
643
655
  `[Error: A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".]`
644
656
  );
@@ -655,9 +667,12 @@ describe("wrangler", () => {
655
667
  "kv:key put my-key my-value --binding someBinding"
656
668
  );
657
669
  expect(stdout).toMatchInlineSnapshot(`""`);
658
- expect(stderr).toMatchInlineSnapshot(
659
- `"someBinding has both a namespace ID and a preview ID. Specify \\"--preview\\" or \\"--preview false\\" to avoid writing data to the wrong namespace."`
660
- );
670
+ expect(stderr).toMatchInlineSnapshot(`
671
+ "someBinding has both a namespace ID and a preview ID. Specify \\"--preview\\" or \\"--preview false\\" to avoid writing data to the wrong namespace.
672
+
673
+ %s
674
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
675
+ `);
661
676
  expect(error).toMatchInlineSnapshot(
662
677
  `[Error: someBinding has both a namespace ID and a preview ID. Specify "--preview" or "--preview false" to avoid writing data to the wrong namespace.]`
663
678
  );
@@ -666,37 +681,6 @@ describe("wrangler", () => {
666
681
  });
667
682
 
668
683
  describe("list", () => {
669
- function mockKeyListRequest(
670
- expectedNamespaceId: string,
671
- expectedKeys: string[],
672
- keysPerRequest = 1000
673
- ) {
674
- const requests = { count: 0 };
675
- setMockRawResponse(
676
- "/accounts/:accountId/storage/kv/namespaces/:namespaceId/keys",
677
- ([_url, accountId, namespaceId], _init, query) => {
678
- requests.count++;
679
- expect(accountId).toEqual("some-account-id");
680
- expect(namespaceId).toEqual(expectedNamespaceId);
681
- if (expectedKeys.length <= keysPerRequest) {
682
- return createFetchResult(expectedKeys);
683
- } else {
684
- const start = parseInt(query.get("cursor")) || 0;
685
- const end = start + keysPerRequest;
686
- const cursor = end < expectedKeys.length ? end : undefined;
687
- return createFetchResult(
688
- expectedKeys.slice(start, end),
689
- true,
690
- [],
691
- [],
692
- { cursor }
693
- );
694
- }
695
- }
696
- );
697
- return requests;
698
- }
699
-
700
684
  it("should list the keys of a namespace specified by namespace-id", async () => {
701
685
  const keys = ["key-1", "key-2", "key-3"];
702
686
  mockKeyListRequest("some-namespace-id", keys);
@@ -706,9 +690,23 @@ describe("wrangler", () => {
706
690
  expect(error).toMatchInlineSnapshot(`undefined`);
707
691
  expect(stderr).toMatchInlineSnapshot(`""`);
708
692
  expect(stdout).toMatchInlineSnapshot(`
709
- "key-1
710
- key-2
711
- key-3"
693
+ "[
694
+ {
695
+ \\"name\\": \\"key-1\\",
696
+ \\"expiration\\": 123456789,
697
+ \\"metadata\\": {}
698
+ },
699
+ {
700
+ \\"name\\": \\"key-2\\",
701
+ \\"expiration\\": 123456789,
702
+ \\"metadata\\": {}
703
+ },
704
+ {
705
+ \\"name\\": \\"key-3\\",
706
+ \\"expiration\\": 123456789,
707
+ \\"metadata\\": {}
708
+ }
709
+ ]"
712
710
  `);
713
711
  });
714
712
 
@@ -722,9 +720,23 @@ describe("wrangler", () => {
722
720
  expect(error).toMatchInlineSnapshot(`undefined`);
723
721
  expect(stderr).toMatchInlineSnapshot(`""`);
724
722
  expect(stdout).toMatchInlineSnapshot(`
725
- "key-1
726
- key-2
727
- key-3"
723
+ "[
724
+ {
725
+ \\"name\\": \\"key-1\\",
726
+ \\"expiration\\": 123456789,
727
+ \\"metadata\\": {}
728
+ },
729
+ {
730
+ \\"name\\": \\"key-2\\",
731
+ \\"expiration\\": 123456789,
732
+ \\"metadata\\": {}
733
+ },
734
+ {
735
+ \\"name\\": \\"key-3\\",
736
+ \\"expiration\\": 123456789,
737
+ \\"metadata\\": {}
738
+ }
739
+ ]"
728
740
  `);
729
741
  });
730
742
 
@@ -738,9 +750,23 @@ describe("wrangler", () => {
738
750
  expect(error).toMatchInlineSnapshot(`undefined`);
739
751
  expect(stderr).toMatchInlineSnapshot(`""`);
740
752
  expect(stdout).toMatchInlineSnapshot(`
741
- "key-1
742
- key-2
743
- key-3"
753
+ "[
754
+ {
755
+ \\"name\\": \\"key-1\\",
756
+ \\"expiration\\": 123456789,
757
+ \\"metadata\\": {}
758
+ },
759
+ {
760
+ \\"name\\": \\"key-2\\",
761
+ \\"expiration\\": 123456789,
762
+ \\"metadata\\": {}
763
+ },
764
+ {
765
+ \\"name\\": \\"key-3\\",
766
+ \\"expiration\\": 123456789,
767
+ \\"metadata\\": {}
768
+ }
769
+ ]"
744
770
  `);
745
771
  });
746
772
 
@@ -754,9 +780,23 @@ describe("wrangler", () => {
754
780
  expect(error).toMatchInlineSnapshot(`undefined`);
755
781
  expect(stderr).toMatchInlineSnapshot(`""`);
756
782
  expect(stdout).toMatchInlineSnapshot(`
757
- "key-1
758
- key-2
759
- key-3"
783
+ "[
784
+ {
785
+ \\"name\\": \\"key-1\\",
786
+ \\"expiration\\": 123456789,
787
+ \\"metadata\\": {}
788
+ },
789
+ {
790
+ \\"name\\": \\"key-2\\",
791
+ \\"expiration\\": 123456789,
792
+ \\"metadata\\": {}
793
+ },
794
+ {
795
+ \\"name\\": \\"key-3\\",
796
+ \\"expiration\\": 123456789,
797
+ \\"metadata\\": {}
798
+ }
799
+ ]"
760
800
  `);
761
801
  });
762
802
 
@@ -770,15 +810,29 @@ describe("wrangler", () => {
770
810
  expect(error).toMatchInlineSnapshot(`undefined`);
771
811
  expect(stderr).toMatchInlineSnapshot(`""`);
772
812
  expect(stdout).toMatchInlineSnapshot(`
773
- "key-1
774
- key-2
775
- key-3"
813
+ "[
814
+ {
815
+ \\"name\\": \\"key-1\\",
816
+ \\"expiration\\": 123456789,
817
+ \\"metadata\\": {}
818
+ },
819
+ {
820
+ \\"name\\": \\"key-2\\",
821
+ \\"expiration\\": 123456789,
822
+ \\"metadata\\": {}
823
+ },
824
+ {
825
+ \\"name\\": \\"key-3\\",
826
+ \\"expiration\\": 123456789,
827
+ \\"metadata\\": {}
828
+ }
829
+ ]"
776
830
  `);
777
831
  });
778
832
 
779
833
  it("should make multiple requests for paginated results", async () => {
780
834
  // Create a lot of mock keys, so that the fetch requests will be paginated
781
- const keys = [];
835
+ const keys: string[] = [];
782
836
  for (let i = 0; i < 550; i++) {
783
837
  keys.push("key-" + i);
784
838
  }
@@ -789,7 +843,7 @@ describe("wrangler", () => {
789
843
  );
790
844
  expect(error).toMatchInlineSnapshot(`undefined`);
791
845
  expect(stderr).toMatchInlineSnapshot(`""`);
792
- expect(stdout).toEqual(keys.join("\n"));
846
+ expect(JSON.parse(stdout).map((k) => k.name)).toEqual(keys);
793
847
  expect(requests.count).toEqual(6);
794
848
  });
795
849
 
@@ -801,9 +855,12 @@ describe("wrangler", () => {
801
855
  expect(error).toMatchInlineSnapshot(
802
856
  `[Error: A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".]`
803
857
  );
804
- expect(stderr).toMatchInlineSnapshot(
805
- `"A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\"."`
806
- );
858
+ expect(stderr).toMatchInlineSnapshot(`
859
+ "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\".
860
+
861
+ %s
862
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
863
+ `);
807
864
  expect(stdout).toMatchInlineSnapshot(`""`);
808
865
  });
809
866
  });
@@ -988,9 +1045,12 @@ describe("wrangler", () => {
988
1045
  `kv:key get key --binding otherBinding`
989
1046
  );
990
1047
  expect(stdout).toMatchInlineSnapshot(`""`);
991
- expect(stderr).toMatchInlineSnapshot(
992
- `"A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\"."`
993
- );
1048
+ expect(stderr).toMatchInlineSnapshot(`
1049
+ "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\".
1050
+
1051
+ %s
1052
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
1053
+ `);
994
1054
  expect(error).toMatchInlineSnapshot(
995
1055
  `[Error: A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".]`
996
1056
  );
@@ -1048,9 +1108,12 @@ describe("wrangler", () => {
1048
1108
  const { stderr } = await runWrangler(
1049
1109
  `kv:key delete --binding otherBinding someKey`
1050
1110
  );
1051
- expect(stderr).toMatchInlineSnapshot(
1052
- `"A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\"."`
1053
- );
1111
+ expect(stderr).toMatchInlineSnapshot(`
1112
+ "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\".
1113
+
1114
+ %s
1115
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
1116
+ `);
1054
1117
  });
1055
1118
 
1056
1119
  it("should delete a key in a namespace specified by binding name in a given environment", async () => {
@@ -1096,3 +1159,41 @@ function writeWranglerConfig() {
1096
1159
  "utf-8"
1097
1160
  );
1098
1161
  }
1162
+
1163
+ export function mockKeyListRequest(
1164
+ expectedNamespaceId: string,
1165
+ expectedKeys: string[],
1166
+ keysPerRequest = 1000
1167
+ ) {
1168
+ const requests = { count: 0 };
1169
+ // See https://api.cloudflare.com/#workers-kv-namespace-list-a-namespace-s-keys
1170
+ const expectedKeyObjects = expectedKeys.map((name) => ({
1171
+ name,
1172
+ expiration: 123456789,
1173
+ metadata: {},
1174
+ }));
1175
+ setMockRawResponse(
1176
+ "/accounts/:accountId/storage/kv/namespaces/:namespaceId/keys",
1177
+ "GET",
1178
+ ([_url, accountId, namespaceId], _init, query) => {
1179
+ requests.count++;
1180
+ expect(accountId).toEqual("some-account-id");
1181
+ expect(namespaceId).toEqual(expectedNamespaceId);
1182
+ if (expectedKeyObjects.length <= keysPerRequest) {
1183
+ return createFetchResult(expectedKeyObjects);
1184
+ } else {
1185
+ const start = parseInt(query.get("cursor") ?? "0") || 0;
1186
+ const end = start + keysPerRequest;
1187
+ const cursor = end < expectedKeyObjects.length ? end : undefined;
1188
+ return createFetchResult(
1189
+ expectedKeyObjects.slice(start, end),
1190
+ true,
1191
+ [],
1192
+ [],
1193
+ { cursor }
1194
+ );
1195
+ }
1196
+ }
1197
+ );
1198
+ return requests;
1199
+ }
@@ -1,5 +1,5 @@
1
1
  import type { RequestInit } from "node-fetch";
2
- import type { URLSearchParams } from "node:url";
2
+ import { URLSearchParams } from "node:url";
3
3
  import { pathToRegexp } from "path-to-regexp";
4
4
  import { CF_API_BASE_URL } from "../cfetch";
5
5
  import type { FetchResult } from "../cfetch";
@@ -9,8 +9,8 @@ import type { FetchResult } from "../cfetch";
9
9
  */
10
10
  export type MockHandler<ResponseType> = (
11
11
  uri: RegExpExecArray,
12
- init?: RequestInit,
13
- queryParams?: URLSearchParams
12
+ init: RequestInit,
13
+ queryParams: URLSearchParams
14
14
  ) => ResponseType;
15
15
 
16
16
  type RemoveMockFn = () => void;
@@ -32,19 +32,21 @@ const mocks: MockFetch<unknown>[] = [];
32
32
  export async function mockFetchInternal(
33
33
  resource: string,
34
34
  init: RequestInit = {},
35
- queryParams?: URLSearchParams
35
+ queryParams: URLSearchParams = new URLSearchParams()
36
36
  ) {
37
37
  for (const { regexp, method, handler } of mocks) {
38
38
  const resourcePath = new URL(resource, CF_API_BASE_URL).pathname;
39
39
  const uri = regexp.exec(resourcePath);
40
40
  // Do the resource path and (if specified) the HTTP method match?
41
- if (uri !== null && (!method || method === init.method)) {
41
+ if (uri !== null && (!method || method === (init.method ?? "GET"))) {
42
42
  // The `resource` regular expression will extract the labelled groups from the URL.
43
43
  // These are passed through to the `handler` call, to allow it to do additional checks or behaviour.
44
44
  return handler(uri, init, queryParams); // TODO: should we have some kind of fallthrough system? we'll see.
45
45
  }
46
46
  }
47
- throw new Error(`no mocks found for ${init.method}: ${resource}`);
47
+ throw new Error(
48
+ `no mocks found for ${init.method ?? "any HTTP"} request to ${resource}`
49
+ );
48
50
  }
49
51
 
50
52
  /**