wrangler 0.0.0-e9a1820 → 0.0.0-e9ab55a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,84 @@
1
- // This file mocks ../cfetch.ts
2
- // so we can insert whatever responses we want from it
1
+ import type { RequestInit } from "node-fetch";
2
+ import { URLSearchParams } from "node:url";
3
3
  import { pathToRegexp } from "path-to-regexp";
4
+ import { CF_API_BASE_URL } from "../cfetch";
5
+ import type { FetchResult } from "../cfetch";
6
+
7
+ /**
8
+ * The signature of the function that will handle a mock request.
9
+ */
10
+ export type MockHandler<ResponseType> = (
11
+ uri: RegExpExecArray,
12
+ init: RequestInit,
13
+ queryParams: URLSearchParams
14
+ ) => ResponseType;
4
15
 
5
- // Sadly we cannot give use the correct `RequestInit` type from node-fetch.
6
- // Jest needs to transform the code as part of the module mocking, and it doesn't know how to cope with such types.
7
- type RequestInit = { method: string; body: string };
8
- type MockHandler = (uri: RegExpExecArray, init?: RequestInit) => unknown;
9
- type MockFetch = {
10
- regexp: RegExp;
11
- method: string | undefined;
12
- handler: MockHandler;
13
- };
14
16
  type RemoveMockFn = () => void;
15
17
 
16
- let mocks: MockFetch[] = [];
18
+ interface MockFetch<ResponseType> {
19
+ regexp: RegExp;
20
+ method: string | undefined;
21
+ handler: MockHandler<ResponseType>;
22
+ }
23
+ const mocks: MockFetch<unknown>[] = [];
17
24
 
