diff --git a/.changeset/sour-toys-heal.md b/.changeset/sour-toys-heal.md new file mode 100644 index 000000000000..4eae131961af --- /dev/null +++ b/.changeset/sour-toys-heal.md @@ -0,0 +1,17 @@ +--- +"wrangler": patch +--- + +feat: enhance `wrangler init` + +This PR adds some enhancements/fixes to the `wrangler init` command. + +- doesn't overwrite `wrangler.toml` if it already exists +- installs `wrangler` when creating `package.json` +- offers to install `wrangler` into `package.json` even if `package.json` already exists +- offers to install `@cloudflare/workers-types` even if `tsconfig.json` already exists +- pipes stdio back to the terminal so there's feedback when it's installing npm packages + +This does have the side effect of making out tests slower. I added `--prefer-offline` to the `npm install` calls to make this a shade quicker, but I can't figure out a good way of mocking these. I'll think about it some more later. We should work on making the installs themselves quicker (re: https://github.com/cloudflare/wrangler2/issues/66) + +This PR also fixes a bug with our tests - `runWrangler` would catch thrown errors, and if we didn't manually verify the error, tests would pass. Instead, it now throws correctly, and I modified all the tests to assert on thrown errors. It seems like a lot, but it was just mechanical rewriting. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index eaea399282f0..f9726f45141f 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -107,6 +107,7 @@ }, "jest": { "restoreMocks": true, + "testTimeout": 30000, "testRegex": ".*.(test|spec)\\.[jt]sx?$", "transformIgnorePatterns": [ "node_modules/(?!node-fetch|fetch-blob|find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|data-uri-to-buffer|formdata-polyfill|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream)" diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index daec88f662b2..29768b456b9f 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -3,16 +3,19 @@ import * as TOML from "@iarna/toml"; import { mockConfirm } from "./mock-dialogs"; import { runWrangler } from "./run-wrangler"; import { runInTempDir } from "./run-in-tmp"; +import { mockConsoleMethods } from "./mock-console"; import * as fs from "node:fs"; describe("wrangler", () => { runInTempDir(); + const std = mockConsoleMethods(); + describe("no command", () => { it("should display a list of available commands", async () => { - const { stdout, stderr } = await runWrangler(); + await runWrangler(); - expect(stdout).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(` "wrangler Commands: @@ -36,16 +39,23 @@ describe("wrangler", () => { -l, --local Run on my machine [boolean] [default: false]" `); - expect(stderr).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(`""`); }); }); describe("invalid command", () => { it("should display an error", async () => { - const { error, stdout, stderr } = await runWrangler("invalid-command"); - - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let err: Error | undefined; + try { + await runWrangler("invalid-command"); + } catch (e) { + err = e; + } finally { + expect(err?.message).toBe(`Unknown command: invalid-command.`); + } + + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler Commands: @@ -70,9 +80,6 @@ describe("wrangler", () => { Unknown command: invalid-command." `); - expect(error).toMatchInlineSnapshot( - `[Error: Unknown command: invalid-command.]` - ); }); }); @@ -90,19 +97,27 @@ describe("wrangler", () => { }); it("should display warning when wrangler.toml already exists, and exit if user does not want to carry on", async () => { - fs.writeFileSync("./wrangler.toml", "", "utf-8"); + fs.writeFileSync( + "./wrangler.toml", + 'compatibility_date="something-else"', // use a fake value to make sure the file is not overwritten + "utf-8" + ); mockConfirm({ text: "Do you want to continue initializing this project?", result: false, }); - const { warnings } = await runWrangler("init"); - expect(warnings).toContain("wrangler.toml file already exists!"); + await runWrangler("init"); + expect(std.warn).toContain("wrangler.toml file already exists!"); const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); - expect(typeof parsed.compatibility_date).toBe("undefined"); + expect(parsed.compatibility_date).toBe("something-else"); }); it("should display warning when wrangler.toml already exists, but continue if user does want to carry on", async () => { - fs.writeFileSync("./wrangler.toml", "", "utf-8"); + fs.writeFileSync( + "./wrangler.toml", + `compatibility_date="something-else"`, + "utf-8" + ); mockConfirm( { text: "Do you want to continue initializing this project?", @@ -113,10 +128,10 @@ describe("wrangler", () => { result: false, } ); - const { warnings } = await runWrangler("init"); - expect(warnings).toContain("wrangler.toml file already exists!"); + await runWrangler("init"); + expect(std.warn).toContain("wrangler.toml file already exists!"); const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); - expect(typeof parsed.compatibility_date).toBe("string"); + expect(parsed.compatibility_date).toBe("something-else"); }); it("should create a package.json if none is found and user confirms", async () => { @@ -137,14 +152,23 @@ describe("wrangler", () => { ); expect(packageJson.name).toEqual("worker"); // TODO: should we infer the name from the directory? expect(packageJson.version).toEqual("0.0.1"); + expect(packageJson.devDependencies).toEqual({ + wrangler: expect.any(String), + }); expect(fs.existsSync("./tsconfig.json")).toBe(false); }); it("should not touch an existing package.json in the same directory", async () => { - mockConfirm({ - text: "Would you like to use typescript?", - result: false, - }); + mockConfirm( + { + text: "Would you like to install wrangler into your package.json?", + result: false, + }, + { + text: "Would you like to use typescript?", + result: false, + } + ); fs.writeFileSync( "./package.json", @@ -160,11 +184,46 @@ describe("wrangler", () => { expect(packageJson.version).toEqual("1.0.0"); }); - it("should not touch an existing package.json in an ancestor directory", async () => { - mockConfirm({ - text: "Would you like to use typescript?", - result: false, + it("should offer to install wrangler into an existing package.json", async () => { + mockConfirm( + { + text: "Would you like to install wrangler into your package.json?", + result: true, + }, + { + text: "Would you like to use typescript?", + result: false, + } + ); + + fs.writeFileSync( + "./package.json", + JSON.stringify({ name: "test", version: "1.0.0" }), + "utf-8" + ); + + await runWrangler("init"); + const packageJson = JSON.parse( + fs.readFileSync("./package.json", "utf-8") + ); + expect(packageJson.name).toEqual("test"); + expect(packageJson.version).toEqual("1.0.0"); + expect(packageJson.devDependencies).toEqual({ + wrangler: expect.any(String), }); + }); + + it("should not touch an existing package.json in an ancestor directory", async () => { + mockConfirm( + { + text: "Would you like to install wrangler into your package.json?", + result: false, + }, + { + text: "Would you like to use typescript?", + result: false, + } + ); fs.writeFileSync( "./package.json", @@ -182,8 +241,12 @@ describe("wrangler", () => { const packageJson = JSON.parse( fs.readFileSync("../../package.json", "utf-8") ); - expect(packageJson.name).toEqual("test"); - expect(packageJson.version).toEqual("1.0.0"); + expect(packageJson).toMatchInlineSnapshot(` + Object { + "name": "test", + "version": "1.0.0", + } + `); }); it("should create a tsconfig.json and install `workers-types` if none is found and user confirms", async () => { @@ -210,13 +273,21 @@ describe("wrangler", () => { ); expect(packageJson.devDependencies).toEqual({ "@cloudflare/workers-types": expect.any(String), + wrangler: expect.any(String), }); }); it("should not touch an existing tsconfig.json in the same directory", async () => { fs.writeFileSync( "./package.json", - JSON.stringify({ name: "test", version: "1.0.0" }), + JSON.stringify({ + name: "test", + version: "1.0.0", + devDependencies: { + wrangler: "0.0.0", + "@cloudflare/workers-types": "0.0.0", + }, + }), "utf-8" ); fs.writeFileSync( @@ -232,10 +303,56 @@ describe("wrangler", () => { expect(tsconfigJson.compilerOptions).toEqual({}); }); - it("should not touch an existing package.json in an ancestor directory", async () => { + it("should offer to install type definitions in an existing typescript project", async () => { + mockConfirm( + { + text: "Would you like to install wrangler into your package.json?", + result: false, + }, + { + text: "Would you like to install the type definitions for Workers into your package.json?", + result: true, + } + ); fs.writeFileSync( "./package.json", - JSON.stringify({ name: "test", version: "1.0.0" }), + JSON.stringify({ + name: "test", + version: "1.0.0", + }), + "utf-8" + ); + fs.writeFileSync( + "./tsconfig.json", + JSON.stringify({ compilerOptions: {} }), + "utf-8" + ); + + await runWrangler("init"); + const tsconfigJson = JSON.parse( + fs.readFileSync("./tsconfig.json", "utf-8") + ); + // unchanged tsconfig + expect(tsconfigJson.compilerOptions).toEqual({}); + const packageJson = JSON.parse( + fs.readFileSync("./package.json", "utf-8") + ); + expect(packageJson.devDependencies).toEqual({ + "@cloudflare/workers-types": expect.any(String), + }); + }); + + it("should not touch an existing tsconfig.json in an ancestor directory", async () => { + fs.writeFileSync( + "./package.json", + JSON.stringify({ + name: "test", + version: "1.0.0", + devDependencies: { + wrangler: "0.0.0", + "@cloudflare/workers-types": "0.0.0", + }, + }), "utf-8" ); fs.writeFileSync( @@ -258,32 +375,50 @@ describe("wrangler", () => { }); it("should error if `--type` is used", async () => { - const { error } = await runWrangler("init --type"); - expect(error).toMatchInlineSnapshot( - `[Error: The --type option is no longer supported.]` - ); + let err: undefined | Error; + try { + await runWrangler("init --type"); + } catch (e) { + err = e; + } finally { + expect(err?.message).toBe(`The --type option is no longer supported.`); + } }); it("should error if `--type javascript` is used", async () => { - const { error } = await runWrangler("init --type javascript"); - expect(error).toMatchInlineSnapshot( - `[Error: The --type option is no longer supported.]` - ); + let err: undefined | Error; + try { + await runWrangler("init --type javascript"); + } catch (e) { + err = e; + } finally { + expect(err?.message).toBe(`The --type option is no longer supported.`); + } }); it("should error if `--type rust` is used", async () => { - const { error } = await runWrangler("init --type rust"); - expect(error).toMatchInlineSnapshot( - `[Error: The --type option is no longer supported.]` - ); + let err: undefined | Error; + try { + await runWrangler("init --type rust"); + } catch (e) { + err = e; + } finally { + expect(err?.message).toBe(`The --type option is no longer supported.`); + } }); it("should error if `--type webpack` is used", async () => { - const { error } = await runWrangler("init --type webpack"); - expect(error).toMatchInlineSnapshot(` - [Error: The --type option is no longer supported. - If you wish to use webpack then you will need to create a custom build.] - `); + let err: undefined | Error; + try { + await runWrangler("init --type webpack"); + } catch (e) { + err = e; + } finally { + expect(err?.message).toBe( + `The --type option is no longer supported. +If you wish to use webpack then you will need to create a custom build.` + ); + } }); }); }); diff --git a/packages/wrangler/src/__tests__/kv.test.ts b/packages/wrangler/src/__tests__/kv.test.ts index fc2a0d177aca..310858ad0f54 100644 --- a/packages/wrangler/src/__tests__/kv.test.ts +++ b/packages/wrangler/src/__tests__/kv.test.ts @@ -8,9 +8,11 @@ import { } from "./mock-cfetch"; import { runWrangler } from "./run-wrangler"; import { runInTempDir } from "./run-in-tmp"; +import { mockConsoleMethods } from "./mock-console"; describe("wrangler", () => { runInTempDir(); + const std = mockConsoleMethods(); afterEach(() => { unsetAllMocks(); @@ -32,11 +34,14 @@ describe("wrangler", () => { } it("should error if no namespace is given", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:namespace create" - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:namespace create"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:namespace create Create a new namespace @@ -62,11 +67,14 @@ describe("wrangler", () => { }); it("should error if the namespace to create contains spaces", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:namespace create abc def ghi" - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:namespace create abc def ghi"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:namespace create Create a new namespace @@ -92,11 +100,15 @@ describe("wrangler", () => { }); it("should error if the namespace to create is not valid", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:namespace create abc-def" - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:namespace create abc-def"); + } catch (e) { + error = e; + } + + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:namespace create Create a new namespace @@ -123,10 +135,8 @@ describe("wrangler", () => { it("should create a namespace", async () => { mockCreateRequest("worker-UnitTestNamespace"); - const { stdout } = await runWrangler( - "kv:namespace create UnitTestNamespace" - ); - expect(stdout).toMatchInlineSnapshot(` + await runWrangler("kv:namespace create UnitTestNamespace"); + expect(std.out).toMatchInlineSnapshot(` "🌀 Creating namespace with title \\"worker-UnitTestNamespace\\" ✨ Success! Add the following to your configuration file in your kv_namespaces array: @@ -136,10 +146,8 @@ describe("wrangler", () => { it("should create a preview namespace if configured to do so", async () => { mockCreateRequest("worker-UnitTestNamespace_preview"); - const { stdout } = await runWrangler( - "kv:namespace create UnitTestNamespace --preview" - ); - expect(stdout).toMatchInlineSnapshot(` + await runWrangler("kv:namespace create UnitTestNamespace --preview"); + expect(std.out).toMatchInlineSnapshot(` "🌀 Creating namespace with title \\"worker-UnitTestNamespace_preview\\" ✨ Success! Add the following to your configuration file in your kv_namespaces array: @@ -150,10 +158,8 @@ describe("wrangler", () => { it("should create a namespace using configured worker name", async () => { writeFileSync("./wrangler.toml", 'name = "otherWorker"', "utf-8"); mockCreateRequest("otherWorker-UnitTestNamespace"); - const { stdout } = await runWrangler( - "kv:namespace create UnitTestNamespace" - ); - expect(stdout).toMatchInlineSnapshot(` + await runWrangler("kv:namespace create UnitTestNamespace"); + expect(std.out).toMatchInlineSnapshot(` "🌀 Creating namespace with title \\"otherWorker-UnitTestNamespace\\" ✨ Success! Add the following to your configuration file in your kv_namespaces array: @@ -163,10 +169,10 @@ describe("wrangler", () => { it("should create a namespace in an environment if configured to do so", async () => { mockCreateRequest("worker-customEnv-UnitTestNamespace"); - const { stdout } = await runWrangler( + await runWrangler( "kv:namespace create UnitTestNamespace --env customEnv" ); - expect(stdout).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(` "🌀 Creating namespace with title \\"worker-customEnv-UnitTestNamespace\\" ✨ Success! Add the following to your configuration file in your kv_namespaces array under [env.customEnv]: @@ -202,12 +208,10 @@ describe("wrangler", () => { { title: "title-2", id: "id-2" }, ]; mockListRequest(kvNamespaces); - const { error, stdout, stderr } = await runWrangler( - "kv:namespace list" - ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - const namespaces = JSON.parse(stdout); + await runWrangler("kv:namespace list"); + + expect(std.err).toMatchInlineSnapshot(`""`); + const namespaces = JSON.parse(std.out); expect(namespaces).toEqual(kvNamespaces); }); @@ -218,8 +222,8 @@ describe("wrangler", () => { kvNamespaces.push({ title: "title-" + i, id: "id-" + i }); } const requests = mockListRequest(kvNamespaces); - const { stdout } = await runWrangler("kv:namespace list"); - const namespaces = JSON.parse(stdout); + await runWrangler("kv:namespace list"); + const namespaces = JSON.parse(std.out); expect(namespaces).toEqual(kvNamespaces); expect(requests.count).toEqual(6); }); @@ -269,10 +273,17 @@ describe("wrangler", () => { it("should error if a given binding name is not in the configured kv namespaces", async () => { writeWranglerConfig(); - const { stderr } = await runWrangler( - `kv:namespace delete --binding otherBinding` - ); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:namespace delete --binding otherBinding"); + } catch (e) { + error = e; + } + expect(error).toMatchInlineSnapshot(` + [Error: Not able to delete namespace. + A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".] + `); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:namespace delete Deletes a given namespace. @@ -297,12 +308,12 @@ describe("wrangler", () => { it("should delete a namespace specified by binding name in a given environment", async () => { writeWranglerConfig(); const requests = mockDeleteRequest("env-bound-id"); - const { stdout, stderr, error } = await runWrangler( - `kv:namespace delete --binding someBinding --env some-environment --preview false` + await runWrangler( + "kv:namespace delete --binding someBinding --env some-environment --preview false" ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -358,28 +369,29 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( + + await runWrangler( "kv:key put my-key my-value --namespace-id some-namespace-id" ); + expect(requests.count).toEqual(1); - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"writing the value \\"my-value\\" to key \\"my-key\\" on namespace some-namespace-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should put a key in a given namespace specified by binding", async () => { writeWranglerConfig(); const requests = mockKeyPutRequest("bound-id", "my-key", "my-value"); - const { error, stderr, stdout } = await runWrangler( + await runWrangler( "kv:key put my-key my-value --binding someBinding --preview false" ); - expect(stdout).toMatchInlineSnapshot( + + expect(std.out).toMatchInlineSnapshot( `"writing the value \\"my-value\\" to key \\"my-key\\" on namespace bound-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -390,14 +402,15 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( + + await runWrangler( "kv:key put my-key my-value --binding someBinding --preview" ); - expect(stdout).toMatchInlineSnapshot( + + expect(std.out).toMatchInlineSnapshot( `"writing the value \\"my-value\\" to key \\"my-key\\" on namespace preview-bound-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -409,14 +422,20 @@ describe("wrangler", () => { 10, 20 ); - const { error, stderr, stdout } = await runWrangler( - "kv:key put my-key my-value --namespace-id some-namespace-id --expiration 10 --ttl 20" - ); + let error: Error | undefined; + try { + await runWrangler( + "kv:key put my-key my-value --namespace-id some-namespace-id --expiration 10 --ttl 20" + ); + } catch (e) { + error = e; + } + expect(requests.count).toEqual(1); - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"writing the value \\"my-value\\" to key \\"my-key\\" on namespace some-namespace-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(error).toMatchInlineSnapshot(`undefined`); }); @@ -427,14 +446,13 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( + await runWrangler( "kv:key put my-key my-value --binding someBinding --env some-environment --preview false" ); - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"writing the value \\"my-value\\" to key \\"my-key\\" on namespace env-bound-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -445,22 +463,26 @@ describe("wrangler", () => { "my-key", "file-contents" ); - const { error, stderr, stdout } = await runWrangler( + await runWrangler( "kv:key put my-key --namespace-id some-namespace-id --path foo.txt" ); - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"writing the contents of foo.txt to the key \\"my-key\\" on namespace some-namespace-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); it("should error if no key is provided", async () => { - const { error, stdout, stderr } = await runWrangler("kv:key put"); + let error: Error | undefined; + try { + await runWrangler("kv:key put"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key put [value] Writes a single key/value pair to the given namespace. @@ -492,12 +514,15 @@ describe("wrangler", () => { }); it("should error if no binding nor namespace is provided", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:key put foo bar" - ); + let error: Error | undefined; + try { + await runWrangler("kv:key put foo bar"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key put [value] Writes a single key/value pair to the given namespace. @@ -529,12 +554,15 @@ describe("wrangler", () => { }); it("should error if both binding and namespace is provided", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:key put foo bar --binding x --namespace-id y" - ); + let error: Error | undefined; + try { + await runWrangler("kv:key put foo bar --binding x --namespace-id y"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key put [value] Writes a single key/value pair to the given namespace. @@ -566,12 +594,15 @@ describe("wrangler", () => { }); it("should error if no value nor path is provided", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:key put key --namespace-id 12345" - ); + let error: Error | undefined; + try { + await runWrangler("kv:key put key --namespace-id 12345"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key put [value] Writes a single key/value pair to the given namespace. @@ -603,12 +634,17 @@ describe("wrangler", () => { }); it("should error if both value and path is provided", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:key put key value --path xyz --namespace-id 12345" - ); + let error: Error | undefined; + try { + await runWrangler( + "kv:key put key value --path xyz --namespace-id 12345" + ); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key put [value] Writes a single key/value pair to the given namespace. @@ -641,11 +677,15 @@ describe("wrangler", () => { it("should error if a given binding name is not in the configured kv namespaces", async () => { writeWranglerConfig(); - const { error, stdout, stderr } = await runWrangler( - `kv:key put key value --binding otherBinding` - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:key put key value --binding otherBinding"); + } catch (e) { + error = e; + } + + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\". %s @@ -663,11 +703,14 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( - "kv:key put my-key my-value --binding someBinding" - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:key put my-key my-value --binding someBinding"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "someBinding has both a namespace ID and a preview ID. Specify \\"--preview\\" or \\"--preview false\\" to avoid writing data to the wrong namespace. %s @@ -684,12 +727,9 @@ describe("wrangler", () => { it("should list the keys of a namespace specified by namespace-id", async () => { const keys = ["key-1", "key-2", "key-3"]; mockKeyListRequest("some-namespace-id", keys); - const { error, stdout, stderr } = await runWrangler( - "kv:key list --namespace-id some-namespace-id" - ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(stdout).toMatchInlineSnapshot(` + await runWrangler("kv:key list --namespace-id some-namespace-id"); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` "[ { \\"name\\": \\"key-1\\", @@ -714,12 +754,10 @@ describe("wrangler", () => { writeWranglerConfig(); const keys = ["key-1", "key-2", "key-3"]; mockKeyListRequest("bound-id", keys); - const { error, stdout, stderr } = await runWrangler( - "kv:key list --binding someBinding" - ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(stdout).toMatchInlineSnapshot(` + + await runWrangler("kv:key list --binding someBinding"); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` "[ { \\"name\\": \\"key-1\\", @@ -744,12 +782,9 @@ describe("wrangler", () => { writeWranglerConfig(); const keys = ["key-1", "key-2", "key-3"]; mockKeyListRequest("preview-bound-id", keys); - const { error, stdout, stderr } = await runWrangler( - "kv:key list --binding someBinding --preview" - ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(stdout).toMatchInlineSnapshot(` + await runWrangler("kv:key list --binding someBinding --preview"); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` "[ { \\"name\\": \\"key-1\\", @@ -774,12 +809,11 @@ describe("wrangler", () => { writeWranglerConfig(); const keys = ["key-1", "key-2", "key-3"]; mockKeyListRequest("env-bound-id", keys); - const { error, stdout, stderr } = await runWrangler( + await runWrangler( "kv:key list --binding someBinding --env some-environment" ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(stdout).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` "[ { \\"name\\": \\"key-1\\", @@ -804,12 +838,11 @@ describe("wrangler", () => { writeWranglerConfig(); const keys = ["key-1", "key-2", "key-3"]; mockKeyListRequest("preview-env-bound-id", keys); - const { error, stdout, stderr } = await runWrangler( + await runWrangler( "kv:key list --binding someBinding --preview --env some-environment" ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(stdout).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` "[ { \\"name\\": \\"key-1\\", @@ -838,30 +871,32 @@ describe("wrangler", () => { } // Ask for the keys in pages of size 100. const requests = mockKeyListRequest("some-namespace-id", keys, 100); - const { stdout, stderr, error } = await runWrangler( + await runWrangler( "kv:key list --namespace-id some-namespace-id --limit 100" ); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(JSON.parse(stdout).map((k) => k.name)).toEqual(keys); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(JSON.parse(std.out).map((k) => k.name)).toEqual(keys); expect(requests.count).toEqual(6); }); it("should error if a given binding name is not in the configured kv namespaces", async () => { writeWranglerConfig(); - const { error, stdout, stderr } = await runWrangler( - "kv:key list --binding otherBinding" - ); + let error: Error | undefined; + try { + await runWrangler("kv:key list --binding otherBinding"); + } catch (e) { + error = e; + } expect(error).toMatchInlineSnapshot( `[Error: A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".]` ); - expect(stderr).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(` "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\". %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." `); - expect(stdout).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(`""`); }); }); @@ -891,24 +926,20 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( - "kv:key get my-key --namespace-id some-namespace-id" - ); - expect(stdout).toMatchInlineSnapshot(`"my-value"`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + await runWrangler("kv:key get my-key --namespace-id some-namespace-id"); + expect(std.out).toMatchInlineSnapshot(`"my-value"`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); it("should get a key in a given namespace specified by binding", async () => { writeWranglerConfig(); const requests = mockKeyGetRequest("bound-id", "my-key", "my-value"); - const { error, stderr, stdout } = await runWrangler( + await runWrangler( "kv:key get my-key --binding someBinding --preview false" ); - expect(stdout).toMatchInlineSnapshot(`"my-value"`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.out).toMatchInlineSnapshot(`"my-value"`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -919,12 +950,9 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( - "kv:key get my-key --binding someBinding --preview" - ); - expect(stdout).toMatchInlineSnapshot(`"my-value"`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + await runWrangler("kv:key get my-key --binding someBinding --preview"); + expect(std.out).toMatchInlineSnapshot(`"my-value"`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); @@ -935,20 +963,23 @@ describe("wrangler", () => { "my-key", "my-value" ); - const { error, stderr, stdout } = await runWrangler( + await runWrangler( "kv:key get my-key my-value --binding someBinding --env some-environment --preview false" ); - expect(stdout).toMatchInlineSnapshot(`"my-value"`); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.out).toMatchInlineSnapshot(`"my-value"`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); it("should error if no key is provided", async () => { - const { error, stdout, stderr } = await runWrangler("kv:key get"); - - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:key get"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key get Reads a single value by key from the given namespace. @@ -976,10 +1007,14 @@ describe("wrangler", () => { }); it("should error if no binding nor namespace is provided", async () => { - const { error, stdout, stderr } = await runWrangler("kv:key get foo"); - - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:key get foo"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key get Reads a single value by key from the given namespace. @@ -1007,12 +1042,15 @@ describe("wrangler", () => { }); it("should error if both binding and namespace is provided", async () => { - const { error, stdout, stderr } = await runWrangler( - "kv:key get foo --binding x --namespace-id y" - ); + let error: Error | undefined; + try { + await runWrangler("kv:key get foo --binding x --namespace-id y"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "wrangler kv:key get Reads a single value by key from the given namespace. @@ -1041,11 +1079,14 @@ describe("wrangler", () => { it("should error if a given binding name is not in the configured kv namespaces", async () => { writeWranglerConfig(); - const { error, stdout, stderr } = await runWrangler( - `kv:key get key --binding otherBinding` - ); - expect(stdout).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("kv:key get key --binding otherBinding"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\". %s @@ -1105,28 +1146,34 @@ describe("wrangler", () => { it("should error if a given binding name is not in the configured kv namespaces", async () => { writeWranglerConfig(); - const { stderr } = await runWrangler( - `kv:key delete --binding otherBinding someKey` - ); - expect(stderr).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler(`kv:key delete --binding otherBinding someKey`); + } catch (e) { + error = e; + } + + expect(std.err).toMatchInlineSnapshot(` "A namespace with binding name \\"otherBinding\\" was not found in the configured \\"kv_namespaces\\". %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." `); + expect(error).toMatchInlineSnapshot( + `[Error: A namespace with binding name "otherBinding" was not found in the configured "kv_namespaces".]` + ); }); it("should delete a key in a namespace specified by binding name in a given environment", async () => { writeWranglerConfig(); const requests = mockDeleteRequest("env-bound-id", "someKey"); - const { stdout, stderr, error } = await runWrangler( + await runWrangler( `kv:key delete --binding someBinding --env some-environment --preview false someKey` ); - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"deleting the key \\"someKey\\" on namespace env-bound-id"` ); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); expect(requests.count).toEqual(1); }); diff --git a/packages/wrangler/src/__tests__/mock-console.ts b/packages/wrangler/src/__tests__/mock-console.ts new file mode 100644 index 000000000000..bdd15c13ef35 --- /dev/null +++ b/packages/wrangler/src/__tests__/mock-console.ts @@ -0,0 +1,34 @@ +/** + * We use this module to mock console methods, and optionally + * assert on the values they're called with in our tests. + */ + +let logSpy: jest.SpyInstance, + errorSpy: jest.SpyInstance, + warnSpy: jest.SpyInstance; + +const std = { + get out() { + return logSpy.mock.calls.flat(2).join("\n"); + }, + get err() { + return errorSpy.mock.calls.flat(2).join("\n"); + }, + get warn() { + return warnSpy.mock.calls.flat(2).join("\n"); + }, +}; + +export function mockConsoleMethods() { + beforeEach(() => { + logSpy = jest.spyOn(console, "log").mockImplementation(); + errorSpy = jest.spyOn(console, "error").mockImplementation(); + warnSpy = jest.spyOn(console, "warn").mockImplementation(); + }); + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + warnSpy.mockRestore(); + }); + return std; +} diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index 6956b448a24f..03cbebfbf94b 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -5,9 +5,11 @@ import { mockKeyListRequest } from "./kv.test"; import { setMockResponse, unsetAllMocks } from "./mock-cfetch"; import { runInTempDir } from "./run-in-tmp"; import { runWrangler } from "./run-wrangler"; +import { mockConsoleMethods } from "./mock-console"; describe("publish", () => { runInTempDir(); + const std = mockConsoleMethods(); afterEach(() => { unsetAllMocks(); @@ -20,9 +22,9 @@ describe("publish", () => { mockUploadWorkerRequest(); mockSubDomainRequest(); - const { stdout, stderr, error } = await runWrangler("publish ./index"); + await runWrangler("publish ./index"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) @@ -32,8 +34,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should be able to use the `build.upload.main` config as the entry-point for ESM sources", async () => { @@ -42,9 +43,9 @@ describe("publish", () => { mockUploadWorkerRequest(); mockSubDomainRequest(); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) @@ -54,8 +55,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should be able to transpile TypeScript", async () => { @@ -63,12 +63,9 @@ describe("publish", () => { writeEsmWorkerSource({ format: "ts" }); mockUploadWorkerRequest({ expectedBody: "var foo = 100;" }); mockSubDomainRequest(); + await runWrangler("publish index.ts"); - const { stdout, stderr, error } = await runWrangler( - "publish " + fs.realpathSync("./index.ts") - ); - - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) @@ -78,8 +75,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should be able to transpile entry-points in sub-directories", async () => { @@ -88,11 +84,9 @@ describe("publish", () => { mockUploadWorkerRequest({ expectedBody: "var foo = 100;" }); mockSubDomainRequest(); - const { stdout, stderr, error } = await runWrangler( - "publish ./src/index.js" - ); + await runWrangler("publish ./src/index.js"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) @@ -102,8 +96,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should warn if there is a `site.entry-point` configuration", async () => { @@ -114,11 +107,9 @@ describe("publish", () => { mockUploadWorkerRequest(); mockSubDomainRequest(); - const { stdout, stderr, error, warnings } = await runWrangler( - "publish ./index.js" - ); + await runWrangler("publish ./index.js"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) @@ -128,9 +119,8 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); - expect(warnings).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(` "Deprecation notice: The \`site.entry-point\` config field is no longer used. The entry-point should be specified via the command line (e.g. \`wrangler publish path/to/script\`) or the \`build.upload.main\` config field. Please remove the \`site.entry-point\` field from the \`wrangler.toml\` file." @@ -142,11 +132,15 @@ describe("publish", () => { writeEsmWorkerSource(); mockUploadWorkerRequest(); mockSubDomainRequest(); + let error: Error | undefined; + try { + await runWrangler("publish"); + } catch (e) { + error = e; + } - const { stdout, stderr, error } = await runWrangler("publish"); - - expect(stripTimings(stdout)).toMatchInlineSnapshot(`""`); - expect(stderr).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` "Missing entry-point: The entry-point should be specified via the command line (e.g. \`wrangler publish path/to/script\`) or the \`build.upload.main\` config field. %s @@ -176,9 +170,9 @@ describe("publish", () => { mockListKVNamespacesRequest(kvNamespace); mockKeyListRequest(kvNamespace.id, []); mockUploadAssetsToKVRequest(kvNamespace.id, assets); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... reading assets/file-2.txt... @@ -192,8 +186,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should only upload files that are not already in the KV namespace", async () => { @@ -218,9 +211,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath !== "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... skipping - already uploaded reading assets/file-2.txt... @@ -234,8 +227,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should only upload files that match the `site-include` arg", async () => { @@ -259,11 +251,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath === "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler( - "publish --site-include file-1.txt" - ); + await runWrangler("publish --site-include file-1.txt"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -275,8 +265,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should not upload files that match the `site-exclude` arg", async () => { @@ -300,11 +289,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath === "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler( - "publish --site-exclude file-2.txt" - ); + await runWrangler("publish --site-exclude file-2.txt"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -316,8 +303,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should only upload files that match the `site.include` config", async () => { @@ -345,9 +331,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath === "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -359,8 +345,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should not upload files that match the `site.exclude` config", async () => { @@ -388,9 +373,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath === "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -402,8 +387,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should use `site-include` arg over `site.include` config", async () => { @@ -431,11 +415,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath === "assets/file-1.txt") ); - const { stdout, stderr, error } = await runWrangler( - "publish --site-include file-1.txt" - ); + await runWrangler("publish --site-include file-1.txt"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -447,8 +429,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should use `site-exclude` arg over `site.exclude` config", async () => { @@ -476,11 +457,9 @@ describe("publish", () => { kvNamespace.id, assets.filter((a) => a.filePath.endsWith("assets/file-1.txt")) ); - const { stdout, stderr, error } = await runWrangler( - "publish --site-exclude file-2.txt" - ); + await runWrangler("publish --site-exclude file-2.txt"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/file-1.txt... uploading as assets/file-1.2ca234f380.txt... Uploaded @@ -492,8 +471,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should walk directories except node_modules", async () => { @@ -520,9 +498,9 @@ describe("publish", () => { mockKeyListRequest(kvNamespace.id, []); // Only expect file-1 to be uploaded mockUploadAssetsToKVRequest(kvNamespace.id, assets.slice(0, 1)); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/directory-1/file-1.txt... uploading as assets/directory-1/file-1.2ca234f380.txt... Uploaded @@ -534,8 +512,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should skip hidden files and directories except `.well-known`", async () => { @@ -566,9 +543,9 @@ describe("publish", () => { mockKeyListRequest(kvNamespace.id, []); // Only expect file-2 to be uploaded mockUploadAssetsToKVRequest(kvNamespace.id, assets.slice(2)); - const { stdout, stderr, error } = await runWrangler("publish"); + await runWrangler("publish"); - expect(stripTimings(stdout)).toMatchInlineSnapshot(` + expect(stripTimings(std.out)).toMatchInlineSnapshot(` "reading assets/.well-known/file-2.txt... uploading as assets/.well-known/file-2.5938485188.txt... Uploaded @@ -580,8 +557,7 @@ describe("publish", () => { test-name.test-sub-domain.workers.dev" `); - expect(stderr).toMatchInlineSnapshot(`""`); - expect(error).toMatchInlineSnapshot(`undefined`); + expect(std.err).toMatchInlineSnapshot(`""`); }); it("should error if the asset is over 25Mb", async () => { @@ -612,13 +588,17 @@ describe("publish", () => { mockListKVNamespacesRequest(kvNamespace); mockKeyListRequest(kvNamespace.id, []); - const { stdout, stderr, error } = await runWrangler("publish"); - - expect(stdout).toMatchInlineSnapshot(` + let error: Error | undefined; + try { + await runWrangler("publish"); + } catch (e) { + error = e; + } + expect(std.out).toMatchInlineSnapshot(` "reading assets/large-file.txt... uploading as assets/large-file.0ea0637a45.txt..." `); - expect(stderr).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(` "File assets/too-large-file.txt is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits %s @@ -646,12 +626,17 @@ describe("publish", () => { mockListKVNamespacesRequest(kvNamespace); mockKeyListRequest(kvNamespace.id, []); - const { stdout, stderr, error } = await runWrangler("publish"); + let error: Error | undefined; + try { + await runWrangler("publish"); + } catch (e) { + error = e; + } - expect(stdout).toMatchInlineSnapshot( + expect(std.out).toMatchInlineSnapshot( `"reading assets/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/file.txt..."` ); - expect(stderr).toMatchInlineSnapshot(` + expect(std.err).toMatchInlineSnapshot(` "The asset path key \\"assets/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/folder/file.3da0d0cd12.txt\\" exceeds the maximum key size limit of 512. See https://developers.cloudflare.com/workers/platform/limits#kv-limits\\", %s diff --git a/packages/wrangler/src/__tests__/run-wrangler.ts b/packages/wrangler/src/__tests__/run-wrangler.ts index fda06e5122f9..350fbe186d9e 100644 --- a/packages/wrangler/src/__tests__/run-wrangler.ts +++ b/packages/wrangler/src/__tests__/run-wrangler.ts @@ -1,32 +1,8 @@ import { main } from "../index"; /** - * Run the wrangler CLI tool in tests, capturing the logging output. + * A helper to 'run' wrangler commands for tests. */ export async function runWrangler(cmd?: string) { - const logSpy = jest.spyOn(console, "log").mockImplementation(); - const errorSpy = jest.spyOn(console, "error").mockImplementation(); - const warnSpy = jest.spyOn(console, "warn").mockImplementation(); - try { - let error: unknown = undefined; - try { - await main(cmd?.split(" ") ?? []); - } catch (e) { - error = e; - } - return { - error, - stdout: captureCalls(logSpy), - stderr: captureCalls(errorSpy), - warnings: captureCalls(warnSpy), - }; - } finally { - logSpy.mockRestore(); - errorSpy.mockRestore(); - warnSpy.mockRestore(); - } -} - -function captureCalls(spy: jest.SpyInstance): string { - return spy.mock.calls.flat(2).join("\n"); + await main(cmd?.split(" ") ?? []); } diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index 275a1ede8281..39b574a3830a 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -195,58 +195,94 @@ export async function main(argv: string[]): Promise { const destination = path.join(process.cwd(), "wrangler.toml"); if (fs.existsSync(destination)) { console.warn(`${destination} file already exists!`); - const result = await confirm( + const shouldContinue = await confirm( "Do you want to continue initializing this project?" ); - if (!result) { + if (!shouldContinue) { return; } + } else { + const compatibilityDate = new Date().toISOString().substring(0, 10); + try { + await writeFile( + destination, + `compatibility_date = "${compatibilityDate}"` + "\n" + ); + console.log(`✨ Successfully created wrangler.toml`); + // TODO: suggest next steps? + } catch (err) { + throw new Error( + `Failed to create wrangler.toml.\n${err.message ?? err}` + ); + } } - const compatibilityDate = new Date().toISOString().substring(0, 10); - try { - await writeFile( - destination, - `compatibility_date = "${compatibilityDate}"` + "\n" - ); - console.log(`✨ Successfully created wrangler.toml`); - // TODO: suggest next steps? - } catch (err) { - throw new Error( - `Failed to create wrangler.toml.\n${err.message ?? err}` - ); - } - - // if no package.json, ask, and if yes, create one let pathToPackageJson = await findUp("package.json"); - if (!pathToPackageJson) { - if ( - await confirm("No package.json found. Would you like to create one?") - ) { + // If no package.json exists, ask to create one + const shouldCreatePackageJson = await confirm( + "No package.json found. Would you like to create one?" + ); + if (shouldCreatePackageJson) { await writeFile( path.join(process.cwd(), "package.json"), JSON.stringify( { name: "worker", version: "0.0.1", + devDependencies: { + wrangler: wranglerVersion, + }, }, null, " " ) + "\n" ); + await execa("npm", ["install", "--prefer-offline"], { + stdio: "inherit", + }); console.log(`✨ Created package.json`); pathToPackageJson = path.join(process.cwd(), "package.json"); } else { return; } + } else { + // If package.json exists and wrangler isn't installed, + // then ask to add wrangler to devDependencies + const packageJson = JSON.parse( + await readFile(pathToPackageJson, "utf-8") + ); + if ( + !( + packageJson.devDependencies?.wrangler || + packageJson.dependencies?.wrangler + ) + ) { + const shouldInstall = await confirm( + "Would you like to install wrangler into your package.json?" + ); + if (shouldInstall) { + await execa( + "npm", + [ + "install", + `wrangler@${wranglerVersion}`, + "--save-dev", + "--prefer-offline", + ], + { + stdio: "inherit", + } + ); + console.log(`✨ Installed wrangler`); + } + } } - // if workers-types doesn't exist as a dependency - // offer to install it - // and make a tsconfig? let pathToTSConfig = await findUp("tsconfig.json"); if (!pathToTSConfig) { + // If there's no tsconfig, offer to create one + // and install @cloudflare/workers-types if (await confirm("Would you like to use typescript?")) { await writeFile( path.join(process.cwd(), "tsconfig.json"), @@ -271,16 +307,60 @@ export async function main(argv: string[]): Promise { " " ) + "\n" ); - await execa("npm", [ - "install", - "@cloudflare/workers-types", - "--save-dev", - ]); + await execa( + "npm", + [ + "install", + "@cloudflare/workers-types", + "--save-dev", + "--prefer-offline", + ], + { stdio: "inherit" } + ); console.log( `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies` ); pathToTSConfig = path.join(process.cwd(), "tsconfig.json"); } + } else { + // If there's a tsconfig, check if @cloudflare/workers-types + // is already installed, and offer to install it if not + const packageJson = JSON.parse( + await readFile(pathToPackageJson, "utf-8") + ); + if ( + !( + packageJson.devDependencies?.["@cloudflare/workers-types"] || + packageJson.dependencies?.["@cloudflare/workers-types"] + ) + ) { + const shouldInstall = await confirm( + "Would you like to install the type definitions for Workers into your package.json?" + ); + if (shouldInstall) { + await execa( + "npm", + [ + "install", + "@cloudflare/workers-types", + "--save-dev", + "--prefer-offline", + ], + { + stdio: "inherit", + } + ); + // We don't update the tsconfig.json because + // it could be complicated in existing projects + // and we don't want to break them. Instead, we simply + // tell the user that they need to update their tsconfig.json + console.log( + `✨ Installed @cloudflare/workers-types.\nPlease add "@cloudflare/workers-types" to compilerOptions.types in your tsconfig.json` + ); + } else { + return; + } + } } } );