From 09d713ea9b455e60850664b94f986465d8e07bc7 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:34:14 -0700 Subject: [PATCH 1/8] test: reconstruct PR 16074 from last-green on current main --- .changeset/wrangler-public-types.md | 7 + packages/wrangler/package.json | 2 +- .../scripts/check-published-declarations.ts | 86 ++++++++++++ .../experimental-commands-api.test.ts | 36 ++++- packages/wrangler/src/complete.ts | 4 +- .../wrangler/src/experimental-commands-api.ts | 125 +++++++++++++++++- packages/wrangler/tsup.config.ts | 20 ++- 7 files changed, 268 insertions(+), 12 deletions(-) create mode 100644 .changeset/wrangler-public-types.md create mode 100644 packages/wrangler/scripts/check-published-declarations.ts diff --git a/.changeset/wrangler-public-types.md b/.changeset/wrangler-public-types.md new file mode 100644 index 00000000000..d7227080a95 --- /dev/null +++ b/.changeset/wrangler-public-types.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Remove private build dependencies from Wrangler's published declaration surface + +Wrangler now resolves bundled, dev-only type dependencies into its declaration output instead of exposing them as imports that downstream projects cannot install. Runtime dependencies and peer dependencies remain external. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 24be307f406..13b296df888 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -79,7 +79,7 @@ "test:debug": "pnpm run test --silent=false --verbose=true", "test:e2e": "dotenv -- vitest run -c ./e2e/vitest.config.mts", "test:watch": "pnpm run test --testTimeout=50000 --watch", - "type:tests": "tsc -p ./src/__tests__/tsconfig.json && tsc -p ./e2e/tsconfig.json" + "type:tests": "tsc -p ./src/__tests__/tsconfig.json && tsc -p ./e2e/tsconfig.json && node -r esbuild-register scripts/check-published-declarations.ts" }, "dependencies": { "@cloudflare/kv-asset-handler": "workspace:*", diff --git a/packages/wrangler/scripts/check-published-declarations.ts b/packages/wrangler/scripts/check-published-declarations.ts new file mode 100644 index 00000000000..57598c75107 --- /dev/null +++ b/packages/wrangler/scripts/check-published-declarations.ts @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import path from "node:path"; +import ts from "typescript"; +import { + getEntryPointPaths, + getPackageNameFromSpecifier, + isBareSpecifier, + validateDistImports, + type PackageJSON, +} from "../../../tools/deployments/validate-package-dependencies"; + +type WranglerPackageJSON = PackageJSON & { types?: string }; + +const packageRoot = path.resolve(__dirname, ".."); +const packageJson = JSON.parse( + fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8") +) as WranglerPackageJSON; + +const typeEntryPoints = new Set( + [ + packageJson.types, + ...getEntryPointPaths(packageJson).filter(isDeclarationPath), + ].filter((value): value is string => value !== undefined) +); + +const declarationFiles = new Set(); +for (const entryPoint of typeEntryPoints) { + const absoluteEntryPoint = path.resolve(packageRoot, entryPoint); + if (!fs.existsSync(absoluteEntryPoint)) { + throw new Error("Missing built type entry point: " + entryPoint); + } + collectDeclarationFiles(path.dirname(absoluteEntryPoint), declarationFiles); +} + +const importedPackages = new Set(); +for (const declarationFile of declarationFiles) { + const preprocessed = ts.preProcessFile( + fs.readFileSync(declarationFile, "utf-8"), + true, + true + ); + const references = [ + ...preprocessed.importedFiles, + ...preprocessed.typeReferenceDirectives, + ]; + for (const { fileName } of references) { + if (isBareSpecifier(fileName)) { + importedPackages.add(getPackageNameFromSpecifier(fileName)); + } + } +} + +const errors = validateDistImports( + packageJson.name, + packageJson, + importedPackages +); +if (errors.length > 0) { + throw new Error( + "Wrangler's published declarations have unresolved package boundaries:\n" + + errors.map((error) => "- " + error).join("\n") + ); +} + +process.stdout.write( + "Checked " + + declarationFiles.size + + " published declaration file(s) across " + + typeEntryPoints.size + + " public type entry point(s).\n" +); + +function isDeclarationPath(filePath: string): boolean { + return /\.d\.(?:ts|mts|cts)$/.test(filePath); +} + +function collectDeclarationFiles(directory: string, files: Set): void { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + collectDeclarationFiles(absolutePath, files); + } else if (isDeclarationPath(entry.name)) { + files.add(absolutePath); + } + } +} diff --git a/packages/wrangler/src/__tests__/experimental-commands-api.test.ts b/packages/wrangler/src/__tests__/experimental-commands-api.test.ts index ee7b0b4dadb..36a75d2908d 100644 --- a/packages/wrangler/src/__tests__/experimental-commands-api.test.ts +++ b/packages/wrangler/src/__tests__/experimental-commands-api.test.ts @@ -1,6 +1,6 @@ import { assert, describe, test } from "vitest"; import { experimental_getWranglerCommands } from "../experimental-commands-api"; -import type { DefinitionTreeNode } from "../core/types"; +import type { ExperimentalDefinitionTreeNode } from "../experimental-commands-api"; describe("experimental_getWranglerCommands", () => { test("returns global flags", ({ expect }) => { @@ -109,6 +109,38 @@ describe("experimental_getWranglerCommands", () => { expect(initCommand.definition.metadata.owner).toBeDefined(); }); + test("preserves public command argument metadata", ({ expect }) => { + const commandTree = experimental_getWranglerCommands().registry; + const quickStart = commandTree.subtree + .get("tunnel") + ?.subtree.get("quick-start"); + assert(quickStart?.definition?.type === "command"); + + const urlArg = quickStart.definition.args?.url; + const allowedMailArg = quickStart.definition.args?.["allowed-mail"]; + assert(urlArg); + assert(allowedMailArg); + + expect(urlArg.description).toBe( + "The local URL to expose (e.g., http://localhost:3000)" + ); + expect(allowedMailArg.nargs).toBe(1); + + // These fields are part of the public structural type even when a + // particular command does not use all of them. + const publicArgMetadata = { + description: urlArg.description, + nargs: allowedMailArg.nargs, + group: "group", + conflicts: ["other"], + implies: "required", + } satisfies NonNullable; + + expect(publicArgMetadata.group).toBe("group"); + expect(publicArgMetadata.conflicts).toEqual(["other"]); + expect(publicArgMetadata.implies).toBe("required"); + }); + test("includes namespace commands", ({ expect }) => { const commandTree = experimental_getWranglerCommands().registry; @@ -142,7 +174,7 @@ describe("experimental_getWranglerCommands", () => { expect, }) => { const supporting: string[] = []; - const walk = (node: DefinitionTreeNode) => { + const walk = (node: ExperimentalDefinitionTreeNode) => { if ( node.definition?.type === "command" && node.definition.behaviour?.supportTemporary diff --git a/packages/wrangler/src/complete.ts b/packages/wrangler/src/complete.ts index 97cecf971e0..f8495cb187d 100644 --- a/packages/wrangler/src/complete.ts +++ b/packages/wrangler/src/complete.ts @@ -2,7 +2,7 @@ import t from "@bomb.sh/tab"; import { CommandLineArgsError } from "@cloudflare/workers-utils"; import { createCommand } from "./core/create-command"; import { experimental_getWranglerCommands } from "./experimental-commands-api"; -import type { DefinitionTreeNode } from "./core/types"; +import type { ExperimentalDefinitionTreeNode } from "./experimental-commands-api"; function setupCompletions(): void { const { registry, globalFlags } = experimental_getWranglerCommands(); @@ -28,7 +28,7 @@ function setupCompletions(): void { // Recursively add commands from the registry tree function addCommandsFromTree( - node: DefinitionTreeNode, + node: ExperimentalDefinitionTreeNode, parentPath: string[] = [] ): void { for (const [name, childNode] of node.subtree.entries()) { diff --git a/packages/wrangler/src/experimental-commands-api.ts b/packages/wrangler/src/experimental-commands-api.ts index 73d8864fdb9..97ad41f8f03 100644 --- a/packages/wrangler/src/experimental-commands-api.ts +++ b/packages/wrangler/src/experimental-commands-api.ts @@ -1,16 +1,129 @@ import { createCLIParser } from "./index"; -import type { DefinitionTreeNode } from "./core/types"; + +export type ExperimentalCommandMetadata = { + description: string; + status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; + statusMessage?: string; + deprecated?: boolean; + deprecatedMessage?: string; + hidden?: boolean; + owner: string; + category?: string; + epilogue?: string; + examples?: Array<{ command: string; description: string }>; + hideGlobalFlags?: string[]; +}; + +type ExperimentalStringRequirement = + | string + | readonly string[] + | Record; + +export type ExperimentalCommandArgDefinition = { + alias?: string | readonly string[]; + array?: boolean; + boolean?: boolean; + choices?: ReadonlyArray; + coerce?: (arg: unknown) => unknown; + config?: boolean; + configParser?: (configPath: string) => object; + conflicts?: ExperimentalStringRequirement; + count?: boolean; + default?: unknown; + defaultDescription?: string; + demand?: boolean | string; + deprecate?: boolean | string; + deprecated?: boolean | string; + demandOption?: boolean | string; + desc?: string; + describe?: string; + description?: string; + global?: boolean; + group?: string; + hidden?: boolean; + implies?: ExperimentalStringRequirement; + nargs?: number; + normalize?: boolean; + number?: boolean; + require?: boolean | string; + required?: boolean | string; + requiresArg?: boolean; + skipValidation?: boolean; + string?: boolean; + type?: "array" | "count" | "boolean" | "number" | "string"; +}; + +export type ExperimentalCommandBehaviour = { + printBanner?: boolean | ((args: unknown) => boolean); + provideConfig?: boolean; + printConfigWarnings?: boolean; + useConfigRedirectIfAvailable?: boolean; + sendMetrics?: boolean; + supportTemporary?: boolean; + suggestSkillsAfterHandler?: boolean | ((args: unknown) => boolean); + [key: string]: unknown; +}; + +export type ExperimentalCommandCommandDefinition = { + type: "command"; + command: `wrangler${string}`; + metadata: ExperimentalCommandMetadata; + args?: Record; + behaviour?: ExperimentalCommandBehaviour; + positionalArgs?: string[]; + validateArgs?: (...args: unknown[]) => unknown; + handler?: (...args: unknown[]) => unknown; +}; + +export type ExperimentalCommandNamespaceDefinition = { + type: "namespace"; + command: `wrangler${string}`; + metadata: ExperimentalCommandMetadata; +}; + +export type ExperimentalCommandAliasDefinition = { + type: "alias"; + command: `wrangler${string}`; + aliasOf: `wrangler${string}`; + metadata?: Partial; +}; + +export type ExperimentalCommandDefinition = + | ExperimentalCommandCommandDefinition + | ExperimentalCommandNamespaceDefinition + | ExperimentalCommandAliasDefinition; + +export type ExperimentalDefinitionTreeNode = { + definition?: ExperimentalCommandDefinition; + subtree: Map; +}; + +export type ExperimentalGlobalFlags = Record< + string, + ExperimentalCommandArgDefinition +>; + +export type ExperimentalWranglerCommands = { + registry: ExperimentalDefinitionTreeNode; + globalFlags: ExperimentalGlobalFlags; +}; /** * EXPERIMENTAL: Get all registered Wrangler commands for documentation generation. * This API is experimental and may change without notice. * + * The published return type is a dependency-free structural view of the + * command registry. Runtime definitions can contain additional internal + * properties, but implementation-only dependency types are intentionally kept + * out of Wrangler's public declaration boundary. + * * @returns An object containing the command tree structure and global flags */ -export function experimental_getWranglerCommands(): { - registry: DefinitionTreeNode; - globalFlags: ReturnType["globalFlags"]; -} { +export function experimental_getWranglerCommands(): ExperimentalWranglerCommands { const { registry, globalFlags } = createCLIParser([]); - return { registry: registry.getDefinitionTreeRoot(), globalFlags }; + return { + registry: + registry.getDefinitionTreeRoot() as ExperimentalDefinitionTreeNode, + globalFlags: globalFlags as ExperimentalGlobalFlags, + }; } diff --git a/packages/wrangler/tsup.config.ts b/packages/wrangler/tsup.config.ts index c9289d35f4e..8c9f933adc3 100644 --- a/packages/wrangler/tsup.config.ts +++ b/packages/wrangler/tsup.config.ts @@ -108,7 +108,25 @@ export default defineConfig((options) => [ platform: "node", format: "cjs", dts: { - resolve: ["@cloudflare/workflows-shared/src/types"], + compilerOptions: { + baseUrl: ".", + paths: { + "@cloudflare/workers-utils": ["../workers-utils/src/index.ts"], + "@cloudflare/remote-bindings": ["../remote-bindings/src/index.ts"], + }, + }, + // Resolve only the remaining private type dependencies that are safe + // for tsup's node_modules resolver to inline. The workspace packages + // above use declaration-only source paths because they publish through + // conditional .d.mts exports, which tsup 8.3's resolver does not probe. + resolve: [ + "@cloudflare/workflows-shared/src/types", + "@cloudflare/containers-shared", + "@cloudflare/workers-shared", + "devtools-protocol", + "devtools-protocol/types/protocol-mapping", + "undici", + ], }, outDir: "wrangler-dist", tsconfig: "tsconfig.json", From 2022223ac8b79dbfd829b7afdbbc4104601e253d Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:38:54 -0700 Subject: [PATCH 2/8] test: preserve workers-utils type identity without externalising runtime --- .changeset/wrangler-public-types.md | 4 ++-- packages/wrangler/package.json | 2 +- packages/wrangler/tsup.config.ts | 15 ++++++++++----- pnpm-lock.yaml | 6 +++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.changeset/wrangler-public-types.md b/.changeset/wrangler-public-types.md index d7227080a95..8bbddc7f3ba 100644 --- a/.changeset/wrangler-public-types.md +++ b/.changeset/wrangler-public-types.md @@ -2,6 +2,6 @@ "wrangler": patch --- -Remove private build dependencies from Wrangler's published declaration surface +Remove unresolved private build dependencies from Wrangler's published declaration surface -Wrangler now resolves bundled, dev-only type dependencies into its declaration output instead of exposing them as imports that downstream projects cannot install. Runtime dependencies and peer dependencies remain external. +Wrangler now inlines declaration dependencies that are safe to bundle, keeps `@cloudflare/workers-utils` external in declarations as a declared dependency so nominal symbol identities remain shared, while continuing to bundle its runtime into Wrangler. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 13b296df888..7798f02f307 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -84,6 +84,7 @@ "dependencies": { "@cloudflare/kv-asset-handler": "workspace:*", "@cloudflare/unenv-preset": "workspace:*", + "@cloudflare/workers-utils": "workspace:*", "blake3-wasm": "2.1.5", "esbuild": "catalog:default", "miniflare": "workspace:*", @@ -111,7 +112,6 @@ "@cloudflare/workers-shared": "workspace:*", "@cloudflare/workers-tsconfig": "workspace:*", "@cloudflare/workers-types": "catalog:default", - "@cloudflare/workers-utils": "workspace:*", "@cloudflare/workflows-shared": "workspace:*", "@cspotcode/source-map-support": "0.8.1", "@netlify/build-info": "10.5.1", diff --git a/packages/wrangler/tsup.config.ts b/packages/wrangler/tsup.config.ts index 8c9f933adc3..a138eb108ed 100644 --- a/packages/wrangler/tsup.config.ts +++ b/packages/wrangler/tsup.config.ts @@ -111,14 +111,16 @@ export default defineConfig((options) => [ compilerOptions: { baseUrl: ".", paths: { - "@cloudflare/workers-utils": ["../workers-utils/src/index.ts"], "@cloudflare/remote-bindings": ["../remote-bindings/src/index.ts"], }, }, - // Resolve only the remaining private type dependencies that are safe - // for tsup's node_modules resolver to inline. The workspace packages - // above use declaration-only source paths because they publish through - // conditional .d.mts exports, which tsup 8.3's resolver does not probe. + // Resolve only private type dependencies that are safe for tsup's + // declaration bundler to inline. @cloudflare/workers-utils stays + // external in declarations because its Binding types contain unique + // symbols whose identity must be shared with downstream consumers. + // @cloudflare/remote-bindings uses a declaration-only source path + // because it publishes through conditional .d.mts exports, which + // tsup 8.3's resolver does not probe. resolve: [ "@cloudflare/workflows-shared/src/types", "@cloudflare/containers-shared", @@ -131,6 +133,9 @@ export default defineConfig((options) => [ outDir: "wrangler-dist", tsconfig: "tsconfig.json", external: EXTERNAL_DEPENDENCIES, + // workers-utils is a published type dependency, but Wrangler's CJS runtime + // must continue bundling it because the package root is ESM-only. + noExternal: ["@cloudflare/workers-utils"], sourcemap: process.env.SOURCEMAPS !== "false", inject: [path.join(__dirname, "import_meta_url.js")], // mainFields: ["module", "main"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21efd01d384..38a6701c2f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4582,6 +4582,9 @@ importers: '@cloudflare/unenv-preset': specifier: workspace:* version: link:../unenv-preset + '@cloudflare/workers-utils': + specifier: workspace:* + version: link:../workers-utils blake3-wasm: specifier: 2.1.5 version: 2.1.5 @@ -4658,9 +4661,6 @@ importers: '@cloudflare/workers-types': specifier: catalog:default version: 5.20261006.1 - '@cloudflare/workers-utils': - specifier: workspace:* - version: link:../workers-utils '@cloudflare/workflows-shared': specifier: workspace:* version: link:../workflows-shared From ee458decffc3eb061d82d35f9a7f8f356172876d Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:44:55 -0700 Subject: [PATCH 3/8] fix: close published declaration boundaries while preserving shared types --- .changeset/wrangler-public-types.md | 5 +- .../src/construct-wrangler-config.ts | 32 ++++- .../tests/construct-wrangler-config.test.ts | 18 ++- packages/workers-utils/tsup.config.ts | 17 ++- packages/wrangler/package.json | 2 +- .../scripts/check-published-declarations.ts | 130 +++++++++++------- packages/wrangler/scripts/deps.ts | 4 + .../__tests__/published-declarations.test.ts | 75 ++++++++++ packages/wrangler/tsup.config.ts | 15 +- pnpm-lock.yaml | 6 +- 10 files changed, 236 insertions(+), 68 deletions(-) create mode 100644 packages/wrangler/src/__tests__/published-declarations.test.ts diff --git a/.changeset/wrangler-public-types.md b/.changeset/wrangler-public-types.md index 8bbddc7f3ba..f8de469aaab 100644 --- a/.changeset/wrangler-public-types.md +++ b/.changeset/wrangler-public-types.md @@ -1,7 +1,8 @@ --- "wrangler": patch +"@cloudflare/workers-utils": patch --- -Remove unresolved private build dependencies from Wrangler's published declaration surface +Fix unresolved imports in Wrangler's published TypeScript declarations -Wrangler now inlines declaration dependencies that are safe to bundle, keeps `@cloudflare/workers-utils` external in declarations as a declared dependency so nominal symbol identities remain shared, while continuing to bundle its runtime into Wrangler. +Projects that check dependency declarations no longer need Wrangler's private build dependencies to resolve its public types. Inherited bindings remain compatible between Wrangler and the shared utilities used by the Vite and Vitest plugins. diff --git a/packages/workers-utils/src/construct-wrangler-config.ts b/packages/workers-utils/src/construct-wrangler-config.ts index ff780df72bf..22a958117b2 100644 --- a/packages/workers-utils/src/construct-wrangler-config.ts +++ b/packages/workers-utils/src/construct-wrangler-config.ts @@ -9,7 +9,6 @@ import type { } from "./config/environment"; import type { WorkerMetadata } from "./types"; import type { AssetConfig } from "@cloudflare/workers-shared"; -import type { Cloudflare } from "cloudflare"; type RoutesRes = { id: string; @@ -18,6 +17,8 @@ type RoutesRes = { script: string; }[]; +// Keep SDK response shapes structural so consumers do not need the SDK's +// declarations. The type assertions in the tests track changes to these shapes. interface APIWorkerConfig { /* sourced from https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/list/ */ name: string; // property renamed from `id`... @@ -31,16 +32,37 @@ interface APIWorkerConfig { migration_tag?: string; /* sourced from https://developers.cloudflare.com/api/resources/workers/subresources/domains/methods/list/ */ - domains: Cloudflare.Workers.Domain[]; + domains: { + id?: string; + environment?: string; + hostname?: string; + service?: string; + zone_id?: string; + zone_name?: string; + }[]; /* sourced from https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/schedules/methods/get/ */ - schedules: Cloudflare.Workers.Scripts.Schedules.ScheduleGetResponse.Schedule[]; + schedules: { + cron: string; + created_on?: string; + modified_on?: string; + }[]; /* sourced from https://developers.cloudflare.com/api/resources/workers/subresources/beta/subresources/workers/subresources/versions/methods/get/ using `{version_id}` of `latest` */ assets?: AssetConfig; bindings: WorkerMetadata["bindings"]; - observability: Cloudflare.Workers.Beta.Worker.Observability | undefined; + observability: + | { + enabled?: boolean; + head_sampling_rate?: number; + logs?: { + enabled?: boolean; + head_sampling_rate?: number; + invocation_logs?: boolean; + }; + } + | undefined; limits: { cpu_ms?: number; subrequests?: number } | undefined; - placement: Cloudflare.Workers.Beta.Workers.Version.Placement | undefined; + placement: { mode?: "smart" } | undefined; subdomain: { enabled: boolean; previews_enabled: boolean; diff --git a/packages/workers-utils/tests/construct-wrangler-config.test.ts b/packages/workers-utils/tests/construct-wrangler-config.test.ts index 071cc057d76..678a54160eb 100644 --- a/packages/workers-utils/tests/construct-wrangler-config.test.ts +++ b/packages/workers-utils/tests/construct-wrangler-config.test.ts @@ -1,5 +1,6 @@ -import { describe, it } from "vitest"; +import { describe, expectTypeOf, it } from "vitest"; import { constructWranglerConfig } from "../src/construct-wrangler-config"; +import type { Cloudflare } from "cloudflare"; type APIWorkerConfig = Parameters[0]; @@ -34,6 +35,21 @@ function makeWorkerConfig( } describe("constructWranglerConfig", () => { + it("preserves the SDK response types without exposing the SDK", () => { + expectTypeOf().toEqualTypeOf< + Cloudflare.Workers.Domain[] + >(); + expectTypeOf().toEqualTypeOf< + Cloudflare.Workers.Scripts.Schedules.ScheduleGetResponse.Schedule[] + >(); + expectTypeOf().toEqualTypeOf< + Cloudflare.Workers.Beta.Worker.Observability | undefined + >(); + expectTypeOf().toEqualTypeOf< + Cloudflare.Workers.Beta.Workers.Version.Placement | undefined + >(); + }); + describe("basic field mapping", () => { it("maps core fields correctly", ({ expect }) => { const config = makeWorkerConfig({ diff --git a/packages/workers-utils/tsup.config.ts b/packages/workers-utils/tsup.config.ts index 3e79a489832..23ca3a69211 100644 --- a/packages/workers-utils/tsup.config.ts +++ b/packages/workers-utils/tsup.config.ts @@ -1,6 +1,19 @@ import { defineConfig } from "tsup"; import { EXTERNAL_DEPENDENCIES } from "./scripts/deps"; +const declarationOptions = { + resolve: ["@cloudflare/workers-shared", "jsonc-parser", "zod", /^\./], + compilerOptions: { + baseUrl: ".", + paths: { + "@cloudflare/workers-shared": ["../workers-shared/index.ts"], + // tsup 8.3 does not load Zod's default .d.cts entry point. Resolve its + // published ESM declarations without changing the runtime build. + zod: ["./node_modules/zod/index.d.ts"], + }, + }, +}; + export default defineConfig(() => [ { treeshake: true, @@ -9,7 +22,7 @@ export default defineConfig(() => [ entry: ["src/browser.ts"], platform: "node", format: "esm", - dts: true, + dts: declarationOptions, outDir: "dist", tsconfig: "tsconfig.json", metafile: true, @@ -44,7 +57,7 @@ export default defineConfig(() => [ js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url || (typeof __filename === "string" ? __filename : "/"));', }, format: "esm", - dts: true, + dts: declarationOptions, outDir: "dist", tsconfig: "tsconfig.json", metafile: true, diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 7798f02f307..ae4b3def275 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -89,6 +89,7 @@ "esbuild": "catalog:default", "miniflare": "workspace:*", "path-to-regexp": "6.3.0", + "undici": "catalog:default", "unenv": "2.0.0-rc.24", "workerd": "catalog:default" }, @@ -184,7 +185,6 @@ "tsdown": "0.16.3", "tsup": "8.3.0", "typescript": "catalog:default", - "undici": "catalog:default", "vitest": "catalog:default", "vitest-websocket-mock": "^0.4.0", "ws": "catalog:default", diff --git a/packages/wrangler/scripts/check-published-declarations.ts b/packages/wrangler/scripts/check-published-declarations.ts index 57598c75107..adc1b5bb050 100644 --- a/packages/wrangler/scripts/check-published-declarations.ts +++ b/packages/wrangler/scripts/check-published-declarations.ts @@ -9,66 +9,96 @@ import { type PackageJSON, } from "../../../tools/deployments/validate-package-dependencies"; -type WranglerPackageJSON = PackageJSON & { types?: string }; +type PublishedPackageJSON = PackageJSON & { types?: string }; -const packageRoot = path.resolve(__dirname, ".."); -const packageJson = JSON.parse( - fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8") -) as WranglerPackageJSON; +if (require.main === module) { + checkPublishedDeclarations(path.resolve(__dirname, "..")); + // Wrangler keeps workers-utils' binding types external to preserve their unique + // symbol identity. Its installed declarations must also resolve without private + // workspace packages or development dependencies. + checkPublishedDeclarations(path.resolve(__dirname, "../../workers-utils")); +} + +/** Validate the declaration files shipped at a package's public type entry points. */ +export function checkPublishedDeclarations(packageRoot: string): void { + const packageJson = JSON.parse( + fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8") + ) as PublishedPackageJSON; -const typeEntryPoints = new Set( - [ - packageJson.types, - ...getEntryPointPaths(packageJson).filter(isDeclarationPath), - ].filter((value): value is string => value !== undefined) -); + const typeEntryPoints = new Set( + [ + packageJson.types, + ...getEntryPointPaths(packageJson).filter(isDeclarationPath), + ].filter((value): value is string => value !== undefined) + ); -const declarationFiles = new Set(); -for (const entryPoint of typeEntryPoints) { - const absoluteEntryPoint = path.resolve(packageRoot, entryPoint); - if (!fs.existsSync(absoluteEntryPoint)) { - throw new Error("Missing built type entry point: " + entryPoint); + const declarationFiles = new Set(); + for (const entryPoint of typeEntryPoints) { + const absoluteEntryPoint = path.resolve(packageRoot, entryPoint); + if (!fs.existsSync(absoluteEntryPoint)) { + throw new Error("Missing built type entry point: " + entryPoint); + } + collectDeclarationFiles(path.dirname(absoluteEntryPoint), declarationFiles); } - collectDeclarationFiles(path.dirname(absoluteEntryPoint), declarationFiles); -} -const importedPackages = new Set(); -for (const declarationFile of declarationFiles) { - const preprocessed = ts.preProcessFile( - fs.readFileSync(declarationFile, "utf-8"), - true, - true - ); - const references = [ - ...preprocessed.importedFiles, - ...preprocessed.typeReferenceDirectives, - ]; - for (const { fileName } of references) { - if (isBareSpecifier(fileName)) { - importedPackages.add(getPackageNameFromSpecifier(fileName)); + const importedPackages = new Set(); + const errors: string[] = []; + for (const declarationFile of declarationFiles) { + const preprocessed = ts.preProcessFile( + fs.readFileSync(declarationFile, "utf-8"), + true, + true + ); + const references = [ + ...preprocessed.importedFiles, + ...preprocessed.typeReferenceDirectives, + ...preprocessed.referencedFiles, + ]; + for (const { fileName } of references) { + if (isBareSpecifier(fileName)) { + importedPackages.add(getPackageNameFromSpecifier(fileName)); + } else if (fileName.startsWith(".")) { + const { resolvedModule } = ts.resolveModuleName( + fileName, + declarationFile, + { moduleResolution: ts.ModuleResolutionKind.Bundler }, + ts.sys + ); + if ( + resolvedModule === undefined || + !declarationFiles.has(path.resolve(resolvedModule.resolvedFileName)) + ) { + errors.push( + path.relative(packageRoot, declarationFile) + + " references an unpublished declaration: " + + fileName + ); + } + } } } -} -const errors = validateDistImports( - packageJson.name, - packageJson, - importedPackages -); -if (errors.length > 0) { - throw new Error( - "Wrangler's published declarations have unresolved package boundaries:\n" + - errors.map((error) => "- " + error).join("\n") + errors.push( + ...validateDistImports(packageJson.name, packageJson, importedPackages) ); -} + if (errors.length > 0) { + throw new Error( + packageJson.name + + "'s published declarations have unresolved package boundaries:\n" + + errors.map((error) => "- " + error).join("\n") + ); + } -process.stdout.write( - "Checked " + - declarationFiles.size + - " published declaration file(s) across " + - typeEntryPoints.size + - " public type entry point(s).\n" -); + process.stdout.write( + "Checked " + + declarationFiles.size + + " published declaration file(s) across " + + typeEntryPoints.size + + " public type entry point(s) for " + + packageJson.name + + ".\n" + ); +} function isDeclarationPath(filePath: string): boolean { return /\.d\.(?:ts|mts|cts)$/.test(filePath); diff --git a/packages/wrangler/scripts/deps.ts b/packages/wrangler/scripts/deps.ts index 2f90199540d..85c0dd72ea9 100644 --- a/packages/wrangler/scripts/deps.ts +++ b/packages/wrangler/scripts/deps.ts @@ -14,6 +14,10 @@ export const EXTERNAL_DEPENDENCIES = [ // Wrangler depends on a pinned version of Miniflare. "miniflare", + // Public fetch options expose Undici's MockAgent type. Keep its class identity + // shared with consumers; tsup's noExternal override still bundles its runtime. + "undici", + // @cloudflare/workers-types is an optional peer dependency of wrangler, so users can // get the types by installing the package (to what version they prefer) themselves "@cloudflare/workers-types", diff --git a/packages/wrangler/src/__tests__/published-declarations.test.ts b/packages/wrangler/src/__tests__/published-declarations.test.ts new file mode 100644 index 00000000000..2abf63968d6 --- /dev/null +++ b/packages/wrangler/src/__tests__/published-declarations.test.ts @@ -0,0 +1,75 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { INHERIT_SYMBOL } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, expectTypeOf, it } from "vitest"; +import { checkPublishedDeclarations } from "../../scripts/check-published-declarations"; +import type { Binding, unstable_startWorker } from "../../wrangler-dist/cli"; +import type { Binding as SharedBinding } from "@cloudflare/workers-utils"; +import type { MockAgent } from "undici"; + +type WorkerInput = Parameters[0]; + +it("preserves shared binding identity in the published declarations", () => { + expectTypeOf().toEqualTypeOf(); + + type InheritedId = Extract< + Extract["id"], + symbol + >; + expectTypeOf().toEqualTypeOf(); + + const binding = { + type: "kv_namespace", + id: INHERIT_SYMBOL, + } satisfies SharedBinding; + expectTypeOf(binding).toExtend(); + expectTypeOf({ + bindings: { CACHE: binding }, + }).toExtend(); + + const differentSymbol = Symbol("different binding"); + expectTypeOf(differentSymbol).not.toExtend(); +}); + +it("preserves Undici's MockAgent type in the published worker options", () => { + type MockFetch = NonNullable["mockFetch"]>; + expectTypeOf().toEqualTypeOf(); +}); + +describe("published declaration boundaries", () => { + runInTempDir(); + beforeEach(() => { + mkdirSync("dist"); + writeFileSync( + "package.json", + JSON.stringify({ name: "declaration-fixture", types: "dist/index.d.ts" }) + ); + }); + + it.for([ + { + declaration: + 'import type { AssetConfig } from "@cloudflare/workers-shared";', + missing: "@cloudflare/workers-shared", + }, + { + declaration: 'import type { RequestInfo } from "./types/index";', + missing: "./types/index", + }, + { + declaration: '/// ', + missing: "private-types", + }, + ])("rejects unresolved $missing", ({ declaration, missing }, { expect }) => { + writeFileSync("dist/index.d.ts", declaration); + expect(() => checkPublishedDeclarations(process.cwd())).toThrow(missing); + }); + + it("accepts relative imports to declarations shipped in the package", ({ + expect, + }) => { + writeFileSync("dist/index.d.ts", 'export type { Binding } from "./types";'); + writeFileSync("dist/types.d.ts", 'export type Binding = { type: "test" };'); + expect(() => checkPublishedDeclarations(process.cwd())).not.toThrow(); + }); +}); diff --git a/packages/wrangler/tsup.config.ts b/packages/wrangler/tsup.config.ts index a138eb108ed..d4673aff64d 100644 --- a/packages/wrangler/tsup.config.ts +++ b/packages/wrangler/tsup.config.ts @@ -111,7 +111,11 @@ export default defineConfig((options) => [ compilerOptions: { baseUrl: ".", paths: { + "@cloudflare/containers-shared": ["../containers-shared/index.ts"], "@cloudflare/remote-bindings": ["../remote-bindings/src/index.ts"], + "@cloudflare/workers-shared": ["../workers-shared/index.ts"], + // Resolve Zod's ESM declarations; tsup 8.3 cannot load .d.cts. + zod: ["./node_modules/zod/index.d.ts"], }, }, // Resolve only private type dependencies that are safe for tsup's @@ -122,20 +126,23 @@ export default defineConfig((options) => [ // because it publishes through conditional .d.mts exports, which // tsup 8.3's resolver does not probe. resolve: [ + // Follow imports within the resolved packages as well, so their + // private relative paths cannot escape into Wrangler's output. + /^\./, "@cloudflare/workflows-shared/src/types", "@cloudflare/containers-shared", "@cloudflare/workers-shared", "devtools-protocol", "devtools-protocol/types/protocol-mapping", - "undici", + "zod", ], }, outDir: "wrangler-dist", tsconfig: "tsconfig.json", external: EXTERNAL_DEPENDENCIES, - // workers-utils is a published type dependency, but Wrangler's CJS runtime - // must continue bundling it because the package root is ESM-only. - noExternal: ["@cloudflare/workers-utils"], + // Preserve the bundled CJS runtime while keeping dependency-owned public + // types external. In particular, workers-utils' package root is ESM-only. + noExternal: ["@cloudflare/workers-utils", "undici"], sourcemap: process.env.SOURCEMAPS !== "false", inject: [path.join(__dirname, "import_meta_url.js")], // mainFields: ["module", "main"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38a6701c2f2..cea208ed7c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4597,6 +4597,9 @@ importers: path-to-regexp: specifier: 6.3.0 version: 6.3.0 + undici: + specifier: catalog:default + version: 7.29.1 unenv: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 @@ -4877,9 +4880,6 @@ importers: typescript: specifier: catalog:default version: 5.8.3 - undici: - specifier: catalog:default - version: 7.29.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.14.6(@types/node@22.15.17)(typescript@5.8.3))(vite@8.2.0(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) From a74cae8ff40afcbf7721c1ed18b53090ce9eed36 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:09:49 +0100 Subject: [PATCH 4/8] refactor(wrangler): define command metadata once for internal and public APIs --- packages/wrangler/src/command-metadata.ts | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/wrangler/src/command-metadata.ts diff --git a/packages/wrangler/src/command-metadata.ts b/packages/wrangler/src/command-metadata.ts new file mode 100644 index 00000000000..b3f025c175b --- /dev/null +++ b/packages/wrangler/src/command-metadata.ts @@ -0,0 +1,25 @@ +/** + * Shared command metadata shape. Keep this module independent of runtime + * command definitions so generated public declarations remain self-contained. + * + * Internal commands supply their constrained Teams and category unions; + * the experimental inspection API intentionally exposes string values. + */ +export type CommandMetadata = { + description: string; + status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; + statusMessage?: string; + deprecated?: boolean; + deprecatedMessage?: string; + hidden?: boolean; + owner: Owner; + /** Prints something at the bottom of the help. */ + epilogue?: string; + examples?: { command: string; description: string }[]; + hideGlobalFlags?: string[]; + /** + * Optional category for grouping commands in the help output. + * Commands without a category appear under the default "COMMANDS" group. + */ + category?: Category; +}; From 4fdd0cb1757c27e3eab898a0d7f667b0f6163284 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:09:51 +0100 Subject: [PATCH 5/8] refactor(wrangler): reuse shared metadata in core definitions --- packages/wrangler/src/core/types.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/wrangler/src/core/types.ts b/packages/wrangler/src/core/types.ts index 515735f3c1f..4ee866fd2f3 100644 --- a/packages/wrangler/src/core/types.ts +++ b/packages/wrangler/src/core/types.ts @@ -1,3 +1,4 @@ +import type { CommandMetadata } from "../command-metadata"; import type { fetchKVGetValue, fetchResult, @@ -61,28 +62,7 @@ export type MetadataCategory = | "Networking & security"; export type Command = `wrangler${string}`; -export type Metadata = { - description: string; - status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; - statusMessage?: string; - deprecated?: boolean; - deprecatedMessage?: string; - hidden?: boolean; - owner: Teams; - /** Prints something at the bottom of the help */ - epilogue?: string; - examples?: { - command: string; - description: string; - }[]; - hideGlobalFlags?: string[]; - /** - * Optional category for grouping commands in the help output. - * Commands with the same category will be grouped together under a shared heading. - * Commands without a category will appear under the default "COMMANDS" group. - */ - category?: MetadataCategory; -}; +export type Metadata = CommandMetadata; export type ArgDefinition = Omit & Pick; From b018fce2cb209da65ddcc80479eb780fc590a20e Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:09:53 +0100 Subject: [PATCH 6/8] refactor(wrangler): reuse shared metadata in experimental API --- .../wrangler/src/experimental-commands-api.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/wrangler/src/experimental-commands-api.ts b/packages/wrangler/src/experimental-commands-api.ts index 97ad41f8f03..7d84b3cd733 100644 --- a/packages/wrangler/src/experimental-commands-api.ts +++ b/packages/wrangler/src/experimental-commands-api.ts @@ -1,18 +1,7 @@ import { createCLIParser } from "./index"; +import type { CommandMetadata } from "./command-metadata"; -export type ExperimentalCommandMetadata = { - description: string; - status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; - statusMessage?: string; - deprecated?: boolean; - deprecatedMessage?: string; - hidden?: boolean; - owner: string; - category?: string; - epilogue?: string; - examples?: Array<{ command: string; description: string }>; - hideGlobalFlags?: string[]; -}; +export type ExperimentalCommandMetadata = CommandMetadata; type ExperimentalStringRequirement = | string From 1ebe991508ab3d6a6f143f4a9ba5e5e5bd762b43 Mon Sep 17 00:00:00 2001 From: AyobamiH Date: Mon, 12 Oct 2026 19:40:26 +0100 Subject: [PATCH 7/8] refactor(wrangler): share command registry types across internal and public APIs --- .../experimental-commands-api.test.ts | 29 ++- packages/wrangler/src/command-metadata.ts | 25 --- packages/wrangler/src/command-types.ts | 82 +++++++++ packages/wrangler/src/core/types.ts | 165 ++++-------------- .../wrangler/src/experimental-commands-api.ts | 65 ++++--- 5 files changed, 172 insertions(+), 194 deletions(-) delete mode 100644 packages/wrangler/src/command-metadata.ts create mode 100644 packages/wrangler/src/command-types.ts diff --git a/packages/wrangler/src/__tests__/experimental-commands-api.test.ts b/packages/wrangler/src/__tests__/experimental-commands-api.test.ts index 36a75d2908d..0e82289d2d3 100644 --- a/packages/wrangler/src/__tests__/experimental-commands-api.test.ts +++ b/packages/wrangler/src/__tests__/experimental-commands-api.test.ts @@ -1,8 +1,33 @@ -import { assert, describe, test } from "vitest"; +import { assert, describe, expectTypeOf, test } from "vitest"; import { experimental_getWranglerCommands } from "../experimental-commands-api"; -import type { ExperimentalDefinitionTreeNode } from "../experimental-commands-api"; +import type { CommandDefinitionTreeNode } from "../command-types"; +import type { + Command, + DefinitionTreeNode, + InternalDefinition, + Metadata, +} from "../core/types"; +import type { + ExperimentalCommandDefinition, + ExperimentalCommandMetadata, + ExperimentalDefinitionTreeNode, +} from "../experimental-commands-api"; describe("experimental_getWranglerCommands", () => { + test("shares registry metadata and recursive structure with internal commands", () => { + expectTypeOf().toExtend(); + expectTypeOf().not.toExtend(); + expectTypeOf< + ExperimentalCommandDefinition["command"] + >().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + CommandDefinitionTreeNode + >(); + expectTypeOf().toEqualTypeOf< + CommandDefinitionTreeNode + >(); + }); + test("returns global flags", ({ expect }) => { const commandTree = experimental_getWranglerCommands().globalFlags; diff --git a/packages/wrangler/src/command-metadata.ts b/packages/wrangler/src/command-metadata.ts deleted file mode 100644 index b3f025c175b..00000000000 --- a/packages/wrangler/src/command-metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Shared command metadata shape. Keep this module independent of runtime - * command definitions so generated public declarations remain self-contained. - * - * Internal commands supply their constrained Teams and category unions; - * the experimental inspection API intentionally exposes string values. - */ -export type CommandMetadata = { - description: string; - status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; - statusMessage?: string; - deprecated?: boolean; - deprecatedMessage?: string; - hidden?: boolean; - owner: Owner; - /** Prints something at the bottom of the help. */ - epilogue?: string; - examples?: { command: string; description: string }[]; - hideGlobalFlags?: string[]; - /** - * Optional category for grouping commands in the help output. - * Commands without a category appear under the default "COMMANDS" group. - */ - category?: Category; -}; diff --git a/packages/wrangler/src/command-types.ts b/packages/wrangler/src/command-types.ts new file mode 100644 index 00000000000..295fe656e4e --- /dev/null +++ b/packages/wrangler/src/command-types.ts @@ -0,0 +1,82 @@ +/** + * Shared command metadata shape. Keep this module independent of runtime + * command definitions so generated public declarations remain self-contained. + * + * Internal commands supply their constrained Teams and category unions; + * the experimental inspection API intentionally exposes string values. + */ +export type CommandMetadata = { + description: string; + status: "experimental" | "alpha" | "private beta" | "open beta" | "stable"; + statusMessage?: string; + deprecated?: boolean; + deprecatedMessage?: string; + hidden?: boolean; + owner: Owner; + /** Prints something at the bottom of the help. */ + epilogue?: string; + examples?: { command: string; description: string }[]; + hideGlobalFlags?: string[]; + /** + * Optional category for grouping commands in the help output. + * Commands without a category appear under the default "COMMANDS" group. + */ + category?: Category; +}; + +/** Single command identity for internal definitions and public registry views. */ +export type Command = `wrangler${string}`; +export type CommandIdentity = { + type: Kind; + command: Command; +}; + +/** Fields shared by executable commands and public structural projections. */ +export type CommandDefinitionData = { + metadata: Metadata; + behaviour?: Behaviour; + args?: Args; + positionalArgs?: Positionals; +}; +export type CommandNamespaceData = { metadata: Metadata }; +export type CommandAliasData = { + aliasOf: Command; + metadata?: Partial; +}; +export type CommandDefinitionTreeNode = { + definition?: Definition; + subtree: Map>; +}; + +/** + * Shared behaviour supported by both the executable command definitions and + * the experimental command registry. Both views use this single field source. + */ +export type CommandBehaviour = { + /** + * By default, Wrangler's version banner is printed before the handler. + * @default true + */ + printBanner?: boolean | ((args: Args) => boolean); + /** + * By default, Wrangler reads and provides its configuration. + * @default true + */ + provideConfig?: boolean; + /** + * By default, Wrangler prints warnings about the configuration file. + * @default true + */ + printConfigWarnings?: boolean; + /** Check for a Wrangler configuration redirect file. */ + useConfigRedirectIfAvailable?: boolean; + /** + * Opt out of sending metrics for this command. + * @default true + */ + sendMetrics?: boolean; + /** Enable authentication using a temporary preview account. */ + supportTemporary?: boolean; + /** Offer Cloudflare skills after a successful handler invocation. */ + suggestSkillsAfterHandler?: boolean | ((args: Args) => boolean); +}; diff --git a/packages/wrangler/src/core/types.ts b/packages/wrangler/src/core/types.ts index 4ee866fd2f3..715408624fe 100644 --- a/packages/wrangler/src/core/types.ts +++ b/packages/wrangler/src/core/types.ts @@ -1,10 +1,19 @@ -import type { CommandMetadata } from "../command-metadata"; import type { fetchKVGetValue, fetchResult, fetchListResult, fetchPagedListResult, } from "../cfetch"; +import type { + CommandAliasData, + CommandBehaviour, + CommandDefinitionData, + CommandDefinitionTreeNode, + CommandIdentity, + CommandMetadata, + CommandNamespaceData, + Command as SharedCommand, +} from "../command-types"; import type { confirm, prompt } from "../dialogs"; import type { ExperimentalFlags } from "../experimental-flags"; import type { Logger } from "../logger"; @@ -61,7 +70,7 @@ export type MetadataCategory = | "Storage & databases" | "Networking & security"; -export type Command = `wrangler${string}`; +export type Command = SharedCommand; export type Metadata = CommandMetadata; export type ArgDefinition = Omit & @@ -133,125 +142,32 @@ export type HandlerContext = { export type CommandDefinition< NamedArgDefs extends NamedArgDefinitions = NamedArgDefinitions, -> = { - /** - * Descriptive information about the command which does not affect behaviour. - * This is used for the CLI --help and subcommand --help output. - * This should be used as the source-of-truth for status and ownership. - */ - metadata: Metadata; - /** - * Controls shared behaviour across all commands. - * This will allow wrangler commands to remain consistent and only diverge intentionally. - */ - behaviour?: { - /** - * By default, wrangler's version banner will be printed before the handler is executed. - * Set this value to `false` to skip printing the banner. - * - * @default true - */ - printBanner?: boolean | ((args: HandlerArgs) => boolean); - - /** - * Opt-in to printing a metrics banner for this command. - * @default false - */ +> = CommandDefinitionData< + Metadata, + NamedArgDefs, + CommandBehaviour> & { + /** Opt in to the metrics banner for this command. */ printMetricsBanner?: boolean; - - /** - * By default, wrangler will print warnings about the Wrangler configuration file. - * Set this value to `false` to skip printing these warnings. - */ - printConfigWarnings?: boolean; - /** - * By default, wrangler will read & provide the wrangler.toml/wrangler.json configuration. - * Set this value to `false` to skip this. - */ - provideConfig?: boolean; - - /** - * By default, wrangler will provide experimental flags in the handler context, - * according to the default values in register-yargs.command.ts - * Use this to override those defaults per command. + * Override experimental flags supplied to a command's handler context. */ overrideExperimentalFlags?: ( args: HandlerArgs ) => ExperimentalFlags; - - /** - * If true, then look for a redirect file at `.wrangler/deploy/config.json` and use that to find the Wrangler configuration file. - */ - useConfigRedirectIfAvailable?: boolean; - - /** - * If true, print a message about whether the command is operating on a local or remote resource - */ + /** Print whether this command operates on local or remote resources. */ printResourceLocation?: | ((args: HandlerArgs) => boolean) | boolean; - - /** - * If true, check for environments in the wrangler config, if there are some and the user hasn't specified an environment - * using the `-e|--env` cli flag, show a warning suggesting that one should instead be specified. - */ + /** Warn when environments are defined but none has been selected. */ warnIfMultipleEnvsConfiguredButNoneSpecified?: boolean; - /** - * Opt out of sending metrics for this command - * @default true - */ - sendMetrics?: boolean; - - /** - * After the command handler completes successfully, suggest installing - * Cloudflare skills for detected AI coding agents that don't have them. - * - * When set to `true`, the suggestion always runs after the handler. - * When set to a function, it receives the parsed args and should return - * `true` to enable the suggestion — use this to skip the prompt in modes - * where interactive output is inappropriate (e.g. `--json`). - * - * @default false - */ - suggestSkillsAfterHandler?: - | boolean - | ((args: HandlerArgs) => boolean); - - /** - * Whether this command can authenticate with a temporary preview account - * (via the hidden `--temporary` flag) when no real credentials are available. - * Only enable this for commands whose API calls are covered by the temporary - * preview-account deploy token (Workers, KV, D1, Hyperdrive, Queues, SSL/Certs). - * @default false - */ - supportTemporary?: boolean; - - /** - * By default, the banner will print "Active profile: " when a non-default - * profile is resolved. Set to `false` to suppress this line while still printing - * the rest of the banner. - * + * Suppress the active-profile banner while keeping the main banner. * @default true */ printActiveProfile?: boolean; - }; - - /** - * A plain key-value object describing the CLI args for this command. - * Shared args can be defined as another plain object and spread into this. - */ - args?: NamedArgDefs; - - /** - * Optionally declare some of the named args as positional args. - * The order of this array is the order they are expected in the command. - * Use args[key].demandOption and args[key].array to declare required and variadic - * positional args, respectively. - */ - positionalArgs?: Array>; - + }, + Array> +> & { /** * A hook to implement custom validation of the args before the handler is called. * Throw `CommandLineArgsError` with actionable error message if args are invalid. @@ -275,36 +191,19 @@ export type CommandDefinition< ) => void | Promise; }; -export type NamespaceDefinition = { - metadata: Metadata; -}; - -export type AliasDefinition = { - aliasOf: Command; - metadata?: Partial; -}; +export type NamespaceDefinition = CommandNamespaceData; +export type AliasDefinition = CommandAliasData; -export type InternalCommandDefinition = { - type: "command"; - command: Command; -} & CommandDefinition; - -export type InternalNamespaceDefinition = { - type: "namespace"; - command: Command; -} & NamespaceDefinition; - -export type InternalAliasDefinition = { - type: "alias"; - command: Command; -} & AliasDefinition; +export type InternalCommandDefinition = CommandIdentity<"command"> & + CommandDefinition; +export type InternalNamespaceDefinition = CommandIdentity<"namespace"> & + NamespaceDefinition; +export type InternalAliasDefinition = CommandIdentity<"alias"> & + AliasDefinition; export type InternalDefinition = | InternalCommandDefinition | InternalNamespaceDefinition | InternalAliasDefinition; -export type DefinitionTreeNode = { - definition?: InternalDefinition; - subtree: DefinitionTree; -}; +export type DefinitionTreeNode = CommandDefinitionTreeNode; export type DefinitionTree = Map; diff --git a/packages/wrangler/src/experimental-commands-api.ts b/packages/wrangler/src/experimental-commands-api.ts index 7d84b3cd733..f3e899b9d5e 100644 --- a/packages/wrangler/src/experimental-commands-api.ts +++ b/packages/wrangler/src/experimental-commands-api.ts @@ -1,5 +1,13 @@ import { createCLIParser } from "./index"; -import type { CommandMetadata } from "./command-metadata"; +import type { + CommandAliasData, + CommandBehaviour, + CommandDefinitionData, + CommandDefinitionTreeNode, + CommandIdentity, + CommandMetadata, + CommandNamespaceData, +} from "./command-types"; export type ExperimentalCommandMetadata = CommandMetadata; @@ -42,50 +50,39 @@ export type ExperimentalCommandArgDefinition = { type?: "array" | "count" | "boolean" | "number" | "string"; }; -export type ExperimentalCommandBehaviour = { - printBanner?: boolean | ((args: unknown) => boolean); - provideConfig?: boolean; - printConfigWarnings?: boolean; - useConfigRedirectIfAvailable?: boolean; - sendMetrics?: boolean; - supportTemporary?: boolean; - suggestSkillsAfterHandler?: boolean | ((args: unknown) => boolean); +/** + * Yargs's internal ArgDefinition is intentionally projected into public + * option metadata: Yargs is not a published Wrangler dependency. + */ +export type ExperimentalCommandBehaviour = CommandBehaviour & { [key: string]: unknown; }; -export type ExperimentalCommandCommandDefinition = { - type: "command"; - command: `wrangler${string}`; - metadata: ExperimentalCommandMetadata; - args?: Record; - behaviour?: ExperimentalCommandBehaviour; - positionalArgs?: string[]; - validateArgs?: (...args: unknown[]) => unknown; - handler?: (...args: unknown[]) => unknown; -}; +export type ExperimentalCommandCommandDefinition = CommandIdentity<"command"> & + CommandDefinitionData< + ExperimentalCommandMetadata, + Record, + ExperimentalCommandBehaviour, + string[] + > & { + validateArgs?: (...args: unknown[]) => unknown; + handler?: (...args: unknown[]) => unknown; + }; -export type ExperimentalCommandNamespaceDefinition = { - type: "namespace"; - command: `wrangler${string}`; - metadata: ExperimentalCommandMetadata; -}; +export type ExperimentalCommandNamespaceDefinition = + CommandIdentity<"namespace"> & + CommandNamespaceData; -export type ExperimentalCommandAliasDefinition = { - type: "alias"; - command: `wrangler${string}`; - aliasOf: `wrangler${string}`; - metadata?: Partial; -}; +export type ExperimentalCommandAliasDefinition = CommandIdentity<"alias"> & + CommandAliasData; export type ExperimentalCommandDefinition = | ExperimentalCommandCommandDefinition | ExperimentalCommandNamespaceDefinition | ExperimentalCommandAliasDefinition; -export type ExperimentalDefinitionTreeNode = { - definition?: ExperimentalCommandDefinition; - subtree: Map; -}; +export type ExperimentalDefinitionTreeNode = + CommandDefinitionTreeNode; export type ExperimentalGlobalFlags = Record< string, From 02f9a5ece928746e2ae573c2243c587abd71c0c7 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:00:36 +0100 Subject: [PATCH 8/8] docs(wrangler): restore supportTemporary JSDoc default tag --- packages/wrangler/src/command-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/command-types.ts b/packages/wrangler/src/command-types.ts index 5a3d32df228..85c66a0adac 100644 --- a/packages/wrangler/src/command-types.ts +++ b/packages/wrangler/src/command-types.ts @@ -79,7 +79,7 @@ export type CommandBehaviour = { * Whether this command can use a temporary preview account with the hidden * `--temporary` or `--event-code` flags. R2 and Containers commands work * only with event accounts created using `--event-code`. - * `default false + * @default false */ supportTemporary?: boolean; /** Offer Cloudflare skills after a successful handler invocation. */