diff --git a/.changeset/strange-dingos-listen.md b/.changeset/strange-dingos-listen.md new file mode 100644 index 000000000000..a8cae6872b11 --- /dev/null +++ b/.changeset/strange-dingos-listen.md @@ -0,0 +1,19 @@ +--- +"wrangler": patch +--- + +fix: validate that bindings have unique names + +We don't want to have, for example, a KV namespace named "DATA" +and a Durable Object also named "DATA". Then it would be ambiguous +what exactly would live at `env.DATA` (or in the case of service workers, +the `DATA` global) which could lead to unexpected behavior -- and errors. + +Similarly, we don't want to have multiple resources of the same type +bound to the same name. If you've been working with some KV namespace +called "DATA", and you add a second namespace but don't change the binding +to something else (maybe you're copying-and-pasting and just changed out the `id`), +you could be reading entirely the wrong stuff out of your KV store. + +So now we check for those sorts of situations and throw an error if +we find that we've encountered one. diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index bae59a6fabb2..23afdf0e7b93 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -1699,487 +1699,947 @@ export default{ }); }); - describe("[wasm_modules]", () => { - it("should be able to define wasm modules for service-worker format workers", async () => { + describe("bindings", () => { + it("should allow bindings with different names", async () => { writeWranglerToml({ - wasm_modules: { - TESTWASMNAME: "./path/to/test.wasm", + durable_objects: { + bindings: [ + { + name: "DURABLE_OBJECT_ONE", + class_name: "SomeDurableObject", + script_name: "some-durable-object-worker", + }, + { + name: "DURABLE_OBJECT_TWO", + class_name: "AnotherDurableObject", + script_name: "another-durable-object-worker", + }, + ], }, - }); - writeWorkerSource({ type: "sw" }); - fs.mkdirSync("./path/to", { recursive: true }); - fs.writeFileSync("./path/to/test.wasm", "SOME WASM CONTENT"); - mockUploadWorkerRequest({ - expectedType: "sw", - expectedModules: { TESTWASMNAME: "SOME WASM CONTENT" }, - expectedBindings: [ - { name: "TESTWASMNAME", part: "TESTWASMNAME", type: "wasm_module" }, + kv_namespaces: [ + { binding: "KV_NAMESPACE_ONE", id: "kv-ns-one-id" }, + { binding: "KV_NAMESPACE_TWO", id: "kv-ns-two-id" }, ], - }); - mockSubDomainRequest(); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); - - it("should error when defining wasm modules for modules format workers", async () => { - writeWranglerToml({ + r2_buckets: [ + { binding: "R2_BUCKET_ONE", bucket_name: "r2-bucket-one-name" }, + { binding: "R2_BUCKET_TWO", bucket_name: "r2-bucket-two-name" }, + ], + text_blobs: { + TEXT_BLOB_ONE: "./my-entire-app-depends-on-this.cfg", + TEXT_BLOB_TWO: "./the-entirety-of-human-knowledge.txt", + }, + unsafe: { + bindings: [ + { + name: "UNSAFE_BINDING_ONE", + type: "some unsafe thing", + data: { some: { unsafe: "thing" } }, + }, + { + name: "UNSAFE_BINDING_TWO", + type: "another unsafe thing", + data: 1337, + }, + ], + }, + vars: { + ENV_VAR_ONE: 123, + ENV_VAR_TWO: "Hello, I'm an environment variable", + }, wasm_modules: { - TESTWASMNAME: "./path/to/test.wasm", + WASM_MODULE_ONE: "./some_wasm.wasm", + WASM_MODULE_TWO: "./more_wasm.wasm", }, }); - writeWorkerSource({ type: "esm" }); - fs.mkdirSync("./path/to", { recursive: true }); - fs.writeFileSync("./path/to/test.wasm", "SOME WASM CONTENT"); - - await expect( - runWrangler("publish index.js") - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"` - ); - expect(std.out).toMatchInlineSnapshot(`""`); - expect(std.err).toMatchInlineSnapshot(` - "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code - - %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." - `); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); - - it("should resolve wasm modules relative to the wrangler.toml file", async () => { - fs.mkdirSync("./path/to/and/the/path/to/", { recursive: true }); - fs.writeFileSync( - "./path/to/wrangler.toml", - TOML.stringify({ - compatibility_date: "2022-01-12", - name: "test-name", - wasm_modules: { - TESTWASMNAME: "./and/the/path/to/test.wasm", - }, - }), - - "utf-8" - ); writeWorkerSource({ type: "sw" }); + fs.writeFileSync("./my-entire-app-depends-on-this.cfg", "config = value"); fs.writeFileSync( - "./path/to/and/the/path/to/test.wasm", - "SOME WASM CONTENT" + "./the-entirety-of-human-knowledge.txt", + "Everything's bigger in Texas" ); - mockUploadWorkerRequest({ - expectedType: "sw", - expectedModules: { TESTWASMNAME: "SOME WASM CONTENT" }, - expectedBindings: [ - { name: "TESTWASMNAME", part: "TESTWASMNAME", type: "wasm_module" }, - ], - expectedCompatibilityDate: "2022-01-12", - }); - mockSubDomainRequest(); - await runWrangler("publish index.js --config ./path/to/wrangler.toml"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); + fs.writeFileSync("./some_wasm.wasm", "some wasm"); + fs.writeFileSync("./more_wasm.wasm", "more wasm"); - it("should be able to import .wasm modules from service-worker format workers", async () => { - writeWranglerToml(); - fs.writeFileSync("./index.js", "import TESTWASMNAME from './test.wasm';"); - fs.writeFileSync("./test.wasm", "SOME WASM CONTENT"); mockUploadWorkerRequest({ expectedType: "sw", - expectedModules: { - __94b240d0d692281e6467aa42043986e5c7eea034_test_wasm: - "SOME WASM CONTENT", - }, expectedBindings: [ { - name: "__94b240d0d692281e6467aa42043986e5c7eea034_test_wasm", - part: "__94b240d0d692281e6467aa42043986e5c7eea034_test_wasm", + name: "KV_NAMESPACE_ONE", + namespace_id: "kv-ns-one-id", + type: "kv_namespace", + }, + { + name: "KV_NAMESPACE_TWO", + namespace_id: "kv-ns-two-id", + type: "kv_namespace", + }, + { + class_name: "SomeDurableObject", + name: "DURABLE_OBJECT_ONE", + script_name: "some-durable-object-worker", + type: "durable_object_namespace", + }, + { + class_name: "AnotherDurableObject", + name: "DURABLE_OBJECT_TWO", + script_name: "another-durable-object-worker", + type: "durable_object_namespace", + }, + { + bucket_name: "r2-bucket-one-name", + name: "R2_BUCKET_ONE", + type: "r2_bucket", + }, + { + bucket_name: "r2-bucket-two-name", + name: "R2_BUCKET_TWO", + type: "r2_bucket", + }, + { json: 123, name: "ENV_VAR_ONE", type: "json" }, + { + name: "ENV_VAR_TWO", + text: "Hello, I'm an environment variable", + type: "plain_text", + }, + { + name: "WASM_MODULE_ONE", + part: "WASM_MODULE_ONE", + type: "wasm_module", + }, + { + name: "WASM_MODULE_TWO", + part: "WASM_MODULE_TWO", type: "wasm_module", }, + { name: "TEXT_BLOB_ONE", part: "TEXT_BLOB_ONE", type: "text_blob" }, + { name: "TEXT_BLOB_TWO", part: "TEXT_BLOB_TWO", type: "text_blob" }, + { + data: { some: { unsafe: "thing" } }, + name: "UNSAFE_BINDING_ONE", + type: "some unsafe thing", + }, + { + data: 1337, + name: "UNSAFE_BINDING_TWO", + type: "another unsafe thing", + }, ], }); mockSubDomainRequest(); - await runWrangler("publish index.js"); + + await expect(runWrangler("publish index.js")).resolves.toBeUndefined(); expect(std.out).toMatchInlineSnapshot(` "Uploaded test-name (TIMINGS) Published test-name (TIMINGS) test-name.test-sub-domain.workers.dev" `); expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time." + `); }); - }); - describe("[text_blobs]", () => { - it("should be able to define text blobs for service-worker format workers", async () => { + it("should error when bindings of different types have the same name", async () => { writeWranglerToml({ - text_blobs: { - TESTTEXTBLOBNAME: "./path/to/text.file", + durable_objects: { + bindings: [ + { + name: "CONFLICTING_NAME_ONE", + class_name: "SomeDurableObject", + script_name: "some-durable-object-worker", + }, + { + name: "CONFLICTING_NAME_TWO", + class_name: "AnotherDurableObject", + script_name: "another-durable-object-worker", + }, + ], }, - }); - writeWorkerSource({ type: "sw" }); - fs.mkdirSync("./path/to", { recursive: true }); - fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT"); - mockUploadWorkerRequest({ - expectedType: "sw", - expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" }, - expectedBindings: [ + kv_namespaces: [ + { binding: "CONFLICTING_NAME_ONE", id: "kv-ns-one-id" }, + { binding: "CONFLICTING_NAME_TWO", id: "kv-ns-two-id" }, + ], + r2_buckets: [ { - name: "TESTTEXTBLOBNAME", - part: "TESTTEXTBLOBNAME", - type: "text_blob", + binding: "CONFLICTING_NAME_ONE", + bucket_name: "r2-bucket-one-name", + }, + { + binding: "CONFLICTING_NAME_THREE", + bucket_name: "r2-bucket-two-name", }, ], - }); - mockSubDomainRequest(); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); - - it("should error when defining text blobs for modules format workers", async () => { - writeWranglerToml({ text_blobs: { - TESTTEXTBLOBNAME: "./path/to/text.file", + CONFLICTING_NAME_THREE: "./my-entire-app-depends-on-this.cfg", + CONFLICTING_NAME_FOUR: "./the-entirety-of-human-knowledge.txt", + }, + unsafe: { + bindings: [ + { + name: "CONFLICTING_NAME_THREE", + type: "some unsafe thing", + data: { some: { unsafe: "thing" } }, + }, + { + name: "CONFLICTING_NAME_FOUR", + type: "another unsafe thing", + data: 1337, + }, + ], + }, + vars: { + ENV_VAR_ONE: 123, + CONFLICTING_NAME_THREE: "Hello, I'm an environment variable", + }, + wasm_modules: { + WASM_MODULE_ONE: "./some_wasm.wasm", + CONFLICTING_NAME_THREE: "./more_wasm.wasm", }, }); - writeWorkerSource({ type: "esm" }); - fs.mkdirSync("./path/to", { recursive: true }); - fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT"); - await expect( - runWrangler("publish index.js") - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml"` + writeWorkerSource({ type: "sw" }); + fs.writeFileSync("./my-entire-app-depends-on-this.cfg", "config = value"); + fs.writeFileSync( + "./the-entirety-of-human-knowledge.txt", + "Everything's bigger in Texas" ); + fs.writeFileSync("./some_wasm.wasm", "some wasm"); + fs.writeFileSync("./more_wasm.wasm", "more wasm"); + + await expect(runWrangler("publish index.js")).rejects + .toMatchInlineSnapshot(` + [Error: Processing wrangler.toml configuration: + - CONFLICTING_NAME_ONE assigned to Durable Object, KV Namespace, and R2 Bucket bindings. + - CONFLICTING_NAME_TWO assigned to Durable Object and KV Namespace bindings. + - CONFLICTING_NAME_THREE assigned to R2 Bucket, Text Blob, Unsafe, Environment Variable, and WASM Module bindings. + - CONFLICTING_NAME_FOUR assigned to Text Blob and Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names.] + `); expect(std.out).toMatchInlineSnapshot(`""`); expect(std.err).toMatchInlineSnapshot(` - "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml + "Processing wrangler.toml configuration: + - CONFLICTING_NAME_ONE assigned to Durable Object, KV Namespace, and R2 Bucket bindings. + - CONFLICTING_NAME_TWO assigned to Durable Object and KV Namespace bindings. + - CONFLICTING_NAME_THREE assigned to R2 Bucket, Text Blob, Unsafe, Environment Variable, and WASM Module bindings. + - CONFLICTING_NAME_FOUR assigned to Text Blob and Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names. %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." `); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); - - it("should resolve text blobs relative to the wrangler.toml file", async () => { - fs.mkdirSync("./path/to/and/the/path/to/", { recursive: true }); - fs.writeFileSync( - "./path/to/wrangler.toml", - TOML.stringify({ - compatibility_date: "2022-01-12", - name: "test-name", - text_blobs: { - TESTTEXTBLOBNAME: "./and/the/path/to/text.file", - }, - }), - - "utf-8" - ); - - writeWorkerSource({ type: "sw" }); - fs.writeFileSync( - "./path/to/and/the/path/to/text.file", - "SOME TEXT CONTENT" - ); - mockUploadWorkerRequest({ - expectedType: "sw", - expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" }, - expectedBindings: [ - { - name: "TESTTEXTBLOBNAME", - part: "TESTTEXTBLOBNAME", - type: "text_blob", - }, - ], - expectedCompatibilityDate: "2022-01-12", - }); - mockSubDomainRequest(); - await runWrangler("publish index.js --config ./path/to/wrangler.toml"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time." `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); }); - }); - describe("vars bindings", () => { - it("should support json bindings", async () => { + it("should error when bindings of the same type have the same name", async () => { writeWranglerToml({ - vars: { - text: "plain ol' string", - count: 1, - complex: { enabled: true, id: 123 }, + durable_objects: { + bindings: [ + { + name: "CONFLICTING_DURABLE_OBJECT_NAME", + class_name: "SomeDurableObject", + script_name: "some-durable-object-worker", + }, + { + name: "CONFLICTING_DURABLE_OBJECT_NAME", + class_name: "AnotherDurableObject", + script_name: "another-durable-object-worker", + }, + ], }, - }); - writeWorkerSource(); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedBindings: [ - { name: "text", type: "plain_text", text: "plain ol' string" }, - { name: "count", type: "json", json: 1 }, + kv_namespaces: [ + { binding: "CONFLICTING_KV_NAMESPACE_NAME", id: "kv-ns-one-id" }, + { binding: "CONFLICTING_KV_NAMESPACE_NAME", id: "kv-ns-two-id" }, + ], + r2_buckets: [ { - name: "complex", - type: "json", - json: { enabled: true, id: 123 }, + binding: "CONFLICTING_R2_BUCKET_NAME", + bucket_name: "r2-bucket-one-name", + }, + { + binding: "CONFLICTING_R2_BUCKET_NAME", + bucket_name: "r2-bucket-two-name", }, ], + unsafe: { + bindings: [ + { + name: "CONFLICTING_UNSAFE_NAME", + type: "some unsafe thing", + data: { some: { unsafe: "thing" } }, + }, + { + name: "CONFLICTING_UNSAFE_NAME", + type: "another unsafe thing", + data: 1337, + }, + ], + }, + // text_blobs, vars, and wasm_modules are fine because they're object literals, + // and by definition cannot have two keys of the same name + // + // text_blobs: { + // CONFLICTING_TEXT_BLOB_NAME: "./my-entire-app-depends-on-this.cfg", + // CONFLICTING_TEXT_BLOB_NAME: "./the-entirety-of-human-knowledge.txt", + // }, + // vars: { + // CONFLICTING_VARS_NAME: 123, + // CONFLICTING_VARS_NAME: "Hello, I'm an environment variable", + // }, + // wasm_modules: { + // CONFLICTING_WASM_MODULE_NAME: "./some_wasm.wasm", + // CONFLICTING_WASM_MODULE_NAME: "./more_wasm.wasm", + // }, }); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); - }); + writeWorkerSource({ type: "sw" }); - describe("r2 bucket bindings", () => { - it("should support r2 bucket bindings", async () => { - writeWranglerToml({ - r2_buckets: [{ binding: "FOO", bucket_name: "foo-bucket" }], - }); - writeWorkerSource(); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedBindings: [ - { bucket_name: "foo-bucket", name: "FOO", type: "r2_bucket" }, - ], - }); + await expect(runWrangler("publish index.js")).rejects + .toMatchInlineSnapshot(` + [Error: Processing wrangler.toml configuration: + - CONFLICTING_DURABLE_OBJECT_NAME assigned to multiple Durable Object bindings. + - CONFLICTING_KV_NAMESPACE_NAME assigned to multiple KV Namespace bindings. + - CONFLICTING_R2_BUCKET_NAME assigned to multiple R2 Bucket bindings. + - CONFLICTING_UNSAFE_NAME assigned to multiple Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names.] + `); + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - CONFLICTING_DURABLE_OBJECT_NAME assigned to multiple Durable Object bindings. + - CONFLICTING_KV_NAMESPACE_NAME assigned to multiple KV Namespace bindings. + - CONFLICTING_R2_BUCKET_NAME assigned to multiple R2 Bucket bindings. + - CONFLICTING_UNSAFE_NAME assigned to multiple Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names. - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" + %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." + `); + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time." `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); }); - }); - describe("durable object bindings", () => { - it("should support durable object bindings", async () => { + it("should error correctly when bindings of the same and different types use the same name", async () => { writeWranglerToml({ durable_objects: { bindings: [ - { name: "EXAMPLE_DO_BINDING", class_name: "ExampleDurableObject" }, - ], + { + name: "CONFLICTING_DURABLE_OBJECT_NAME", + class_name: "SomeDurableObject", + script_name: "some-durable-object-worker", + }, + { + name: "CONFLICTING_DURABLE_OBJECT_NAME", + class_name: "AnotherDurableObject", + script_name: "another-durable-object-worker", + }, + ], }, - }); - writeWorkerSource({ type: "esm" }); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedBindings: [ + kv_namespaces: [ { - class_name: "ExampleDurableObject", - name: "EXAMPLE_DO_BINDING", - type: "durable_object_namespace", + binding: "CONFLICTING_KV_NAMESPACE_NAME", + id: "kv-ns-one-id", + }, + { + binding: "CONFLICTING_KV_NAMESPACE_NAME", + id: "kv-ns-two-id", + }, + { binding: "CONFLICTING_NAME_ONE", id: "kv-ns-three-id" }, + { binding: "CONFLICTING_NAME_TWO", id: "kv-ns-four-id" }, + ], + r2_buckets: [ + { + binding: "CONFLICTING_R2_BUCKET_NAME", + bucket_name: "r2-bucket-one-name", + }, + { + binding: "CONFLICTING_R2_BUCKET_NAME", + bucket_name: "r2-bucket-two-name", + }, + { + binding: "CONFLICTING_NAME_THREE", + bucket_name: "r2-bucket-three-name", + }, + { + binding: "CONFLICTING_NAME_FOUR", + bucket_name: "r2-bucket-four-name", }, ], + text_blobs: { + CONFLICTING_NAME_THREE: "./my-entire-app-depends-on-this.cfg", + CONFLICTING_NAME_FOUR: "./the-entirety-of-human-knowledge.txt", + }, + unsafe: { + bindings: [ + { + name: "CONFLICTING_UNSAFE_NAME", + type: "some unsafe thing", + data: { some: { unsafe: "thing" } }, + }, + { + name: "CONFLICTING_UNSAFE_NAME", + type: "another unsafe thing", + data: 1337, + }, + { + name: "CONFLICTING_NAME_THREE", + type: "yet another unsafe thing", + data: "how is a string unsafe?", + }, + { + name: "CONFLICTING_NAME_FOUR", + type: "a fourth unsafe thing", + data: null, + }, + ], + }, + vars: { + ENV_VAR_ONE: 123, + CONFLICTING_NAME_THREE: "Hello, I'm an environment variable", + }, + wasm_modules: { + WASM_MODULE_ONE: "./some_wasm.wasm", + CONFLICTING_NAME_THREE: "./more_wasm.wasm", + }, }); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" + writeWorkerSource({ type: "sw" }); + fs.writeFileSync("./my-entire-app-depends-on-this.cfg", "config = value"); + fs.writeFileSync( + "./the-entirety-of-human-knowledge.txt", + "Everything's bigger in Texas" + ); + fs.writeFileSync("./some_wasm.wasm", "some wasm"); + fs.writeFileSync("./more_wasm.wasm", "more wasm"); + + await expect(runWrangler("publish index.js")).rejects + .toMatchInlineSnapshot(` + [Error: Processing wrangler.toml configuration: + - CONFLICTING_DURABLE_OBJECT_NAME assigned to multiple Durable Object bindings. + - CONFLICTING_KV_NAMESPACE_NAME assigned to multiple KV Namespace bindings. + - CONFLICTING_R2_BUCKET_NAME assigned to multiple R2 Bucket bindings. + - CONFLICTING_NAME_THREE assigned to R2 Bucket, Text Blob, Unsafe, Environment Variable, and WASM Module bindings. + - CONFLICTING_NAME_FOUR assigned to R2 Bucket, Text Blob, and Unsafe bindings. + - CONFLICTING_UNSAFE_NAME assigned to multiple Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names.] + `); + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - CONFLICTING_DURABLE_OBJECT_NAME assigned to multiple Durable Object bindings. + - CONFLICTING_KV_NAMESPACE_NAME assigned to multiple KV Namespace bindings. + - CONFLICTING_R2_BUCKET_NAME assigned to multiple R2 Bucket bindings. + - CONFLICTING_NAME_THREE assigned to R2 Bucket, Text Blob, Unsafe, Environment Variable, and WASM Module bindings. + - CONFLICTING_NAME_FOUR assigned to R2 Bucket, Text Blob, and Unsafe bindings. + - CONFLICTING_UNSAFE_NAME assigned to multiple Unsafe bindings. + - Bindings must have unique names, so that they can all be referenced in the worker. + Please change your bindings to have unique names. + + %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." + `); + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time." `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); }); - it("should support service-workers binding to external durable objects", async () => { - writeWranglerToml({ - durable_objects: { - bindings: [ + describe("[wasm_modules]", () => { + it("should be able to define wasm modules for service-worker format workers", async () => { + writeWranglerToml({ + wasm_modules: { + TESTWASMNAME: "./path/to/test.wasm", + }, + }); + writeWorkerSource({ type: "sw" }); + fs.mkdirSync("./path/to", { recursive: true }); + fs.writeFileSync("./path/to/test.wasm", "SOME WASM CONTENT"); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedModules: { TESTWASMNAME: "SOME WASM CONTENT" }, + expectedBindings: [ + { name: "TESTWASMNAME", part: "TESTWASMNAME", type: "wasm_module" }, + ], + }); + mockSubDomainRequest(); + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should error when defining wasm modules for modules format workers", async () => { + writeWranglerToml({ + wasm_modules: { + TESTWASMNAME: "./path/to/test.wasm", + }, + }); + writeWorkerSource({ type: "esm" }); + fs.mkdirSync("./path/to", { recursive: true }); + fs.writeFileSync("./path/to/test.wasm", "SOME WASM CONTENT"); + + await expect( + runWrangler("publish index.js") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"` + ); + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` + "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code + + %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." + `); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should resolve wasm modules relative to the wrangler.toml file", async () => { + fs.mkdirSync("./path/to/and/the/path/to/", { recursive: true }); + fs.writeFileSync( + "./path/to/wrangler.toml", + TOML.stringify({ + compatibility_date: "2022-01-12", + name: "test-name", + wasm_modules: { + TESTWASMNAME: "./and/the/path/to/test.wasm", + }, + }), + + "utf-8" + ); + + writeWorkerSource({ type: "sw" }); + fs.writeFileSync( + "./path/to/and/the/path/to/test.wasm", + "SOME WASM CONTENT" + ); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedModules: { TESTWASMNAME: "SOME WASM CONTENT" }, + expectedBindings: [ + { name: "TESTWASMNAME", part: "TESTWASMNAME", type: "wasm_module" }, + ], + expectedCompatibilityDate: "2022-01-12", + }); + mockSubDomainRequest(); + await runWrangler("publish index.js --config ./path/to/wrangler.toml"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should be able to import .wasm modules from service-worker format workers", async () => { + writeWranglerToml(); + fs.writeFileSync( + "./index.js", + "import TESTWASMNAME from './test.wasm';" + ); + fs.writeFileSync("./test.wasm", "SOME WASM CONTENT"); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedModules: { + __94b240d0d692281e6467aa42043986e5c7eea034_test_wasm: + "SOME WASM CONTENT", + }, + expectedBindings: [ { - name: "EXAMPLE_DO_BINDING", - class_name: "ExampleDurableObject", - script_name: "example-do-binding-worker", + name: "__94b240d0d692281e6467aa42043986e5c7eea034_test_wasm", + part: "__94b240d0d692281e6467aa42043986e5c7eea034_test_wasm", + type: "wasm_module", }, ], - }, + }); + mockSubDomainRequest(); + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); }); - writeWorkerSource({ type: "sw" }); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedType: "sw", - expectedBindings: [ - { - name: "EXAMPLE_DO_BINDING", - class_name: "ExampleDurableObject", - script_name: "example-do-binding-worker", - type: "durable_object_namespace", + }); + + describe("[text_blobs]", () => { + it("should be able to define text blobs for service-worker format workers", async () => { + writeWranglerToml({ + text_blobs: { + TESTTEXTBLOBNAME: "./path/to/text.file", }, - ], + }); + writeWorkerSource({ type: "sw" }); + fs.mkdirSync("./path/to", { recursive: true }); + fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT"); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" }, + expectedBindings: [ + { + name: "TESTTEXTBLOBNAME", + part: "TESTTEXTBLOBNAME", + type: "text_blob", + }, + ], + }); + mockSubDomainRequest(); + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); }); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); - }); + it("should error when defining text blobs for modules format workers", async () => { + writeWranglerToml({ + text_blobs: { + TESTTEXTBLOBNAME: "./path/to/text.file", + }, + }); + writeWorkerSource({ type: "esm" }); + fs.mkdirSync("./path/to", { recursive: true }); + fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT"); + + await expect( + runWrangler("publish index.js") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml"` + ); + expect(std.out).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(` + "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml - it("should support module workers implementing durable objects", async () => { - writeWranglerToml({ - durable_objects: { - bindings: [ + %s If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new." + `); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should resolve text blobs relative to the wrangler.toml file", async () => { + fs.mkdirSync("./path/to/and/the/path/to/", { recursive: true }); + fs.writeFileSync( + "./path/to/wrangler.toml", + TOML.stringify({ + compatibility_date: "2022-01-12", + name: "test-name", + text_blobs: { + TESTTEXTBLOBNAME: "./and/the/path/to/text.file", + }, + }), + + "utf-8" + ); + + writeWorkerSource({ type: "sw" }); + fs.writeFileSync( + "./path/to/and/the/path/to/text.file", + "SOME TEXT CONTENT" + ); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" }, + expectedBindings: [ { - name: "EXAMPLE_DO_BINDING", - class_name: "ExampleDurableObject", + name: "TESTTEXTBLOBNAME", + part: "TESTTEXTBLOBNAME", + type: "text_blob", }, ], - }, + expectedCompatibilityDate: "2022-01-12", + }); + mockSubDomainRequest(); + await runWrangler("publish index.js --config ./path/to/wrangler.toml"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); }); - writeWorkerSource({ type: "esm" }); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedType: "esm", - expectedBindings: [ - { - name: "EXAMPLE_DO_BINDING", - class_name: "ExampleDurableObject", - type: "durable_object_namespace", + }); + + describe("[vars]", () => { + it("should support json bindings", async () => { + writeWranglerToml({ + vars: { + text: "plain ol' string", + count: 1, + complex: { enabled: true, id: 123 }, }, - ], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { name: "text", type: "plain_text", text: "plain ol' string" }, + { name: "count", type: "json", json: 1 }, + { + name: "complex", + type: "json", + json: { enabled: true, id: 123 }, + }, + ], + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); }); + }); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(`""`); + describe("[r2_buckets]", () => { + it("should support r2 bucket bindings", async () => { + writeWranglerToml({ + r2_buckets: [{ binding: "FOO", bucket_name: "foo-bucket" }], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { bucket_name: "foo-bucket", name: "FOO", type: "r2_bucket" }, + ], + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); }); - it("should error when detecting service workers implementing durable objects", async () => { - writeWranglerToml({ - durable_objects: { - bindings: [ + describe("[durable_objects]", () => { + it("should support durable object bindings", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { + name: "EXAMPLE_DO_BINDING", + class_name: "ExampleDurableObject", + }, + ], + }, + }); + writeWorkerSource({ type: "esm" }); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + class_name: "ExampleDurableObject", + name: "EXAMPLE_DO_BINDING", + type: "durable_object_namespace", + }, + ], + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should support service-workers binding to external durable objects", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { + name: "EXAMPLE_DO_BINDING", + class_name: "ExampleDurableObject", + script_name: "example-do-binding-worker", + }, + ], + }, + }); + writeWorkerSource({ type: "sw" }); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedType: "sw", + expectedBindings: [ { name: "EXAMPLE_DO_BINDING", class_name: "ExampleDurableObject", + script_name: "example-do-binding-worker", + type: "durable_object_namespace", }, ], - }, + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); }); - writeWorkerSource({ type: "sw" }); - mockSubDomainRequest(); - await expect(runWrangler("publish index.js")).rejects - .toThrowErrorMatchingInlineSnapshot(` + it("should support module workers implementing durable objects", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { + name: "EXAMPLE_DO_BINDING", + class_name: "ExampleDurableObject", + }, + ], + }, + }); + writeWorkerSource({ type: "esm" }); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedType: "esm", + expectedBindings: [ + { + name: "EXAMPLE_DO_BINDING", + class_name: "ExampleDurableObject", + type: "durable_object_namespace", + }, + ], + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should error when detecting service workers implementing durable objects", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { + name: "EXAMPLE_DO_BINDING", + class_name: "ExampleDurableObject", + }, + ], + }, + }); + writeWorkerSource({ type: "sw" }); + mockSubDomainRequest(); + + await expect(runWrangler("publish index.js")).rejects + .toThrowErrorMatchingInlineSnapshot(` "You seem to be trying to use Durable Objects in a Worker written with Service Worker syntax. You can use Durable Objects defined in other Workers by specifying a \`script_name\` in your wrangler.toml, where \`script_name\` is the name of the Worker that implements that Durable Object. For example: { name = EXAMPLE_DO_BINDING, class_name = ExampleDurableObject } ==> { name = EXAMPLE_DO_BINDING, class_name = ExampleDurableObject, script_name = example-do-binding-worker } Alternatively, migrate your worker to ES Module syntax to implement a Durable Object in this Worker: https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/" `); + }); }); - }); - describe("unsafe bindings", () => { - it("should warn if using unsafe bindings", async () => { - writeWranglerToml({ - unsafe: { - bindings: [ + describe("[unsafe]", () => { + it("should warn if using unsafe bindings", async () => { + writeWranglerToml({ + unsafe: { + bindings: [ + { + name: "my-binding", + type: "binding-type", + param: "binding-param", + }, + ], + }, + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ { name: "my-binding", type: "binding-type", param: "binding-param", }, ], - }, + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time." + `); }); - writeWorkerSource(); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedBindings: [ - { - name: "my-binding", - type: "binding-type", - param: "binding-param", + it("should warn if using unsafe bindings already handled by wrangler", async () => { + writeWranglerToml({ + unsafe: { + bindings: [ + { + name: "my-binding", + type: "plain_text", + text: "text", + }, + ], }, - ], - }); - - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(` - "Processing wrangler.toml configuration: - - \\"unsafe\\" fields are experimental and may change or break at any time." - `); - }); - it("should warn if using unsafe bindings already handled by wrangler", async () => { - writeWranglerToml({ - unsafe: { - bindings: [ + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ { name: "my-binding", type: "plain_text", text: "text", }, ], - }, - }); - writeWorkerSource(); - mockSubDomainRequest(); - mockUploadWorkerRequest({ - expectedBindings: [ - { - name: "my-binding", - type: "plain_text", - text: "text", - }, - ], - }); + }); - await runWrangler("publish index.js"); - expect(std.out).toMatchInlineSnapshot(` - "Uploaded test-name (TIMINGS) - Published test-name (TIMINGS) - test-name.test-sub-domain.workers.dev" - `); - expect(std.err).toMatchInlineSnapshot(`""`); - expect(std.warn).toMatchInlineSnapshot(` - "Processing wrangler.toml configuration: - - \\"unsafe\\" fields are experimental and may change or break at any time. - - \\"unsafe.bindings[0]\\": {\\"name\\":\\"my-binding\\",\\"type\\":\\"plain_text\\",\\"text\\":\\"text\\"} - - The binding type \\"plain_text\\" is directly supported by wrangler. - Consider migrating this unsafe binding to a format for 'plain_text' bindings that is supported by wrangler for optimal support. - For more details, see https://developers.cloudflare.com/workers/cli-wrangler/configuration" - `); + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(` + "Processing wrangler.toml configuration: + - \\"unsafe\\" fields are experimental and may change or break at any time. + - \\"unsafe.bindings[0]\\": {\\"name\\":\\"my-binding\\",\\"type\\":\\"plain_text\\",\\"text\\":\\"text\\"} + - The binding type \\"plain_text\\" is directly supported by wrangler. + Consider migrating this unsafe binding to a format for 'plain_text' bindings that is supported by wrangler for optimal support. + For more details, see https://developers.cloudflare.com/workers/cli-wrangler/configuration" + `); + }); }); }); diff --git a/packages/wrangler/src/config/validation-helpers.ts b/packages/wrangler/src/config/validation-helpers.ts index 06c5a690225e..8b890a81c933 100644 --- a/packages/wrangler/src/config/validation-helpers.ts +++ b/packages/wrangler/src/config/validation-helpers.ts @@ -502,3 +502,53 @@ export const validateAdditionalProperties = ( } return true; }; + +/** + * Get the names of the bindings collection in `value`. + * + * Will return an empty array if it doesn't understand the value + * passed in, so another form of validation should be + * performed externally. + */ +export const getBindingNames = (value: unknown): string[] => { + if (typeof value !== "object" || value === null) { + return []; + } + + if (isBindingList(value)) { + return value.bindings.map(({ name }) => name); + } else if (isNamespaceList(value)) { + return value.map(({ binding }) => binding); + } else if (isRecord(value)) { + return Object.keys(value); + } else { + return []; + } +}; + +const isBindingList = ( + value: unknown +): value is { + bindings: { + name: string; + }[]; +} => + isRecord(value) && + "bindings" in value && + Array.isArray(value.bindings) && + value.bindings.every( + (binding) => + isRecord(binding) && "name" in binding && typeof binding.name === "string" + ); + +const isNamespaceList = (value: unknown): value is { binding: string }[] => + Array.isArray(value) && + value.every( + (entry) => + isRecord(entry) && "binding" in entry && typeof entry.binding === "string" + ); + +const isRecord = ( + value: unknown +): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 337863eebab2..000df6e8173a 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -23,6 +23,7 @@ import { isMutuallyExclusiveWith, inheritableInLegacyEnvironments, appendEnvName, + getBindingNames, } from "./validation-helpers"; import type { Config, DevConfig, RawConfig, RawDevConfig } from "./config"; import type { @@ -33,6 +34,8 @@ import type { } from "./environment"; import type { ValidatorFn } from "./validation-helpers"; +const ENGLISH = new Intl.ListFormat("en"); + /** * Validate the given `rawConfig` object that was loaded from `configPath`. * @@ -167,6 +170,8 @@ export function normalizeAndValidateConfig( ), }; + validateBindingsHaveUniqueNames(diagnostics, config); + validateAdditionalProperties( diagnostics, "top-level", @@ -882,14 +887,6 @@ const validateBindingsProperty = return isValid; }; -/** - * Get the names of the bindings collection in `value`. - */ -const getBindingNames = (value: unknown): string[] => - ((value as { bindings: { name: string }[] })?.bindings ?? []).map( - (binding) => binding.name - ); - /** * Check that the given field is a valid "durable_object" binding object. */ @@ -1094,3 +1091,99 @@ const validateR2Binding: ValidatorFn = (diagnostics, field, value) => { } return isValid; }; + +/** + * Check that bindings whose names might conflict, don't. + * + * We don't want to have, for example, a KV namespace named "DATA" + * and a Durable Object also named "DATA". Then it would be ambiguous + * what exactly would live at `env.DATA` (or in the case of service workers, + * the `DATA` global). + */ +const validateBindingsHaveUniqueNames = ( + diagnostics: Diagnostics, + { + durable_objects, + kv_namespaces, + r2_buckets, + text_blobs, + unsafe, + vars, + wasm_modules, + }: Partial +): boolean => { + let hasDuplicates = false; + + const bindingsGroupedByType = { + "Durable Object": getBindingNames(durable_objects), + "KV Namespace": getBindingNames(kv_namespaces), + "R2 Bucket": getBindingNames(r2_buckets), + "Text Blob": getBindingNames(text_blobs), + Unsafe: getBindingNames(unsafe), + "Environment Variable": getBindingNames(vars), + "WASM Module": getBindingNames(wasm_modules), + } as Record; + + const bindingsGroupedByName: Record = {}; + + for (const bindingType in bindingsGroupedByType) { + const bindingNames = bindingsGroupedByType[bindingType]; + + for (const bindingName of bindingNames) { + if (!(bindingName in bindingsGroupedByName)) { + bindingsGroupedByName[bindingName] = []; + } + + bindingsGroupedByName[bindingName].push(bindingType); + } + } + + for (const bindingName in bindingsGroupedByName) { + const bindingTypes = bindingsGroupedByName[bindingName]; + if (bindingTypes.length < 2) { + // there's only one (or zero) binding(s) with this name, which is fine, actually + continue; + } + + hasDuplicates = true; + + // there's two types of duplicates we want to look for: + // - bindings with the same name of the same type (e.g. two Durable Objects both named "OBJ") + // - bindings with the same name of different types (a KV namespace and DO both named "DATA") + + const sameType = bindingTypes + // filter once to find duplicate binding types + .filter((type, i) => bindingTypes.indexOf(type) !== i) + // filter twice to only get _unique_ duplicate binding types + .filter( + (type, i, duplicateBindingTypes) => + duplicateBindingTypes.indexOf(type) === i + ); + + const differentTypes = bindingTypes.filter( + (type, i) => bindingTypes.indexOf(type) === i + ); + + if (differentTypes.length > 1) { + // we have multiple different types using the same name + diagnostics.errors.push( + `${bindingName} assigned to ${ENGLISH.format(differentTypes)} bindings.` + ); + } + + sameType.forEach((bindingType) => { + diagnostics.errors.push( + `${bindingName} assigned to multiple ${bindingType} bindings.` + ); + }); + } + + if (hasDuplicates) { + const problem = + "Bindings must have unique names, so that they can all be referenced in the worker."; + const resolution = "Please change your bindings to have unique names."; + diagnostics.errors.push(`${problem}\n${resolution}`); + } + + return !hasDuplicates; +}; diff --git a/packages/wrangler/src/intl-polyfill.d.ts b/packages/wrangler/src/intl-polyfill.d.ts new file mode 100644 index 000000000000..fbba31017f74 --- /dev/null +++ b/packages/wrangler/src/intl-polyfill.d.ts @@ -0,0 +1,139 @@ +/** + * TODO Remove once https://github.com/microsoft/TypeScript/pull/47254 lands + */ +declare namespace Intl { + interface DateTimeFormatOptions { + formatMatcher?: "basic" | "best fit" | "best fit" | undefined; + dateStyle?: "full" | "long" | "medium" | "short" | undefined; + timeStyle?: "full" | "long" | "medium" | "short" | undefined; + dayPeriod?: "narrow" | "short" | "long" | undefined; + fractionalSecondDigits?: 0 | 1 | 2 | 3 | undefined; + } + + interface ResolvedDateTimeFormatOptions { + formatMatcher?: "basic" | "best fit" | "best fit"; + dateStyle?: "full" | "long" | "medium" | "short"; + timeStyle?: "full" | "long" | "medium" | "short"; + hourCycle?: "h11" | "h12" | "h23" | "h24"; + dayPeriod?: "narrow" | "short" | "long"; + fractionalSecondDigits?: 0 | 1 | 2 | 3; + } + + interface NumberFormat { + formatRange(startDate: number | bigint, endDate: number | bigint): string; + formatRangeToParts( + startDate: number | bigint, + endDate: number | bigint + ): NumberFormatPart[]; + } + + /** + * The locale matching algorithm to use. + * + * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_negotiation). + */ + type ListFormatLocaleMatcher = "lookup" | "best fit"; + + /** + * The format of output message. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#parameters). + */ + type ListFormatType = "conjunction" | "disjunction" | "unit"; + + /** + * The length of the formatted message. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#parameters). + */ + type ListFormatStyle = "long" | "short" | "narrow"; + + /** + * An object with some or all properties of the `Intl.ListFormat` constructor `options` parameter. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#parameters). + */ + interface ListFormatOptions { + /** The locale matching algorithm to use. For information about this option, see [Intl page](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_negotiation). */ + localeMatcher?: ListFormatLocaleMatcher; + /** The format of output message. */ + type?: ListFormatType; + /** The length of the internationalized message. */ + style?: ListFormatStyle; + } + + interface ListFormat { + /** + * Returns a string with a language-specific representation of the list. + * + * @param list - An iterable object, such as an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). + * + * @throws `TypeError` if `list` includes something other than the possible values. + * + * @returns {string} A language-specific formatted string representing the elements of the list. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/format). + */ + format(list: Iterable): string; + + /** + * Returns an Array of objects representing the different components that can be used to format a list of values in a locale-aware fashion. + * + * @param list - An iterable object, such as an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array), to be formatted according to a locale. + * + * @throws `TypeError` if `list` includes something other than the possible values. + * + * @returns {{ type: "element" | "literal", value: string; }[]} An Array of components which contains the formatted parts from the list. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts). + */ + formatToParts( + list: Iterable + ): { type: "element" | "literal"; value: string }[]; + } + + const ListFormat: { + prototype: ListFormat; + + /** + * Creates [Intl.ListFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat) objects that + * enable language-sensitive list formatting. + * + * @param locales - A string with a [BCP 47 language tag](http://tools.ietf.org/html/rfc5646), or an array of such strings. + * For the general form and interpretation of the `locales` argument, + * see the [`Intl` page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation). + * + * @param options - An [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#parameters) + * with some or all options of `ListFormatOptions`. + * + * @returns [Intl.ListFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat) object. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat). + */ + new ( + locales?: BCP47LanguageTag | BCP47LanguageTag[], + options?: ListFormatOptions + ): ListFormat; + + /** + * Returns an array containing those of the provided locales that are + * supported in list formatting without having to fall back to the runtime's default locale. + * + * @param locales - A string with a [BCP 47 language tag](http://tools.ietf.org/html/rfc5646), or an array of such strings. + * For the general form and interpretation of the `locales` argument, + * see the [`Intl` page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation). + * + * @param options - An [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/supportedLocalesOf#parameters). + * with some or all possible options. + * + * @returns An array of strings representing a subset of the given locale tags that are supported in list + * formatting without having to fall back to the runtime's default locale. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/supportedLocalesOf). + */ + supportedLocalesOf( + locales: BCP47LanguageTag | BCP47LanguageTag[], + options?: Pick + ): BCP47LanguageTag[]; + }; +}