diff --git a/.changeset/improve-error-messages-cli-types-auth-dev-compat.md b/.changeset/improve-error-messages-cli-types-auth-dev-compat.md new file mode 100644 index 0000000000..27a666f943 --- /dev/null +++ b/.changeset/improve-error-messages-cli-types-auth-dev-compat.md @@ -0,0 +1,13 @@ +--- +"wrangler": patch +--- + +Improve error messages for CLI flags, type generation, auth scopes, dev server tunnels, and compatibility flags + +Error messages across several areas now name the exact flags or values involved and suggest how to fix the problem: + +- KV commands (`kv key put`, `kv key get`, `kv key delete`): error messages now include `--` prefixes and clear "Missing required option" / "Conflicting options" phrasing instead of the vague "Exactly one of the arguments ... is required". +- `wrangler types --include-env=false --include-runtime=false`: the error now names both flags and explains what each does. +- `wrangler login --scopes`: invalid scopes are individually identified instead of dumping the entire array. +- `wrangler dev --tunnel --remote`: the error now explains why tunnels require local mode and suggests two concrete fixes. +- Conflicting compatibility flags (`nodejs_compat_populate_process_env` / `nodejs_compat_do_not_populate_process_env`, `global_navigator` / `no_global_navigator`): errors now name the specific conflicting flags. diff --git a/.changeset/improve-hyperdrive-errors.md b/.changeset/improve-hyperdrive-errors.md new file mode 100644 index 0000000000..2f1236e134 --- /dev/null +++ b/.changeset/improve-hyperdrive-errors.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Improve Hyperdrive error messages for missing required options + +Error messages thrown when creating or updating a Hyperdrive config with missing individual parameters (e.g. `--origin-host`, `--origin-port`, `--database`, `--origin-user`, `--origin-password`, `--origin-scheme`, `--access-client-id`/`--access-client-secret`) now clearly state which option is missing, provide a usage example, and suggest `--connection-string` as an alternative where applicable. diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index eb120da21a..2ade8057af 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -3077,7 +3077,7 @@ describe.sequential("wrangler dev", () => { await expect( runWrangler("dev --tunnel --remote") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: --tunnel is only supported in local mode.]` + `[Error: --tunnel cannot be used with --remote. Tunnels expose your local dev server to the internet, which is only applicable in local mode. Remove --remote to use --tunnel, or remove --tunnel to use --remote.]` ); }); }); diff --git a/packages/wrangler/src/__tests__/hyperdrive.test.ts b/packages/wrangler/src/__tests__/hyperdrive.test.ts index 9f5b1eb65b..854c839dd4 100644 --- a/packages/wrangler/src/__tests__/hyperdrive.test.ts +++ b/packages/wrangler/src/__tests__/hyperdrive.test.ts @@ -600,7 +600,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide an origin hostname for the database + "X [ERROR] Missing required option --origin-host. Specify the hostname of the origin database, e.g. --origin-host=database.example.com. " `); @@ -861,7 +861,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide both an Access Client ID and Access Client Secret when configuring Hyperdrive-over-Access + "X [ERROR] Missing required option --access-client-id or --access-client-secret. Both --access-client-id and --access-client-secret must be provided together when configuring Hyperdrive-over-Access. " `); @@ -1437,7 +1437,28 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide a password for the origin database + "X [ERROR] Missing required option --origin-password. Specify the password for the origin database, e.g. --origin-password=mypassword. Alternatively, use --connection-string to provide all origin details at once. + + " + `); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + " + `); + }); + + it("should throw an exception when creating a hyperdrive config without network origin options", async ({ + expect, + }) => { + await expect(() => + runWrangler( + "hyperdrive create test123 --database=mydb --origin-user=newuser --origin-password=mypassword" + ) + ).rejects.toThrow(); + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] Missing required network origin options. Provide the origin host and port via --origin-host and --origin-port, a Workers VPC Service ID via --service-id, or use --connection-string to provide all origin details at once. " `); @@ -1459,7 +1480,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide an origin hostname for the database + "X [ERROR] Missing required option --origin-host. Specify the hostname of the origin database, e.g. --origin-host=database.example.com. " `); @@ -1735,7 +1756,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide a nonzero origin port for the database + "X [ERROR] Missing required option --origin-port. Specify the port of the origin database, e.g. --origin-port=5432. " `); @@ -1757,7 +1778,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide an origin hostname for the database + "X [ERROR] Missing required option --origin-host. Specify the hostname of the origin database, e.g. --origin-host=database.example.com. " `); @@ -1795,7 +1816,7 @@ describe("hyperdrive commands", () => { ) ).rejects.toThrow(); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] You must provide both an Access Client ID and Access Client Secret when configuring Hyperdrive-over-Access + "X [ERROR] Missing required option --access-client-id or --access-client-secret. Both --access-client-id and --access-client-secret must be provided together when configuring Hyperdrive-over-Access. " `); diff --git a/packages/wrangler/src/__tests__/kv/key.test.ts b/packages/wrangler/src/__tests__/kv/key.test.ts index 34f7cdf2cf..ff37db3384 100644 --- a/packages/wrangler/src/__tests__/kv/key.test.ts +++ b/packages/wrangler/src/__tests__/kv/key.test.ts @@ -441,7 +441,7 @@ describe("kv", () => { await expect( runWrangler("kv key put --remote foo bar") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Exactly one of the arguments binding and namespace-id is required]` + `[Error: Missing required option: exactly one of --binding and --namespace-id must be provided]` ); expect(std.out).toMatchInlineSnapshot(` @@ -479,7 +479,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Exactly one of the arguments binding and namespace-id is required + "X [ERROR] Missing required option: exactly one of --binding and --namespace-id must be provided " `); @@ -493,7 +493,7 @@ describe("kv", () => { "kv key put --remote foo bar --binding x --namespace-id y" ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Arguments binding and namespace-id are mutually exclusive]` + `[Error: Conflicting options: --binding and --namespace-id cannot be used together. Please provide only one.]` ); expect(std.out).toMatchInlineSnapshot(` @@ -531,7 +531,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Arguments binding and namespace-id are mutually exclusive + "X [ERROR] Conflicting options: --binding and --namespace-id cannot be used together. Please provide only one. " `); @@ -543,7 +543,7 @@ describe("kv", () => { await expect( runWrangler("kv key put --remote key --namespace-id 12345") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Exactly one of the arguments value and path is required]` + `[Error: Missing required option: exactly one of and --path must be provided]` ); expect(std.out).toMatchInlineSnapshot(` @@ -581,7 +581,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Exactly one of the arguments value and path is required + "X [ERROR] Missing required option: exactly one of and --path must be provided " `); @@ -642,7 +642,7 @@ describe("kv", () => { "kv key put --remote key value --path xyz --namespace-id 12345" ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Arguments value and path are mutually exclusive]` + `[Error: Conflicting options: and --path cannot be used together. Please provide only one.]` ); expect(std.out).toMatchInlineSnapshot(` @@ -680,7 +680,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Arguments value and path are mutually exclusive + "X [ERROR] Conflicting options: and --path cannot be used together. Please provide only one. " `); @@ -1133,7 +1133,7 @@ describe("kv", () => { await expect( runWrangler("kv key get --remote foo") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Exactly one of the arguments binding and namespace-id is required]` + `[Error: Missing required option: exactly one of --binding and --namespace-id must be provided]` ); expect(std.out).toMatchInlineSnapshot(` " @@ -1163,7 +1163,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Exactly one of the arguments binding and namespace-id is required + "X [ERROR] Missing required option: exactly one of --binding and --namespace-id must be provided " `); @@ -1175,7 +1175,7 @@ describe("kv", () => { await expect( runWrangler("kv key get --remote foo --binding x --namespace-id y") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Arguments binding and namespace-id are mutually exclusive]` + `[Error: Conflicting options: --binding and --namespace-id cannot be used together. Please provide only one.]` ); expect(std.out).toMatchInlineSnapshot(` @@ -1206,7 +1206,7 @@ describe("kv", () => { --persist-to Directory for local persistence [string]" `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Arguments binding and namespace-id are mutually exclusive + "X [ERROR] Conflicting options: --binding and --namespace-id cannot be used together. Please provide only one. " `); diff --git a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts index 0cde409fb4..884ecb7f7c 100644 --- a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -73,7 +73,7 @@ describe("isNavigatorDefined", () => { assert(false, "Unreachable"); } catch (e) { expect(e).toMatchInlineSnapshot( - `[Error: Can't both enable and disable a flag]` + `[Error: Conflicting compatibility flags: "global_navigator" and "no_global_navigator" cannot both be set. Remove one of these flags from your configuration.]` ); } }); diff --git a/packages/wrangler/src/__tests__/process-env-populated.test.ts b/packages/wrangler/src/__tests__/process-env-populated.test.ts index 45142d49e7..c9ae009144 100644 --- a/packages/wrangler/src/__tests__/process-env-populated.test.ts +++ b/packages/wrangler/src/__tests__/process-env-populated.test.ts @@ -77,7 +77,7 @@ describe("isProcessEnvPopulated", () => { assert(false, "Unreachable"); } catch (e) { expect(e).toMatchInlineSnapshot( - `[Error: Can't both enable and disable a flag]` + `[Error: Conflicting compatibility flags: "nodejs_compat_populate_process_env" and "nodejs_compat_do_not_populate_process_env" cannot both be set. Remove one of these flags from your configuration.]` ); } }); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 00aab22f81..da2896228b 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -3413,6 +3413,24 @@ describe("generate types - CLI", () => { ); } }); + + it("should error if both --include-env and --include-runtime are false", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + vars: bindingsConfigMock.vars, + }), + "utf-8" + ); + + await expect( + runWrangler("types --include-env=false --include-runtime=false") + ).rejects.toThrowError( + "At least one of --include-env or --include-runtime must be enabled." + ); + }); }); it("should allow multiple customizations to be applied together", async ({ @@ -3897,7 +3915,7 @@ describe("generate types - API", () => { includeRuntime: false, }) ).rejects.toThrow( - "You cannot run this command without including either Env or Runtime types" + "At least one of includeEnv or includeRuntime must be enabled." ); await expect( diff --git a/packages/wrangler/src/__tests__/user.test.ts b/packages/wrangler/src/__tests__/user.test.ts index 1ef9b1839e..bd903be4e3 100644 --- a/packages/wrangler/src/__tests__/user.test.ts +++ b/packages/wrangler/src/__tests__/user.test.ts @@ -428,6 +428,26 @@ describe("User", () => { ); }); }); + + it("should error if --scopes contains an invalid scope", async ({ + expect, + }) => { + await expect( + runWrangler("login --scopes account:read bogus_scope") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid authentication scope: "bogus_scope". Run "wrangler login --scopes-list" to see all valid scopes.]` + ); + }); + + it("should error if --scopes contains multiple invalid scopes", async ({ + expect, + }) => { + await expect( + runWrangler("login --scopes bad_one bad_two") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid authentication scopes: "bad_one", "bad_two". Run "wrangler login --scopes-list" to see all valid scopes.]` + ); + }); }); it("should handle errors for failed token refresh in a non-interactive environment", async ({ diff --git a/packages/wrangler/src/core/helpers.ts b/packages/wrangler/src/core/helpers.ts index f8f9302d52..caa94299ad 100644 --- a/packages/wrangler/src/core/helpers.ts +++ b/packages/wrangler/src/core/helpers.ts @@ -7,27 +7,59 @@ import type { NamespaceDefinition, } from "./types"; +/** + * Formats an argument name for display in error messages. + * Positional arguments are shown as ``, flags as `--name`. + * + * @param name - The argument name + * @param positionalArgs - Optional set of argument names that are positional (not flags) + * @returns The formatted argument name + */ +function formatArgName( + name: string, + positionalArgs?: ReadonlySet +): string { + return positionalArgs?.has(name) ? `<${name}>` : `--${name}`; +} + +/** Formats a list of items as a human-readable conjunction (e.g. "a, b, and c"). */ +const listFormatter = new Intl.ListFormat("en-US"); + /** * A helper to demand one of a set of options * via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261 + * + * @param options - The option names to demand exactly one of + * @param def - Optional command definition used to distinguish positional arguments + * from flags in error messages (positional args are shown as `` instead of `--name`). + * @returns A validation function that checks the argv object */ -export function demandOneOfOption(...options: string[]) { +export function demandOneOfOption( + options: string[], + def?: { positionalArgs?: ReadonlyArray } +) { + const positionalArgs = def?.positionalArgs + ? new Set(def.positionalArgs) + : undefined; + return function (argv: { [key: string]: unknown }) { const count = options.filter((option) => argv[option]).length; - const lastOption = options.pop(); + const flagList = listFormatter.format( + options.map((o) => formatArgName(o, positionalArgs)) + ); if (count === 0) { throw new CommandLineArgsError( - `Exactly one of the arguments ${options.join( - ", " - )} and ${lastOption} is required`, + `Missing required option: exactly one of ${flagList} must be provided`, { telemetryMessage: "core arguments missing exclusive option" } ); } else if (count > 1) { + const provided = options + .filter((option) => argv[option]) + .map((o) => formatArgName(o, positionalArgs)); + const providedList = listFormatter.format(provided); throw new CommandLineArgsError( - `Arguments ${options.join( - ", " - )} and ${lastOption} are mutually exclusive`, + `Conflicting options: ${providedList} cannot be used together. Please provide only one.`, { telemetryMessage: "core arguments mutually exclusive options" } ); } diff --git a/packages/wrangler/src/core/register-yargs-command.ts b/packages/wrangler/src/core/register-yargs-command.ts index dac9b8ef6c..085d402c8e 100644 --- a/packages/wrangler/src/core/register-yargs-command.ts +++ b/packages/wrangler/src/core/register-yargs-command.ts @@ -178,7 +178,7 @@ function createHandler(def: InternalCommandDefinition, argv: string[]) { } } - await def.validateArgs?.(args); + await def.validateArgs?.(args, def); const shouldPrintResourceLocation = typeof def.behaviour?.printResourceLocation === "function" diff --git a/packages/wrangler/src/core/types.ts b/packages/wrangler/src/core/types.ts index 15473cb0e0..c3f671307b 100644 --- a/packages/wrangler/src/core/types.ts +++ b/packages/wrangler/src/core/types.ts @@ -267,8 +267,14 @@ export type CommandDefinition< * A hook to implement custom validation of the args before the handler is called. * Throw `CommandLineArgsError` with actionable error message if args are invalid. * The return value is ignored. + * + * @param args - The parsed CLI arguments + * @param def - The command definition, useful for passing to helpers like `demandOneOfOption` */ - validateArgs?: (args: HandlerArgs) => void | Promise; + validateArgs?: ( + args: HandlerArgs, + def: CommandDefinition + ) => void | Promise; /** * The implementation of the command which is given camelCase'd args diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index 47c22075d6..ca3fe490de 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -286,9 +286,12 @@ export const dev = createCommand({ ); } if (args.tunnel && args.remote) { - throw new UserError("--tunnel is only supported in local mode.", { - telemetryMessage: "dev command tunnel remote conflict", - }); + throw new UserError( + "--tunnel cannot be used with --remote. Tunnels expose your local dev server to the internet, which is only applicable in local mode. Remove --remote to use --tunnel, or remove --tunnel to use --remote.", + { + telemetryMessage: "dev command tunnel remote conflict", + } + ); } if (isWebContainer()) { diff --git a/packages/wrangler/src/hyperdrive/index.ts b/packages/wrangler/src/hyperdrive/index.ts index ea6e4144ab..289080b30d 100644 --- a/packages/wrangler/src/hyperdrive/index.ts +++ b/packages/wrangler/src/hyperdrive/index.ts @@ -1,3 +1,4 @@ +import assert from "node:assert"; import { UserError } from "@cloudflare/workers-utils"; import { createNamespace } from "../core/create-command"; import { MySqlSslmode, PostgresSslmode } from "./client"; @@ -238,23 +239,26 @@ export function getOriginFromArgs< } if (!allowPartialOrigin) { - if (!args.originScheme) { + // --origin-scheme always has a default value ("postgresql") when + // allowPartialOrigin is false (the create command), so this assertion + // should never fire. It narrows the type for downstream code. + assert(args.originScheme, "Expected --origin-scheme to be set"); + + if (!args.database) { throw new UserError( - "You must specify the database protocol as --origin-scheme - e.g. 'postgresql'", - { telemetryMessage: "hyperdrive origin missing protocol" } + "Missing required option --database. Specify the name of the database on the origin server, e.g. --database=mydb. Alternatively, use --connection-string to provide all origin details at once.", + { + telemetryMessage: "hyperdrive origin missing database", + } ); - } else if (!args.database) { - throw new UserError("You must provide a database name", { - telemetryMessage: "hyperdrive origin missing database", - }); } else if (!args.originUser) { throw new UserError( - "You must provide a username for the origin database", + "Missing required option --origin-user. Specify the username for the origin database, e.g. --origin-user=myuser. Alternatively, use --connection-string to provide all origin details at once.", { telemetryMessage: "hyperdrive origin missing username" } ); } else if (!args.originPassword) { throw new UserError( - "You must provide a password for the origin database", + "Missing required option --origin-password. Specify the password for the origin database, e.g. --origin-password=mypassword. Alternatively, use --connection-string to provide all origin details at once.", { telemetryMessage: "hyperdrive origin missing password" } ); } @@ -277,14 +281,14 @@ export function getOriginFromArgs< } else if (args.accessClientId || args.accessClientSecret) { if (!args.accessClientId || !args.accessClientSecret) { throw new UserError( - "You must provide both an Access Client ID and Access Client Secret when configuring Hyperdrive-over-Access", + "Missing required option --access-client-id or --access-client-secret. Both --access-client-id and --access-client-secret must be provided together when configuring Hyperdrive-over-Access.", { telemetryMessage: "hyperdrive access missing credentials" } ); } if (!args.originHost || args.originHost === "") { throw new UserError( - "You must provide an origin hostname for the database", + "Missing required option --origin-host. Specify the hostname of the origin database, e.g. --origin-host=database.example.com.", { telemetryMessage: "hyperdrive access missing origin host" } ); } @@ -297,14 +301,14 @@ export function getOriginFromArgs< } else if (args.originHost || args.originPort) { if (!args.originHost) { throw new UserError( - "You must provide an origin hostname for the database", + "Missing required option --origin-host. Specify the hostname of the origin database, e.g. --origin-host=database.example.com.", { telemetryMessage: "hyperdrive origin missing host" } ); } if (!args.originPort) { throw new UserError( - "You must provide a nonzero origin port for the database", + "Missing required option --origin-port. Specify the port of the origin database, e.g. --origin-port=5432.", { telemetryMessage: "hyperdrive origin missing port" } ); } @@ -313,6 +317,11 @@ export function getOriginFromArgs< host: args.originHost, port: args.originPort, }; + } else if (!allowPartialOrigin) { + throw new UserError( + "Missing required network origin options. Provide the origin host and port via --origin-host and --origin-port, a Workers VPC Service ID via --service-id, or use --connection-string to provide all origin details at once.", + { telemetryMessage: "hyperdrive origin missing network origin" } + ); } const origin = { diff --git a/packages/wrangler/src/kv/index.ts b/packages/wrangler/src/kv/index.ts index 7a43496b68..e802ca517b 100644 --- a/packages/wrangler/src/kv/index.ts +++ b/packages/wrangler/src/kv/index.ts @@ -499,9 +499,9 @@ export const kvKeyPutCommand = createCommand({ }, ...putCommonArgs, }, - validateArgs(args) { - demandOneOfOption("binding", "namespace-id")(args); - demandOneOfOption("value", "path")(args); + validateArgs(args, def) { + demandOneOfOption(["binding", "namespace-id"])(args); + demandOneOfOption(["value", "path"], def)(args); }, async handler({ key, ttl, expiration, metadata, ...args }) { @@ -615,7 +615,7 @@ export const kvKeyListCommand = createCommand({ }, }, validateArgs(args) { - demandOneOfOption("binding", "namespace-id")(args); + demandOneOfOption(["binding", "namespace-id"])(args); }, async handler({ prefix, ...args }) { @@ -716,7 +716,7 @@ export const kvKeyGetCommand = createCommand({ ...getCommonArgs, }, validateArgs(args) { - demandOneOfOption("binding", "namespace-id")(args); + demandOneOfOption(["binding", "namespace-id"])(args); }, async handler({ key, ...args }) { const localMode = isLocal(args); diff --git a/packages/wrangler/src/navigator-user-agent.ts b/packages/wrangler/src/navigator-user-agent.ts index 8b86af012a..8e8b71592d 100644 --- a/packages/wrangler/src/navigator-user-agent.ts +++ b/packages/wrangler/src/navigator-user-agent.ts @@ -8,9 +8,12 @@ export function isNavigatorDefined( compatibility_flags.includes("global_navigator") && compatibility_flags.includes("no_global_navigator") ) { - throw new UserError("Can't both enable and disable a flag", { - telemetryMessage: "navigator user agent compatibility flags conflict", - }); + throw new UserError( + 'Conflicting compatibility flags: "global_navigator" and "no_global_navigator" cannot both be set. Remove one of these flags from your configuration.', + { + telemetryMessage: "navigator user agent compatibility flags conflict", + } + ); } if (compatibility_flags.includes("global_navigator")) { diff --git a/packages/wrangler/src/process-env.ts b/packages/wrangler/src/process-env.ts index 2dbdb92878..6b139fedef 100644 --- a/packages/wrangler/src/process-env.ts +++ b/packages/wrangler/src/process-env.ts @@ -8,9 +8,12 @@ export function isProcessEnvPopulated( compatibility_flags.includes("nodejs_compat_populate_process_env") && compatibility_flags.includes("nodejs_compat_do_not_populate_process_env") ) { - throw new UserError("Can't both enable and disable a flag", { - telemetryMessage: "process env compatibility flags conflict", - }); + throw new UserError( + 'Conflicting compatibility flags: "nodejs_compat_populate_process_env" and "nodejs_compat_do_not_populate_process_env" cannot both be set. Remove one of these flags from your configuration.', + { + telemetryMessage: "process env compatibility flags conflict", + } + ); } if ( diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 444dd9e897..c1b18f343e 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -196,7 +196,7 @@ export const typesCommand = createCommand({ if (!args.includeEnv && !args.includeRuntime) { throw new CommandLineArgsError( - `You cannot run this command without including either Env or Runtime types`, + "At least one of --include-env or --include-runtime must be enabled. Use --include-env to generate environment/binding types, or --include-runtime to generate Workers runtime types.", { telemetryMessage: "type generation args missing type selection", } @@ -208,6 +208,7 @@ export const typesCommand = createCommand({ logSecondaryEntries: true, validateOptions: false, validateOutputPath: false, + source: "cli", }); resolvedOptions.envHeaderCommand = buildGenerateTypesHeaderCommand( @@ -298,6 +299,7 @@ export async function generateTypesFromWranglerOptions( logSecondaryEntries: false, validateOptions: true, validateOutputPath: false, + source: "api", }); resolvedOptions.envHeaderCommand = buildGenerateTypesHeaderCommand( options, @@ -410,10 +412,12 @@ async function resolveGenerateTypesOptions( logSecondaryEntries, validateOptions, validateOutputPath, + source, }: { logSecondaryEntries: boolean; validateOptions: boolean; validateOutputPath: boolean; + source: "cli" | "api"; } ): Promise { const envInterface = options.envInterface ?? "Env"; @@ -429,6 +433,7 @@ async function resolveGenerateTypesOptions( includeRuntime, path, validateOutputPath, + source, }); } @@ -656,12 +661,15 @@ function assertConfigFileDetected( } /** - * Validates programmatic type-generation options. + * Validates type-generation options. * * Applies the same constraints as CLI argument validation for env interface * naming, output file extension, and include-env/include-runtime combinations. * * @param options - Normalized options to validate. + * @param options.source - Whether the caller is `"cli"` or `"api"`, so error + * messages can reference CLI flags (`--include-env`) or JS option names + * (`includeEnv`) accordingly. * * @throws {UserError} When any option is invalid. */ @@ -671,12 +679,14 @@ function validateGenerateTypesOptions({ includeRuntime, path, validateOutputPath, + source, }: { envInterface: string; includeEnv: boolean; includeRuntime: boolean; path: string; validateOutputPath: boolean; + source: "cli" | "api"; }): void { const validInterfaceRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/; if (!validInterfaceRegex.test(envInterface)) { @@ -698,8 +708,13 @@ function validateGenerateTypesOptions({ } if (!includeEnv && !includeRuntime) { + const [envOpt, runtimeOpt] = + source === "cli" + ? ["--include-env", "--include-runtime"] + : ["includeEnv", "includeRuntime"]; + throw new UserError( - `You cannot run this command without including either Env or Runtime types`, + `At least one of ${envOpt} or ${runtimeOpt} must be enabled. Use ${envOpt} to generate environment/binding types, or ${runtimeOpt} to generate Workers runtime types.`, { telemetryMessage: "type generation args missing type selection" } ); } diff --git a/packages/wrangler/src/user/commands.ts b/packages/wrangler/src/user/commands.ts index 670e11bcf6..c708d494e2 100644 --- a/packages/wrangler/src/user/commands.ts +++ b/packages/wrangler/src/user/commands.ts @@ -4,6 +4,7 @@ import { createCommand, createNamespace } from "../core/create-command"; import { logger } from "../logger"; import * as metrics from "../metrics"; import { + DefaultScopeKeys, getAuthFromEnv, getOAuthTokenFromLocalState, listScopes, @@ -73,8 +74,10 @@ export const loginCommand = createCommand({ return; } if (!validateScopeKeys(args.scopes)) { + const validSet = new Set(DefaultScopeKeys); + const invalidScopes = args.scopes.filter((s) => !validSet.has(s)); throw new CommandLineArgsError( - `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --scopes-list" to see the valid scopes.`, + `Invalid authentication scope${invalidScopes.length > 1 ? "s" : ""}: ${invalidScopes.map((s) => `"${s}"`).join(", ")}. Run "wrangler login --scopes-list" to see all valid scopes.`, { telemetryMessage: "user login invalid scope" } ); }