18
- export default function mockCfetch(
25
+ /**
26
+ * The mock implementation of `cfApi.fetch()`.
27
+ *
28
+ * This function will attempt to match the given request to one of the mock handlers configured by calls to `setMock`.
29
+ *
30
+ * Once found the handler will be used to generate a mock response.
31
+ */
32
+ export async function mockFetchInternal(
19
33
  resource: string,
20
- init: RequestInit
21
- ): unknown {
34
+ init: RequestInit = {},
35
+ queryParams: URLSearchParams = new URLSearchParams()
36
+ ) {
22
37
  for (const { regexp, method, handler } of mocks) {
23
- const uri = regexp.exec(resource);
38
+ const resourcePath = new URL(resource, CF_API_BASE_URL).pathname;
39
+ const uri = regexp.exec(resourcePath);
24
40
  // Do the resource path and (if specified) the HTTP method match?
25
41
  if (uri !== null && (!method || method === init.method)) {
26
42
  // The `resource` regular expression will extract the labelled groups from the URL.
27
43
  // These are passed through to the `handler` call, to allow it to do additional checks or behaviour.
28
- return handler(uri, init); // TODO: should we have some kind of fallthrough system? we'll see.
44
+ return handler(uri, init, queryParams); // TODO: should we have some kind of fallthrough system? we'll see.
29
45
  }
30
46
  }
31
- throw new Error(`no mocks found for ${resource}`);
47
+ throw new Error(`no mocks found for ${init.method}: ${resource}`);
32
48
  }
33
49
 
34
50
  /**
35
- * Specify an expected resource path that is to be handled.
51
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
52
+ *
53
+ * @param resource The path of the resource to be matched.
54
+ * This can include wildcards whose value will be passed to the `handler`.
55
+ * @param handler The function that will generate the mock response for this request.
36
56
  */
37
- export function setMock(
57
+ export function setMockRawResponse<ResponseType>(
58
+ resource: string,
59
+ handler: MockHandler<ResponseType>
60
+ ): RemoveMockFn;
61
+ /**
62
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
63
+ *
64
+ * @param resource The path of the resource to be matched.
65
+ * This can include wildcards whose value will be passed to the `handler`.
66
+ * @param method The HTTP method (e.g. GET, POST, etc) that the request must have to match this mock handler.
67
+ * @param handler The function that will generate the mock response for this request.
68
+ */
69
+ export function setMockRawResponse<ResponseType>(
38
70
  resource: string,
39
71
  method: string,
40
- handler: MockHandler
72
+ handler: MockHandler<ResponseType>
41
73
  ): RemoveMockFn;
42
- export function setMock(resource: string, handler: MockHandler): RemoveMockFn;
43
- export function setMock(resource: string, ...args: unknown[]): RemoveMockFn {
44
- const handler = args.pop() as MockHandler;
74
+ /**
75
+ * Specify an expected resource path that is to be handled, resulting in a raw JSON response.
76
+ */
77
+ export function setMockRawResponse<ResponseType>(
78
+ resource: string,
79
+ ...args: [string, MockHandler<ResponseType>] | [MockHandler<ResponseType>]
80
+ ): RemoveMockFn {
81
+ const handler = args.pop() as MockHandler<ResponseType>;
45
82
  const method = args.pop() as string;
46
83
  const mock = {
47
84
  resource,
@@ -51,13 +88,84 @@ export function setMock(resource: string, ...args: unknown[]): RemoveMockFn {
51
88
  };
52
89
  mocks.push(mock);
53
90
  return () => {
54
- mocks = mocks.filter((x) => x !== mock);
91
+ const mockIndex = mocks.indexOf(mock);
92
+ if (mockIndex !== -1) {
93
+ mocks.splice(mockIndex, 1);
94
+ }
55
95
  };
56
96
  }
57
97
 
58
- export function unsetAllMocks() {
59
- mocks = [];
98
+ /**
99
+ * Specify an expected resource path that is to be handled, resulting in a `FetchRequest`.
100
+ *
101
+ * The mock `handler` should return the `result`, which will then be wrapped in a `FetchRequest` object.
102
+ *
103
+ * @param resource The path of the resource to be matched.
104
+ * This can include wildcards whose value will be passed to the `handler`.
105
+ * @param handler The function that will generate the mock response for this request.
106
+ */
107
+ export function setMockResponse<ResponseType>(
108
+ resource: string,
109
+ handler: MockHandler<ResponseType>
110
+ ): RemoveMockFn;
111
+ /**
112
+ * Specify an expected resource path that is to be handled, resulting in a FetchRequest..
113
+ *
114
+ * @param resource The path of the resource to be matched.
115
+ * This can include wildcards whose value will be passed to the `handler`.
116
+ * @param method The HTTP method (e.g. GET, POST, etc) that the request must have to match this mock handler.
117
+ * @param handler The function that will generate the mock response for this request.
118
+ */
119
+ export function setMockResponse<ResponseType>(
120
+ resource: string,
121
+ method: string,
122
+ handler: MockHandler<ResponseType>
123
+ ): RemoveMockFn;
124
+ /**
125
+ * Specify an expected resource path that is to be handled, resulting in a FetchRequest.
126
+ */
127
+ export function setMockResponse<ResponseType>(
128
+ resource: string,
129
+ ...args: [string, MockHandler<ResponseType>] | [MockHandler<ResponseType>]
130
+ ): RemoveMockFn {
131
+ const handler = args.pop() as MockHandler<ResponseType>;
132
+ const method = args.pop() as string;
133
+ return setMockRawResponse(resource, method, (...handlerArgs) =>
134
+ createFetchResult(handler(...handlerArgs))
135
+ );
136
+ }
137
+
138
+ /**
139
+ * A helper to make it easier to create `FetchResult` objects in tests.
140
+ */
141
+ export function createFetchResult<ResponseType>(
142
+ result: ResponseType,
143
+ success = true,
144
+ errors = [],
145
+ messages = [],
146
+ result_info?: unknown
147
+ ): FetchResult<ResponseType> {
148
+ return result_info
149
+ ? {
150
+ result,
151
+ success,
152
+ errors,
153
+ messages,
154
+ result_info,
155
+ }
156
+ : {
157
+ result,
158
+ success,
159
+ errors,
160
+ messages,
161
+ };
60
162
  }
61
163
 
62
- export const CF_API_BASE_URL =
63
- process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
164
+ /**
165
+ * Remove all the configured mock handlers.
166
+ *
167
+ * This should be called in an `afterEach()` block to ensure that mock handlers do not leak between tests.
168
+ */
169
+ export function unsetAllMocks() {
170
+ mocks.length = 0;
171
+ }
@@ -0,0 +1,102 @@
1
+ import * as fs from "node:fs";
2
+ import { setMockResponse } from "./mock-cfetch";
3
+ import { runInTempDir } from "./run-in-tmp";
4
+ import { runWrangler } from "./run-wrangler";
5
+
6
+ describe("publish", () => {
7
+ runInTempDir();
8
+
9
+ it("should be able to use `index` with no extension as the entry-point", async () => {
10
+ writeWranglerToml();
11
+ writeEsmWorkerSource();
12
+ mockUploadWorkerRequest();
13
+ mockSubDomainRequest();
14
+
15
+ const { stdout, stderr, error } = await runWrangler("publish ./index");
16
+
17
+ expect(stdout).toMatchInlineSnapshot(`
18
+ "Uploaded
19
+ test-name
20
+ (0.00 sec)
21
+ Deployed
22
+ test-name
23
+ (0.00 sec)
24
+
25
+ test-name.test-sub-domain.workers.dev"
26
+ `);
27
+ expect(stderr).toMatchInlineSnapshot(`""`);
28
+ expect(error).toMatchInlineSnapshot(`undefined`);
29
+ });
30
+
31
+ it("should be able to use the `build.upload.main` config as the entry-point for ESM sources", async () => {
32
+ writeWranglerToml("./index.js");
33
+ writeEsmWorkerSource();
34
+ mockUploadWorkerRequest();
35
+ mockSubDomainRequest();
36
+
37
+ const { stdout, stderr, error } = await runWrangler("publish");
38
+
39
+ expect(stdout).toMatchInlineSnapshot(`
40
+ "Uploaded
41
+ test-name
42
+ (0.00 sec)
43
+ Deployed
44
+ test-name
45
+ (0.00 sec)
46
+
47
+ test-name.test-sub-domain.workers.dev"
48
+ `);
49
+ expect(stderr).toMatchInlineSnapshot(`""`);
50
+ expect(error).toMatchInlineSnapshot(`undefined`);
51
+ });
52
+ });
53
+
54
+ /** Write a mock wrangler.toml file to disk. */
55
+ function writeWranglerToml(main?: string) {
56
+ fs.writeFileSync(
57
+ "./wrangler.toml",
58
+ [
59
+ `compatibility_date = "2022-01-12"`,
60
+ `name = "test-name"`,
61
+ main !== undefined ? `[build.upload]\nmain = "${main}"` : "",
62
+ ].join("\n"),
63
+ "utf-8"
64
+ );
65
+ }
66
+
67
+ /** Write a mock Worker script to disk. */
68
+ function writeEsmWorkerSource() {
69
+ fs.writeFileSync(
70
+ "index.js",
71
+ [
72
+ `import { foo } from "./another";`,
73
+ `export default {`,
74
+ ` async fetch(request) {`,
75
+ ` return new Response('Hello' + foo);`,
76
+ ` },`,
77
+ `};`,
78
+ ].join("\n")
79
+ );
80
+ fs.writeFileSync("another.js", `export const foo = 100;`);
81
+ }
82
+
83
+ /** Create a mock handler for the request to upload a worker script. */
84
+ function mockUploadWorkerRequest(available_on_subdomain = true) {
85
+ setMockResponse(
86
+ "/accounts/:accountId/workers/scripts/:scriptName",
87
+ "PUT",
88
+ ([_url, accountId, scriptName], _init, queryParams) => {
89
+ expect(accountId).toEqual("some-account-id");
90
+ expect(scriptName).toEqual("test-name");
91
+ expect(queryParams.get("available_on_subdomains")).toEqual("true");
92
+ return { available_on_subdomain };
93
+ }
94
+ );
95
+ }
96
+
97
+ /** Create a mock handler the request for the account's subdomain. */
98
+ function mockSubDomainRequest(subdomain = "test-sub-domain") {
99
+ setMockResponse("/accounts/:accountId/workers/subdomain", () => {
100
+ return { subdomain };
101
+ });
102
+ }
@@ -23,7 +23,7 @@ export function toMimeType(type: CfModuleType): string {
23
23
  }
24
24
  }
25
25
 
26
- function toModule(module: CfModule, entryType?: CfModuleType): Blob {
26
+ function toModule(module: CfModule, entryType: CfModuleType): Blob {
27
27
  const { type: moduleType, content } = module;
28
28
  const type = toMimeType(moduleType ?? entryType);
29
29
 
@@ -1,7 +1,7 @@
1
- import cfetch from "../cfetch";
1
+ import fetch from "node-fetch";
2
+ import { fetchResult } from "../cfetch";
2
3
  import { toFormData } from "./form_data";
3
4
  import type { CfAccount, CfWorkerInit } from "./worker";
4
- import fetch from "node-fetch";
5
5
 
6
6
  /**
7
7
  * A preview mode.
@@ -60,7 +60,7 @@ async function sessionToken(account: CfAccount): Promise<CfPreviewToken> {
60
60
  ? `/zones/${zoneId}/workers/edge-preview`
61
61
  : `/accounts/${accountId}/workers/subdomain/edge-preview`;
62
62
 
63
- const { exchange_url } = await cfetch<{ exchange_url: string }>(initUrl);
63
+ const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl);
64
64
  const { inspector_websocket, token } = (await (
65
65
  await fetch(exchange_url)
66
66
  ).json()) as { inspector_websocket: string; token: string };
@@ -106,7 +106,7 @@ export async function previewToken(
106
106
  const formData = toFormData(worker);
107
107
  formData.set("wrangler-session-config", JSON.stringify(mode));
108
108
 
109
- const { preview_token } = await cfetch<{ preview_token: string }>(url, {
109
+ const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
110
110
  method: "POST",
111
111
  // @ts-expect-error TODO: fix this
112
112
  body: formData,
package/src/api/worker.ts CHANGED
@@ -112,7 +112,7 @@ export interface CfWorkerInit {
112
112
  /**
113
113
  * The name of the worker.
114
114
  */
115
- name: string | void;
115
+ name: string | undefined;
116
116
  /**
117
117
  * The entrypoint module.
118
118
  */
@@ -120,7 +120,7 @@ export interface CfWorkerInit {
120
120
  /**
121
121
  * The list of additional modules.
122
122
  */
123
- modules: void | CfModule[];
123
+ modules: undefined | CfModule[];
124
124
  /**
125
125
  * All the bindings
126
126
  */
@@ -130,10 +130,10 @@ export interface CfWorkerInit {
130
130
  vars?: CfVars;
131
131
  services?: CfService[];
132
132
  };
133
- migrations: void | CfDurableObjectMigrations;
134
- compatibility_date: string | void;
135
- compatibility_flags: void | string[];
136
- usage_model: void | "bundled" | "unbound";
133
+ migrations: undefined | CfDurableObjectMigrations;
134
+ compatibility_date: string | undefined;
135
+ compatibility_flags: undefined | string[];
136
+ usage_model: undefined | "bundled" | "unbound";
137
137
  }
138
138
 
139
139
  /**
@@ -0,0 +1,102 @@
1
+ import type { RequestInit } from "node-fetch";
2
+ import { URLSearchParams } from "url";
3
+ import { fetchInternal } from "./internal";
4
+
5
+ // Check out https://api.cloudflare.com/ for API docs.
6
+
7
+ export { CF_API_BASE_URL } from "./internal";
8
+
9
+ export interface FetchError {
10
+ code: number;
11
+ message: string;
12
+ }
13
+ export interface FetchResult<ResponseType = unknown> {
14
+ success: boolean;
15
+ result: ResponseType;
16
+ errors: FetchError[];
17
+ messages: string[];
18
+ result_info?: unknown;
19
+ }
20
+
21
+ /**
22
+ * Make a fetch request for a raw JSON value.
23
+ */
24
+ export async function fetchRaw<ResponseType>(
25
+ resource: string,
26
+ init: RequestInit = {},
27
+ queryParams?: URLSearchParams
28
+ ): Promise<ResponseType> {
29
+ return fetchInternal<ResponseType>(resource, init, queryParams);
30
+ }
31
+
32
+ /**
33
+ * Make a fetch request, and extract the `result` from the JSON response.
34
+ */
35
+ export async function fetchResult<ResponseType>(
36
+ resource: string,
37
+ init: RequestInit = {},
38
+ queryParams?: URLSearchParams
39
+ ): Promise<ResponseType> {
40
+ const json = await fetchInternal<FetchResult<ResponseType>>(
41
+ resource,
42
+ init,
43
+ queryParams
44
+ );
45
+ if (json.success) {
46
+ return json.result;
47
+ } else {
48
+ throwFetchError(resource, json);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Make a fetch request for a list of values,
54
+ * extracting the `result` from the JSON response,
55
+ * and repeating the request if the results are paginated.
56
+ */
57
+ export async function fetchListResult<ResponseType>(
58
+ resource: string,
59
+ init: RequestInit = {},
60
+ queryParams?: URLSearchParams
61
+ ): Promise<ResponseType[]> {
62
+ const results: ResponseType[] = [];
63
+ let getMoreResults = true;
64
+ let cursor: string | undefined;
65
+ while (getMoreResults) {
66
+ if (cursor) {
67
+ queryParams = new URLSearchParams(queryParams);
68
+ queryParams.set("cursor", cursor);
69
+ }
70
+ const json = await fetchInternal<FetchResult<ResponseType[]>>(
71
+ resource,
72
+ init,
73
+ queryParams
74
+ );
75
+ if (json.success) {
76
+ results.push(...json.result);
77
+ if (hasCursor(json.result_info)) {
78
+ cursor = json.result_info?.cursor;
79
+ } else {
80
+ getMoreResults = false;
81
+ }
82
+ } else {
83
+ throwFetchError(resource, json);
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ function throwFetchError(
90
+ resource: string,
91
+ response: FetchResult<unknown>
92
+ ): never {
93
+ response.messages.forEach((message) => console.warn(message));
94
+ const errors = response.errors
95
+ .map((error) => `${error.code}: ${error.message}`)
96
+ .join("\n");
97
+ throw new Error(`Failed to fetch ${resource} - ${errors}`);
98
+ }
99
+
100
+ function hasCursor(result_info: unknown): result_info is { cursor: string } {
101
+ return (result_info as { cursor } | undefined)?.cursor !== undefined;
102
+ }
@@ -0,0 +1,69 @@
1
+ import fetch from "node-fetch";
2
+ import type { RequestInit, HeadersInit } from "node-fetch";
3
+ import { getAPIToken, loginOrRefreshIfRequired } from "../user";
4
+
5
+ export const CF_API_BASE_URL =
6
+ process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
7
+
8
+ /**
9
+ * Make a fetch request to the Cloudflare API.
10
+ *
11
+ * This function handles acquiring the API token and logging the caller in, as necessary.
12
+ *
13
+ * Check out https://api.cloudflare.com/ for API docs.
14
+ *
15
+ * This function should not be used directly, instead use the functions in `cfetch/index.ts`.
16
+ */
17
+ export async function fetchInternal<ResponseType>(
18
+ resource: string,
19
+ init: RequestInit = {},
20
+ queryParams?: URLSearchParams
21
+ ): Promise<ResponseType> {
22
+ await requireLoggedIn();
23
+ const apiToken = requireApiToken();
24
+ const headers = cloneHeaders(init.headers);
25
+ addAuthorizationHeader(headers, apiToken);
26
+
27
+ const queryString = queryParams ? `?${queryParams.toString()}` : "";
28
+ const response = await fetch(`${CF_API_BASE_URL}${resource}${queryString}`, {
29
+ method: "GET",
30
+ ...init,
31
+ headers,
32
+ });
33
+
34
+ if (response.ok) {
35
+ return (await response.json()) as ResponseType;
36
+ } else {
37
+ throw new Error(
38
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText}`
39
+ );
40
+ }
41
+ }
42
+
43
+ function cloneHeaders(headers: HeadersInit | undefined): HeadersInit {
44
+ return { ...headers };
45
+ }
46
+
47
+ async function requireLoggedIn(): Promise<void> {
48
+ const loggedIn = await loginOrRefreshIfRequired();
49
+ if (!loggedIn) {
50
+ throw new Error("Not logged in.");
51
+ }
52
+ }
53
+
54
+ function requireApiToken(): string {
55
+ const apiToken = getAPIToken();
56
+ if (!apiToken) {
57
+ throw new Error("No API token found.");
58
+ }
59
+ return apiToken;
60
+ }
61
+
62
+ function addAuthorizationHeader(headers: HeadersInit, apiToken: string): void {
63
+ if (headers["Authorization"]) {
64
+ throw new Error(
65
+ "The request already specifies an authorisation header - cannot add a new one."
66
+ );
67
+ }
68
+ headers["Authorization"] = `Bearer ${apiToken}`;
69
+ }