wrangler 0.0.0-e4e9eaf → 0.0.0-e9a1820

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.
@@ -0,0 +1,322 @@
1
+ import { setMock, unsetAllMocks } from "./mock-cfetch";
2
+ import { runWrangler } from "./run-wrangler";
3
+ import { runInTempDir } from "./run-in-tmp";
4
+ import { writeFileSync } from "fs";
5
+
6
+ describe("wrangler", () => {
7
+ runInTempDir();
8
+
9
+ afterEach(() => {
10
+ unsetAllMocks();
11
+ });
12
+
13
+ describe("kv:namespace", () => {
14
+ describe("create", () => {
15
+ function mockCreateRequest(expectedTitle: string) {
16
+ setMock(
17
+ "/accounts/:accountId/storage/kv/namespaces",
18
+ "POST",
19
+ ([_url, accountId], { body }) => {
20
+ expect(accountId).toEqual("some-account-id");
21
+ const title = JSON.parse(body).title;
22
+ expect(title).toEqual(expectedTitle);
23
+ return { id: "some-namespace-id" };
24
+ }
25
+ );
26
+ }
27
+
28
+ it("should error if no namespace is given", async () => {
29
+ const { error, stdout, stderr } = await runWrangler(
30
+ "kv:namespace create"
31
+ );
32
+ expect(stdout).toMatchInlineSnapshot(`""`);
33
+ expect(stderr).toMatchInlineSnapshot(`
34
+ "wrangler kv:namespace create <namespace>
35
+
36
+ Create a new namespace
37
+
38
+ Positionals:
39
+ namespace The name of the new namespace [string] [required]
40
+
41
+ Flags:
42
+ -c, --config Path to .toml configuration file [string]
43
+ -h, --help Show help [boolean]
44
+ -v, --version Show version number [boolean]
45
+
46
+ Options:
47
+ -l, --local Run on my machine [boolean] [default: false]
48
+ --env Perform on a specific environment [string]
49
+ --preview Interact with a preview namespace [boolean]
50
+
51
+ Not enough non-option arguments: got 0, need at least 1"
52
+ `);
53
+ expect(error).toMatchInlineSnapshot(
54
+ `[Error: Not enough non-option arguments: got 0, need at least 1]`
55
+ );
56
+ });
57
+
58
+ it("should error if the namespace to create contains spaces", async () => {
59
+ const { error, stdout, stderr } = await runWrangler(
60
+ "kv:namespace create abc def ghi"
61
+ );
62
+ expect(stdout).toMatchInlineSnapshot(`""`);
63
+ expect(stderr).toMatchInlineSnapshot(`
64
+ "wrangler kv:namespace create <namespace>
65
+
66
+ Create a new namespace
67
+
68
+ Positionals:
69
+ namespace The name of the new namespace [string] [required]
70
+
71
+ Flags:
72
+ -c, --config Path to .toml configuration file [string]
73
+ -h, --help Show help [boolean]
74
+ -v, --version Show version number [boolean]
75
+
76
+ Options:
77
+ -l, --local Run on my machine [boolean] [default: false]
78
+ --env Perform on a specific environment [string]
79
+ --preview Interact with a preview namespace [boolean]
80
+
81
+ Unexpected additional positional arguments \\"def ghi\\"."
82
+ `);
83
+ expect(error).toMatchInlineSnapshot(
84
+ `[Error: Unexpected additional positional arguments "def ghi".]`
85
+ );
86
+ });
87
+
88
+ it("should error if the namespace to create is not valid", async () => {
89
+ const { error, stdout, stderr } = await runWrangler(
90
+ "kv:namespace create abc-def"
91
+ );
92
+ expect(stdout).toMatchInlineSnapshot(`""`);
93
+ expect(stderr).toMatchInlineSnapshot(`
94
+ "wrangler kv:namespace create <namespace>
95
+
96
+ Create a new namespace
97
+
98
+ Positionals:
99
+ namespace The name of the new namespace [string] [required]
100
+
101
+ Flags:
102
+ -c, --config Path to .toml configuration file [string]
103
+ -h, --help Show help [boolean]
104
+ -v, --version Show version number [boolean]
105
+
106
+ Options:
107
+ -l, --local Run on my machine [boolean] [default: false]
108
+ --env Perform on a specific environment [string]
109
+ --preview Interact with a preview namespace [boolean]
110
+
111
+ The namespace binding name \\"abc-def\\" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number."
112
+ `);
113
+ expect(error).toMatchInlineSnapshot(
114
+ `[Error: The namespace binding name "abc-def" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.]`
115
+ );
116
+ });
117
+
118
+ it("should create a namespace", async () => {
119
+ mockCreateRequest("worker-UnitTestNamespace");
120
+ const { stdout } = await runWrangler(
121
+ "kv:namespace create UnitTestNamespace"
122
+ );
123
+ expect(stdout).toMatchInlineSnapshot(`
124
+ "🌀 Creating namespace with title \\"worker-UnitTestNamespace\\"
125
+ ✨ Success!
126
+ Add the following to your configuration file in your kv_namespaces array:
127
+ { binding = \\"UnitTestNamespace\\", id = \\"some-namespace-id\\" }"
128
+ `);
129
+ });
130
+
131
+ it("should create a preview namespace if configured to do so", async () => {
132
+ mockCreateRequest("worker-UnitTestNamespace_preview");
133
+ const { stdout } = await runWrangler(
134
+ "kv:namespace create UnitTestNamespace --preview"
135
+ );
136
+ expect(stdout).toMatchInlineSnapshot(`
137
+ "🌀 Creating namespace with title \\"worker-UnitTestNamespace_preview\\"
138
+ ✨ Success!
139
+ Add the following to your configuration file in your kv_namespaces array:
140
+ { binding = \\"UnitTestNamespace\\", preview_id = \\"some-namespace-id\\" }"
141
+ `);
142
+ });
143
+
144
+ it("should create a namespace using configured worker name", async () => {
145
+ writeFileSync("./wrangler.toml", 'name = "otherWorker"', "utf-8");
146
+ mockCreateRequest("otherWorker-UnitTestNamespace");
147
+ const { stdout } = await runWrangler(
148
+ "kv:namespace create UnitTestNamespace"
149
+ );
150
+ expect(stdout).toMatchInlineSnapshot(`
151
+ "🌀 Creating namespace with title \\"otherWorker-UnitTestNamespace\\"
152
+ ✨ Success!
153
+ Add the following to your configuration file in your kv_namespaces array:
154
+ { binding = \\"UnitTestNamespace\\", id = \\"some-namespace-id\\" }"
155
+ `);
156
+ });
157
+
158
+ it("should create a namespace in an environment if configured to do so", async () => {
159
+ mockCreateRequest("worker-customEnv-UnitTestNamespace");
160
+ const { stdout } = await runWrangler(
161
+ "kv:namespace create UnitTestNamespace --env customEnv"
162
+ );
163
+ expect(stdout).toMatchInlineSnapshot(`
164
+ "🌀 Creating namespace with title \\"worker-customEnv-UnitTestNamespace\\"
165
+ ✨ Success!
166
+ Add the following to your configuration file in your kv_namespaces array under [env.customEnv]:
167
+ { binding = \\"UnitTestNamespace\\", id = \\"some-namespace-id\\" }"
168
+ `);
169
+ });
170
+ });
171
+
172
+ describe("list", () => {
173
+ function mockListRequest(namespaces: unknown[]) {
174
+ const requests = { count: 0 };
175
+ setMock(
176
+ "/accounts/:accountId/storage/kv/namespaces\\?:qs",
177
+ ([_url, accountId, query], init) => {
178
+ requests.count++;
179
+ expect(accountId).toEqual("some-account-id");
180
+ expect(query).toContain("per_page=100");
181
+ expect(query).toContain("order=title");
182
+ expect(query).toContain("direction=asc");
183
+ expect(query).toContain("page=");
184
+ expect(init).toBe(undefined);
185
+ const pageSize = Number(/\bper_page=(\d+)\b/.exec(query)[1]);
186
+ const page = Number(/\bpage=(\d+)/.exec(query)[1]);
187
+ return namespaces.slice((page - 1) * pageSize, page * pageSize);
188
+ }
189
+ );
190
+ return requests;
191
+ }
192
+
193
+ it("should list namespaces", async () => {
194
+ const KVNamespaces = [
195
+ { title: "title-1", id: "id-1" },
196
+ { title: "title-2", id: "id-2" },
197
+ ];
198
+ mockListRequest(KVNamespaces);
199
+ const { stdout } = await runWrangler("kv:namespace list");
200
+ const namespaces = JSON.parse(stdout);
201
+ expect(namespaces).toEqual(KVNamespaces);
202
+ });
203
+
204
+ it("should make multiple requests for paginated results", async () => {
205
+ // Create a lot of mock namespaces, so that the cfetch requests will be paginated
206
+ const KVNamespaces = [];
207
+ for (let i = 0; i < 550; i++) {
208
+ KVNamespaces.push({ title: "title-" + i, id: "id-" + i });
209
+ }
210
+ const requests = mockListRequest(KVNamespaces);
211
+ const { stdout } = await runWrangler("kv:namespace list");
212
+ const namespaces = JSON.parse(stdout);
213
+ expect(namespaces).toEqual(KVNamespaces);
214
+ expect(requests.count).toEqual(6);
215
+ });
216
+ });
217
+
218
+ describe("delete", () => {
219
+ function mockDeleteRequest(expectedNamespaceId: string) {
220
+ const requests = { count: 0 };
221
+ setMock(
222
+ "/accounts/:accountId/storage/kv/namespaces/:namespaceId",
223
+ "DELETE",
224
+ ([_url, accountId, namespaceId]) => {
225
+ requests.count++;
226
+ expect(accountId).toEqual("some-account-id");
227
+ expect(namespaceId).toEqual(expectedNamespaceId);
228
+ return null;
229
+ }
230
+ );
231
+ return requests;
232
+ }
233
+
234
+ function writeWranglerConfig() {
235
+ writeFileSync(
236
+ "./wrangler.toml",
237
+ [
238
+ 'name = "otherWorker"',
239
+ "kv_namespaces = [",
240
+ ' { binding = "someBinding", id = "bound-id", preview_id = "preview-bound-id" }',
241
+ "]",
242
+ "",
243
+ "[env.some-environment]",
244
+ "kv_namespaces = [",
245
+ ' { binding = "someBinding", id = "env-bound-id", preview_id = "preview-env-bound-id" }',
246
+ "]",
247
+ ].join("\n"),
248
+ "utf-8"
249
+ );
250
+ }
251
+
252
+ it("should delete a namespace specified by id", async () => {
253
+ const requests = mockDeleteRequest("some-namespace-id");
254
+ await runWrangler(
255
+ `kv:namespace delete --namespace-id some-namespace-id`
256
+ );
257
+ expect(requests.count).toEqual(1);
258
+ });
259
+
260
+ it("should delete a namespace specified by binding name", async () => {
261
+ writeWranglerConfig();
262
+ const requests = mockDeleteRequest("bound-id");
263
+ await runWrangler(`kv:namespace delete --binding someBinding`);
264
+ expect(requests.count).toEqual(1);
265
+ });
266
+
267
+ it("should delete a preview namespace specified by binding name", async () => {
268
+ writeWranglerConfig();
269
+ const requests = mockDeleteRequest("preview-bound-id");
270
+ await runWrangler(
271
+ `kv:namespace delete --binding someBinding --preview`
272
+ );
273
+ expect(requests.count).toEqual(1);
274
+ });
275
+
276
+ it("should error if a given binding name is not in the configured kv namespaces", async () => {
277
+ writeWranglerConfig();
278
+ const { stderr } = await runWrangler(
279
+ `kv:namespace delete --binding otherBinding`
280
+ );
281
+ expect(stderr).toMatchInlineSnapshot(`
282
+ "wrangler kv:namespace delete
283
+
284
+ Deletes a given namespace.
285
+
286
+ Flags:
287
+ -c, --config Path to .toml configuration file [string]
288
+ -h, --help Show help [boolean]
289
+ -v, --version Show version number [boolean]
290
+
291
+ Options:
292
+ -l, --local Run on my machine [boolean] [default: false]
293
+ --binding The name of the namespace to delete [string]
294
+ --namespace-id The id of the namespace to delete [string]
295
+ --env Perform on a specific environment [string]
296
+ --preview Interact with a preview namespace [boolean]
297
+
298
+ Not able to delete namespace.
299
+ A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\"."
300
+ `);
301
+ });
302
+
303
+ it("should delete a namespace specified by binding name in a given environment", async () => {
304
+ writeWranglerConfig();
305
+ const requests = mockDeleteRequest("env-bound-id");
306
+ await runWrangler(
307
+ `kv:namespace delete --binding someBinding --env some-environment`
308
+ );
309
+ expect(requests.count).toEqual(1);
310
+ });
311
+
312
+ it("should delete a preview namespace specified by binding name in a given environment", async () => {
313
+ writeWranglerConfig();
314
+ const requests = mockDeleteRequest("preview-env-bound-id");
315
+ await runWrangler(
316
+ `kv:namespace delete --binding someBinding --env some-environment --preview`
317
+ );
318
+ expect(requests.count).toEqual(1);
319
+ });
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,63 @@
1
+ // This file mocks ../cfetch.ts
2
+ // so we can insert whatever responses we want from it
3
+ import { pathToRegexp } from "path-to-regexp";
4
+
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
+ type RemoveMockFn = () => void;
15
+
16
+ let mocks: MockFetch[] = [];
17
+
18
+ export default function mockCfetch(
19
+ resource: string,
20
+ init: RequestInit
21
+ ): unknown {
22
+ for (const { regexp, method, handler } of mocks) {
23
+ const uri = regexp.exec(resource);
24
+ // Do the resource path and (if specified) the HTTP method match?
25
+ if (uri !== null && (!method || method === init.method)) {
26
+ // The `resource` regular expression will extract the labelled groups from the URL.
27
+ // 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.
29
+ }
30
+ }
31
+ throw new Error(`no mocks found for ${resource}`);
32
+ }
33
+
34
+ /**
35
+ * Specify an expected resource path that is to be handled.
36
+ */
37
+ export function setMock(
38
+ resource: string,
39
+ method: string,
40
+ handler: MockHandler
41
+ ): 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;
45
+ const method = args.pop() as string;
46
+ const mock = {
47
+ resource,
48
+ method,
49
+ handler,
50
+ regexp: pathToRegexp(resource),
51
+ };
52
+ mocks.push(mock);
53
+ return () => {
54
+ mocks = mocks.filter((x) => x !== mock);
55
+ };
56
+ }
57
+
58
+ export function unsetAllMocks() {
59
+ mocks = [];
60
+ }
61
+
62
+ export const CF_API_BASE_URL =
63
+ process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
@@ -0,0 +1,65 @@
1
+ import { confirm, prompt } from "../dialogs";
2
+
3
+ /**
4
+ * The expected values for a confirmation request.
5
+ */
6
+ export interface ConfirmExpectation {
7
+ /** The text expected to be seen in the confirmation dialog. */
8
+ text: string;
9
+ /** The mock response send back from the confirmation dialog. */
10
+ result: boolean;
11
+ }
12
+
13
+ /**
14
+ * Mock the implementation of `confirm()` that will respond with configured results
15
+ * for configured confirmation text messages.
16
+ *
17
+ * If there is a call to `confirm()` that does not match any of the expectations
18
+ * then an error is thrown.
19
+ */
20
+ export function mockConfirm(...expectations: ConfirmExpectation[]) {
21
+ (confirm as jest.Mock).mockImplementation((text: string) => {
22
+ for (const { text: expectedText, result } of expectations) {
23
+ if (text === expectedText) {
24
+ return Promise.resolve(result);
25
+ }
26
+ }
27
+ throw new Error(`Unexpected confirmation message: ${text}`);
28
+ });
29
+ }
30
+
31
+ /**
32
+ * The expected values for a prompt request.
33
+ */
34
+ export interface PromptExpectation {
35
+ /** The text expected to be seen in the prompt dialog. */
36
+ text: string;
37
+ /** The type of the prompt. */
38
+ type: "text" | "password";
39
+ /** The mock response send back from the prompt dialog. */
40
+ result: string;
41
+ }
42
+
43
+ /**
44
+ * Mock the implementation of `prompt()` that will respond with configured results
45
+ * for configured prompt text messages.
46
+ *
47
+ * If there is a call to `prompt()` that does not match any of the expectations
48
+ * then an error is thrown.
49
+ */
50
+ export function mockPrompt(...expectations: PromptExpectation[]) {
51
+ (prompt as jest.Mock).mockImplementation(
52
+ (text: string, type: "text" | "password") => {
53
+ for (const {
54
+ text: expectedText,
55
+ type: expectedType,
56
+ result,
57
+ } of expectations) {
58
+ if (text === expectedText && type == expectedType) {
59
+ return Promise.resolve(result);
60
+ }
61
+ }
62
+ throw new Error(`Unexpected confirmation message: ${text}`);
63
+ }
64
+ );
65
+ }
@@ -0,0 +1,19 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import * as fs from "fs";
4
+
5
+ const originalCwd = process.cwd();
6
+
7
+ export function runInTempDir() {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wrangler-tests"));
12
+ process.chdir(tmpDir);
13
+ });
14
+
15
+ afterEach(() => {
16
+ process.chdir(originalCwd);
17
+ fs.rmSync(tmpDir, { recursive: true });
18
+ });
19
+ }
@@ -0,0 +1,32 @@
1
+ import { main } from "../index";
2
+
3
+ /**
4
+ * Run the wrangler CLI tool in tests, capturing the logging output.
5
+ */
6
+ export async function runWrangler(cmd?: string) {
7
+ const logSpy = jest.spyOn(console, "log").mockImplementation();
8
+ const errorSpy = jest.spyOn(console, "error").mockImplementation();
9
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation();
10
+ try {
11
+ let error: unknown = undefined;
12
+ try {
13
+ await main(cmd?.split(" ") ?? []);
14
+ } catch (e) {
15
+ error = e;
16
+ }
17
+ return {
18
+ error,
19
+ stdout: captureCalls(logSpy),
20
+ stderr: captureCalls(errorSpy),
21
+ warnings: captureCalls(warnSpy),
22
+ };
23
+ } finally {
24
+ logSpy.mockRestore();
25
+ errorSpy.mockRestore();
26
+ warnSpy.mockRestore();
27
+ }
28
+ }
29
+
30
+ function captureCalls(spy: jest.SpyInstance): string {
31
+ return spy.mock.calls.flat(2).join("\n");
32
+ }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ CfWorkerInit,
3
+ CfModuleType,
4
+ CfModule,
5
+ CfDurableObjectMigrations,
6
+ } from "./worker.js";
7
+ import { FormData, Blob } from "formdata-node";
8
+
9
+ export function toMimeType(type: CfModuleType): string {
10
+ switch (type) {
11
+ case "esm":
12
+ return "application/javascript+module";
13
+ case "commonjs":
14
+ return "application/javascript";
15
+ case "compiled-wasm":
16
+ return "application/wasm";
17
+ case "buffer":
18
+ return "application/octet-stream";
19
+ case "text":
20
+ return "text/plain";
21
+ default:
22
+ throw new TypeError("Unsupported module: " + type);
23
+ }
24
+ }
25
+
26
+ function toModule(module: CfModule, entryType?: CfModuleType): Blob {
27
+ const { type: moduleType, content } = module;
28
+ const type = toMimeType(moduleType ?? entryType);
29
+
30
+ return new Blob([content], { type });
31
+ }
32
+
33
+ interface WorkerMetadata {
34
+ compatibility_date?: string;
35
+ compatibility_flags?: string[];
36
+ usage_model?: "bundled" | "unbound";
37
+ migrations?: CfDurableObjectMigrations;
38
+ bindings: (
39
+ | { type: "kv_namespace"; name: string; namespace_id: string }
40
+ | { type: "plain_text"; name: string; text: string }
41
+ | {
42
+ type: "durable_object_namespace";
43
+ name: string;
44
+ class_name: string;
45
+ script_name?: string;
46
+ }
47
+ | {
48
+ type: "service";
49
+ name: string;
50
+ service: string;
51
+ environment: string;
52
+ }
53
+ )[];
54
+ }
55
+
56
+ /**
57
+ * Creates a `FormData` upload from a `CfWorkerInit`.
58
+ */
59
+ export function toFormData(worker: CfWorkerInit): FormData {
60
+ const formData = new FormData();
61
+ const {
62
+ main,
63
+ modules,
64
+ bindings,
65
+ migrations,
66
+ usage_model,
67
+ compatibility_date,
68
+ compatibility_flags,
69
+ } = worker;
70
+
71
+ const metadataBindings: WorkerMetadata["bindings"] = [];
72
+
73
+ bindings.kv_namespaces?.forEach(({ id, binding }) => {
74
+ metadataBindings.push({
75
+ name: binding,
76
+ type: "kv_namespace",
77
+ namespace_id: id,
78
+ });
79
+ });
80
+
81
+ bindings.durable_objects?.bindings.forEach(
82
+ ({ name, class_name, script_name }) => {
83
+ metadataBindings.push({
84
+ name,
85
+ type: "durable_object_namespace",
86
+ class_name: class_name,
87
+ ...(script_name && { script_name }),
88
+ });
89
+ }
90
+ );
91
+
92
+ Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
93
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
94
+ });
95
+
96
+ bindings.services?.forEach(({ name, service, environment }) => {
97
+ metadataBindings.push({
98
+ name,
99
+ type: "service",
100
+ service,
101
+ environment,
102
+ });
103
+ });
104
+
105
+ const metadata: WorkerMetadata = {
106
+ ...(main.type !== "commonjs"
107
+ ? { main_module: main.name }
108
+ : { body_part: main.name }),
109
+ bindings: metadataBindings,
110
+ ...(compatibility_date && { compatibility_date }),
111
+ ...(compatibility_flags && { compatibility_flags }),
112
+ ...(usage_model && { usage_model }),
113
+ ...(migrations && { migrations }),
114
+ };
115
+
116
+ formData.set("metadata", JSON.stringify(metadata));
117
+
118
+ if (main.type === "commonjs" && modules && modules.length > 0) {
119
+ throw new TypeError(
120
+ "More than one module can only be specified when type = 'esm'"
121
+ );
122
+ }
123
+
124
+ for (const module of [main].concat(modules || [])) {
125
+ const { name } = module;
126
+ const blob = toModule(module, main.type ?? "esm");
127
+ formData.set(name, blob, name);
128
+ }
129
+
130
+ return formData;
131
+ }