wrangler 2.1.0 → 2.1.3

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.
@@ -6033,7 +6033,7 @@ async function generateAssetsFetch(directory, log) {
6033
6033
  let metadata = createMetadataObject({
6034
6034
  redirects,
6035
6035
  headers,
6036
- logger: log.warn
6036
+ logger: log.warn.bind(log)
6037
6037
  });
6038
6038
  watch([headersFile, redirectsFile], { persistent: true }).on(
6039
6039
  "change",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "Command-line interface for all things Cloudflare Workers",
5
5
  "keywords": [
6
6
  "wrangler",
@@ -148,6 +148,7 @@
148
148
  "jest-fetch-mock": "^3.0.3",
149
149
  "jest-websocket-mock": "^2.3.0",
150
150
  "mime": "^3.0.0",
151
+ "msw": "^0.47.1",
151
152
  "open": "^8.4.0",
152
153
  "p-queue": "^7.2.0",
153
154
  "pretty-bytes": "^6.0.0",
@@ -1025,7 +1025,8 @@ describe("wrangler dev", () => {
1025
1025
  -l, --local Run on my machine [boolean] [default: false]
1026
1026
  --minify Minify the script [boolean]
1027
1027
  --node-compat Enable node.js compatibility [boolean]
1028
- --experimental-enable-local-persistence Enable persistence for this session (only for local mode) [boolean]
1028
+ --persist Enable persistence for local mode, using default path: .wrangler/state [boolean]
1029
+ --persist-to Specify directory to use for local persistence (implies --persist) [string]
1029
1030
  --inspect Enable dev tools [deprecated] [boolean]",
1030
1031
  "warn": "",
1031
1032
  }
@@ -0,0 +1,40 @@
1
+ jest.unmock("../dialogs");
2
+ import { fromDashMessagePrompt } from "../dialogs";
3
+ import { CI } from "../is-ci";
4
+
5
+ describe("fromDashMessagePrompt", () => {
6
+ it("should return undefined in CI when last deployed from api", async () => {
7
+ //in CI
8
+ jest.spyOn(CI, "isCI").mockReturnValue(true);
9
+ const result = await fromDashMessagePrompt("api");
10
+ expect(result).toBe(undefined);
11
+ });
12
+
13
+ it("should return undefined in CI when last deployed from wrangler", async () => {
14
+ //in CI
15
+ jest.spyOn(CI, "isCI").mockReturnValue(true);
16
+ const result = await fromDashMessagePrompt("wrangler");
17
+ expect(result).toBe(undefined);
18
+ });
19
+
20
+ it("should return true in CI when last deployed from dash", async () => {
21
+ //in CI
22
+ jest.spyOn(CI, "isCI").mockReturnValue(true);
23
+ const result = await fromDashMessagePrompt("dash");
24
+ expect(result).toBe(true);
25
+ });
26
+
27
+ it("should return undefined when last deployed from api", async () => {
28
+ //not in CI
29
+ jest.spyOn(CI, "isCI").mockReturnValue(false);
30
+ const result = await fromDashMessagePrompt("api");
31
+ expect(result).toBe(undefined);
32
+ });
33
+
34
+ it("should return undefined when last deployed from wrangler", async () => {
35
+ //not in CI
36
+ jest.spyOn(CI, "isCI").mockReturnValue(false);
37
+ const result = await fromDashMessagePrompt("wrangler");
38
+ expect(result).toBe(undefined);
39
+ });
40
+ });
@@ -4,8 +4,24 @@ import { pathToRegexp } from "path-to-regexp";
4
4
  import { Response } from "undici";
5
5
  import { getCloudflareApiBaseUrl } from "../../cfetch";
6
6
  import type { FetchResult, FetchError } from "../../cfetch";
7
+ import type { fetchInternal, fetchR2Objects } from "../../cfetch/internal";
7
8
  import type { RequestInit, BodyInit, HeadersInit } from "undici";
8
9
 
10
+ /**
11
+ * When the custom mocks fallthrough in tests because they aren't set, instead of throwing an error,
12
+ * we use real fetch to make the request which will use Mock Service Workers.
13
+ */
14
+ const {
15
+ fetchInternal: realFetchInternal,
16
+ }: { fetchInternal: typeof fetchInternal } = jest.requireActual(
17
+ "../../cfetch/internal"
18
+ );
19
+ const {
20
+ fetchR2Objects: realFetchR2Objects,
21
+ }: { fetchR2Objects: typeof fetchR2Objects } = jest.requireActual(
22
+ "../../cfetch/internal"
23
+ );
24
+
9
25
  /**
10
26
  * The signature of the function that will handle a mock request.
11
27
  */
@@ -34,7 +50,7 @@ const mocks: MockFetch<unknown>[] = [];
34
50
  export async function mockFetchInternal(
35
51
  resource: string,
36
52
  init: RequestInit = {},
37
- queryParams: URLSearchParams = new URLSearchParams()
53
+ queryParams: URLSearchParams | undefined
38
54
  ) {
39
55
  for (const { regexp, method, handler } of mocks) {
40
56
  const resourcePath = new URL(resource, getCloudflareApiBaseUrl()).pathname;
@@ -43,12 +59,13 @@ export async function mockFetchInternal(
43
59
  if (uri !== null && (!method || method === (init.method ?? "GET"))) {
44
60
  // The `resource` regular expression will extract the labelled groups from the URL.
45
61
  // These are passed through to the `handler` call, to allow it to do additional checks or behaviour.
46
- return await handler(uri, init, queryParams); // TODO: should we have some kind of fallthrough system? we'll see.
62
+ return await handler(uri, init, queryParams ?? new URLSearchParams()); // TODO: should we have some kind of fallthrough system? we'll see.
47
63
  }
48
64
  }
49
- throw new Error(
50
- `no mocks found for ${init.method ?? "any HTTP"} request to ${resource}`
51
- );
65
+
66
+ // let it fall through to mock-service-worker
67
+ // (do a real, unmocked fetch)
68
+ return await realFetchInternal(resource, init, queryParams);
52
69
  }
53
70
 
54
71
  /**
@@ -221,21 +238,24 @@ export async function mockFetchR2Objects(
221
238
  method: "PUT" | "GET" | "DELETE";
222
239
  }
223
240
  ): Promise<Response> {
224
- /**
225
- * Here we destroy & removeListeners to "drain" the stream, for testing purposes
226
- * mimicking the fetch request taking in the stream and draining it.
227
- */
228
- if (bodyInit.body instanceof Readable) {
229
- bodyInit.body.destroy();
230
- bodyInit.body.removeAllListeners();
231
- }
232
-
233
241
  if (r2GetMocks.has(resource)) {
242
+ /**
243
+ * Here we destroy & removeListeners to "drain" the stream, for testing purposes
244
+ * mimicking the fetch request taking in the stream and draining it.
245
+ */
246
+ if (bodyInit.body instanceof Readable) {
247
+ bodyInit.body.destroy();
248
+ bodyInit.body.removeAllListeners();
249
+ }
250
+
234
251
  const value = r2GetMocks.get(resource);
235
252
 
236
253
  return new Response(value);
237
254
  }
238
- throw new Error(`no mock found for \`r2 object\` - ${resource}`);
255
+
256
+ // No mocks found for ${init.method ?? "any HTTP"} request to ${resource}
257
+ // let it fall through to Mock Service Worker with a real fetch.
258
+ return await realFetchR2Objects(resource, bodyInit);
239
259
  }
240
260
 
241
261
  /**
@@ -0,0 +1,13 @@
1
+ // barrel import for msw handlers
2
+ import { mswNamespacesHandlers } from "./namespaces";
3
+ import { mswOauthHandlers } from "./oauth";
4
+ import { mswR2handlers } from "./r2";
5
+ import { mswUserHandlers } from "./user";
6
+
7
+ // All the handlers are used in msw/index for setting up the server
8
+ export const mswDefaultHandlers = [
9
+ ...mswOauthHandlers,
10
+ ...mswR2handlers,
11
+ ...mswUserHandlers,
12
+ ...mswNamespacesHandlers,
13
+ ];
@@ -0,0 +1,104 @@
1
+ import { rest } from "msw";
2
+
3
+ export const mswNamespacesHandlers = [
4
+ rest.post(
5
+ "*/accounts/:accountId/workers/dispatch/namespaces",
6
+ (request, response, context) => {
7
+ return response(
8
+ context.status(200),
9
+ context.json({
10
+ success: true,
11
+ errors: [],
12
+ messages: [],
13
+ result: {
14
+ namespace_id: "some-namespace-id",
15
+ namespace_name: "namespace-name",
16
+ created_on: "2022-06-29T14:30:08.16152Z",
17
+ created_by: "1fc1df98cc4420fe00367c3ab68c1639",
18
+ modified_on: "2022-06-29T14:30:08.16152Z",
19
+ modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
20
+ },
21
+ })
22
+ );
23
+ }
24
+ ),
25
+ rest.delete(
26
+ "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
27
+ (_, response, context) => {
28
+ return response(
29
+ context.status(200),
30
+ context.json({
31
+ success: true,
32
+ errors: [],
33
+ messages: [],
34
+ result: null,
35
+ })
36
+ );
37
+ }
38
+ ),
39
+ rest.put(
40
+ "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
41
+ (_, response, context) => {
42
+ return response(
43
+ context.status(200),
44
+ context.json({
45
+ success: true,
46
+ errors: [],
47
+ messages: [],
48
+ result: {
49
+ namespace_id: "some-namespace-id",
50
+ namespace_name: "namespace-name",
51
+ created_on: "2022-06-29T14:30:08.16152Z",
52
+ created_by: "1fc1df98cc4420fe00367c3ab68c1639",
53
+ modified_on: "2022-06-29T14:30:08.16152Z",
54
+ modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
55
+ },
56
+ })
57
+ );
58
+ }
59
+ ),
60
+ rest.get(
61
+ "*/accounts/:accountId/workers/dispatch/namespaces/:namespaceName",
62
+ (_, response, context) => {
63
+ return response(
64
+ context.status(200),
65
+ context.json({
66
+ success: true,
67
+ errors: [],
68
+ messages: [],
69
+ result: {
70
+ namespace_id: "some-namespace-id",
71
+ namespace_name: "namespace-name",
72
+ created_on: "2022-06-29T14:30:08.16152Z",
73
+ created_by: "1fc1df98cc4420fe00367c3ab68c1639",
74
+ modified_on: "2022-06-29T14:30:08.16152Z",
75
+ modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
76
+ },
77
+ })
78
+ );
79
+ }
80
+ ),
81
+ rest.get(
82
+ "*/accounts/:accountId/workers/dispatch/namespaces",
83
+ (_, response, context) => {
84
+ return response(
85
+ context.status(200),
86
+ context.json({
87
+ success: true,
88
+ errors: [],
89
+ messages: [],
90
+ result: [
91
+ {
92
+ namespace_id: "some-namespace-id",
93
+ namespace_name: "namespace-name",
94
+ created_on: "2022-06-29T14:30:08.16152Z",
95
+ created_by: "1fc1df98cc4420fe00367c3ab68c1639",
96
+ modified_on: "2022-06-29T14:30:08.16152Z",
97
+ modified_by: "1fc1df98cc4420fe00367c3ab68c1639",
98
+ },
99
+ ],
100
+ })
101
+ );
102
+ }
103
+ ),
104
+ ];
@@ -0,0 +1,36 @@
1
+ import { rest } from "msw";
2
+
3
+ export const mswOauthHandlers = [
4
+ rest.all("*/oauth/callback", (_, response, context) => {
5
+ return response(
6
+ context.status(200),
7
+ context.json({
8
+ success: true,
9
+ errors: [],
10
+ messages: [],
11
+ code: "test-oauth-code",
12
+ })
13
+ );
14
+ }),
15
+ // revoke access token
16
+ rest.post(
17
+ "https://dash.cloudflare.com/oauth2/revoke",
18
+ (_, response, context) => response(context.status(200), context.text(""))
19
+ ),
20
+
21
+ // exchange (auth code | refresh token) for access token
22
+ rest.post(
23
+ "https://dash.cloudflare.com/oauth2/token",
24
+ (_, response, context) => {
25
+ return response(
26
+ context.status(200),
27
+ context.json({
28
+ access_token: "test-access-token",
29
+ expires_in: 100000,
30
+ refresh_token: "test-refresh-token",
31
+ scope: "account:read",
32
+ })
33
+ );
34
+ }
35
+ ),
36
+ ];
@@ -0,0 +1,80 @@
1
+ import { rest } from "msw";
2
+
3
+ export const mswR2handlers = [
4
+ // List endpoint r2Buckets
5
+ rest.get("*/accounts/:accountId/r2/buckets", (_, response, context) =>
6
+ response(
7
+ context.status(200),
8
+ context.json({
9
+ success: true,
10
+ errors: [],
11
+ messages: [],
12
+ result: {
13
+ buckets: [
14
+ { name: "bucket-1", creation_date: "01-01-2001" },
15
+ { name: "bucket-2", creation_date: "01-01-2001" },
16
+ ],
17
+ },
18
+ })
19
+ )
20
+ ),
21
+ rest.post("*/accounts/:accountId/r2/buckets", (_, response, context) =>
22
+ response(
23
+ context.status(200),
24
+ context.json({ success: true, errors: [], messages: [], result: {} })
25
+ )
26
+ ),
27
+ rest.put(
28
+ "*/accounts/:accountId/r2/buckets/:bucketName",
29
+ (_, response, context) =>
30
+ response(
31
+ context.status(200),
32
+ context.json({ success: true, errors: [], messages: [], result: {} })
33
+ )
34
+ ),
35
+ rest.delete(
36
+ "*/accounts/:accountId/r2/buckets/:bucketName",
37
+ (_, response, context) =>
38
+ response(
39
+ context.status(200),
40
+ context.json({ success: true, errors: [], messages: [], result: null })
41
+ )
42
+ ),
43
+ rest.get(
44
+ "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
45
+ (_, response, context) => {
46
+ const imageBuffer = Buffer.from("wormhole-img.png");
47
+ return response(
48
+ context.set("Content-Length", imageBuffer.byteLength.toString()),
49
+ context.set("Content-Type", "image/png"),
50
+ context.status(200),
51
+ context.body(imageBuffer)
52
+ );
53
+ }
54
+ ),
55
+ rest.put(
56
+ "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
57
+ (_, response, context) =>
58
+ response(
59
+ context.status(200),
60
+ context.json({
61
+ success: true,
62
+ errors: [],
63
+ messages: [],
64
+ result: {
65
+ accountId: "some-account-id",
66
+ bucketName: "bucketName-object-test",
67
+ objectName: "wormhole-img.png",
68
+ },
69
+ })
70
+ )
71
+ ),
72
+ rest.delete(
73
+ "*/accounts/:accountId/r2/buckets/:bucketName/objects/:objectName",
74
+ (_, response, context) =>
75
+ response(
76
+ context.status(200),
77
+ context.json({ success: true, errors: [], messages: [], result: null })
78
+ )
79
+ ),
80
+ ];
@@ -0,0 +1,63 @@
1
+ import { rest } from "msw";
2
+
3
+ export const mswUserHandlers = [
4
+ rest.get("*/user", (_, res, cxt) => {
5
+ return res(
6
+ cxt.status(200),
7
+ cxt.json({
8
+ success: true,
9
+ errors: [],
10
+ messages: [],
11
+ result: {
12
+ id: "7c5dae5552338874e5053f2534d2767a",
13
+ email: "user@example.com",
14
+ first_name: "John",
15
+ last_name: "Appleseed",
16
+ username: "cfuser12345",
17
+ telephone: "+1 123-123-1234",
18
+ country: "US",
19
+ zipcode: "12345",
20
+ created_on: "2014-01-01T05:20:00Z",
21
+ modified_on: "2014-01-01T05:20:00Z",
22
+ two_factor_authentication_enabled: false,
23
+ suspended: false,
24
+ },
25
+ })
26
+ );
27
+ }),
28
+ rest.get("*/accounts", (_, res, cxt) => {
29
+ return res(
30
+ cxt.status(200),
31
+ cxt.json({
32
+ success: true,
33
+ errors: [],
34
+ messages: [],
35
+ result: [
36
+ { name: "Account One", id: "account-1" },
37
+ { name: "Account Two", id: "account-2" },
38
+ { name: "Account Three", id: "account-3" },
39
+ ],
40
+ })
41
+ );
42
+ }),
43
+ rest.get("*/memberships", (_, response, context) => {
44
+ return response(
45
+ context.status(200),
46
+ context.json({
47
+ success: true,
48
+ errors: [],
49
+ messages: [],
50
+ result: [
51
+ {
52
+ id: "membership-id-1",
53
+ account: { id: "account-id-1", name: "My Personal Account" },
54
+ },
55
+ {
56
+ id: "membership-id-2",
57
+ account: { id: "account-id-2", name: "Enterprise Account" },
58
+ },
59
+ ],
60
+ })
61
+ );
62
+ }),
63
+ ];
@@ -0,0 +1,4 @@
1
+ import { setupServer } from "msw/node";
2
+ import { mswDefaultHandlers } from "./handlers";
3
+
4
+ export const msw = setupServer(...mswDefaultHandlers);
@@ -36,6 +36,7 @@ describe("wrangler", () => {
36
36
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
37
37
  wrangler tail [worker] 🦚 Starts a log tailing session for a published Worker.
38
38
  wrangler secret 🤫 Generate a secret that can be referenced in a Worker
39
+ wrangler secret:bulk <json> 🗄️ Bulk upload secrets for a Worker
39
40
  wrangler kv:namespace 🗂️ Interact with your Workers KV Namespaces
40
41
  wrangler kv:key 🔑 Individually manage Workers KV key-value pairs
41
42
  wrangler kv:bulk 💪 Interact with multiple Workers KV key-value pairs at once
@@ -75,6 +76,7 @@ describe("wrangler", () => {
75
76
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
76
77
  wrangler tail [worker] 🦚 Starts a log tailing session for a published Worker.
77
78
  wrangler secret 🤫 Generate a secret that can be referenced in a Worker
79
+ wrangler secret:bulk <json> 🗄️ Bulk upload secrets for a Worker
78
80
  wrangler kv:namespace 🗂️ Interact with your Workers KV Namespaces
79
81
  wrangler kv:key 🔑 Individually manage Workers KV key-value pairs
80
82
  wrangler kv:bulk 💪 Interact with multiple Workers KV key-value pairs at once
@@ -14,6 +14,7 @@ import {
14
14
  mockFetchR2Objects,
15
15
  } from "./helpers/mock-cfetch";
16
16
  import { MockWebSocket } from "./helpers/mock-web-socket";
17
+ import { msw } from "./helpers/msw";
17
18
 
18
19
  /**
19
20
  * The relative path between the bundled code and the Wrangler package.
@@ -57,6 +58,21 @@ fetchMock.doMock(() => {
57
58
 
58
59
  jest.mock("../package-manager");
59
60
 
61
+ // requests not mocked with `jest-fetch-mock` fall through
62
+ // to `mock-service-worker`
63
+ fetchMock.dontMock();
64
+ beforeAll(() => {
65
+ msw.listen({
66
+ onUnhandledRequest: (request) => {
67
+ throw new Error(
68
+ `No mock found for ${request.method} ${request.url.href}`
69
+ );
70
+ },
71
+ });
72
+ });
73
+ afterEach(() => msw.resetHandlers());
74
+ afterAll(() => msw.close());
75
+
60
76
  jest.mock("../cfetch/internal");
61
77
  (fetchInternal as jest.Mock).mockImplementation(mockFetchInternal);
62
78
  (fetchKVGetValue as jest.Mock).mockImplementation(mockFetchKVGetValue);
@@ -6488,6 +6488,58 @@ addEventListener('fetch', event => {});`
6488
6488
  `);
6489
6489
  });
6490
6490
  });
6491
+
6492
+ it("should publish if the last deployed source check fails", async () => {
6493
+ unsetAllMocks();
6494
+ writeWorkerSource();
6495
+ writeWranglerToml();
6496
+ mockSubDomainRequest();
6497
+ mockUploadWorkerRequest();
6498
+ setMockRawResponse(
6499
+ "/accounts/:accountId/workers/services/:scriptName",
6500
+ "GET",
6501
+ () => {
6502
+ return createFetchResult(null, false, [
6503
+ { code: 10090, message: "workers.api.error.service_not_found" },
6504
+ ]);
6505
+ }
6506
+ );
6507
+
6508
+ await runWrangler("publish index.js");
6509
+ expect(std).toMatchInlineSnapshot(`
6510
+ Object {
6511
+ "debug": "",
6512
+ "err": "",
6513
+ "out": "Total Upload: xx KiB / gzip: xx KiB
6514
+ Uploaded test-name (TIMINGS)
6515
+ Published test-name (TIMINGS)
6516
+ https://test-name.test-sub-domain.workers.dev",
6517
+ "warn": "",
6518
+ }
6519
+ `);
6520
+ });
6521
+
6522
+ it("should not publish if there's any other kind of error when checking deployment source", async () => {
6523
+ unsetAllMocks();
6524
+ writeWorkerSource();
6525
+ writeWranglerToml();
6526
+ mockSubDomainRequest();
6527
+ mockUploadWorkerRequest();
6528
+ setMockRawResponse(
6529
+ "/accounts/:accountId/workers/services/:scriptName",
6530
+ "GET",
6531
+ () => {
6532
+ return createFetchResult(null, false, [
6533
+ { code: 10000, message: "Authentication error" },
6534
+ ]);
6535
+ }
6536
+ );
6537
+
6538
+ await runWrangler("publish index.js");
6539
+ expect(std.err).toContain(
6540
+ `A request to the Cloudflare API (/accounts/some-account-id/workers/services/test-name) failed`
6541
+ );
6542
+ });
6491
6543
  });
6492
6544
 
6493
6545
  /** Write mock assets to the file system so they can be uploaded. */