diff --git a/.changeset/cf-wrangler-build-delegate.md b/.changeset/cf-wrangler-build-delegate.md new file mode 100644 index 0000000000..ba848c1df5 --- /dev/null +++ b/.changeset/cf-wrangler-build-delegate.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Add `cf-wrangler build` delegate support + +The experimental `cf-wrangler` delegate binary now accepts `build` and emits the Build Output API directory through Wrangler's new-config build path. This lets parent tools invoke Wrangler's build-output implementation with `cf-wrangler build` instead of shelling out through the public Wrangler CLI. diff --git a/.changeset/fancy-crabs-occur.md b/.changeset/fancy-crabs-occur.md new file mode 100644 index 0000000000..3a7885be44 --- /dev/null +++ b/.changeset/fancy-crabs-occur.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/deploy-helpers": patch +--- + +add `skipLastDeployedFromApiCheck` to override deploy source check diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts index 8bcb66e339..ff2abb4d99 100644 --- a/packages/deploy-helpers/src/deploy/deploy.ts +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -260,7 +260,10 @@ export default async function deploy( return { versionId, workerTag }; } } - } else if (script.last_deployed_from === "api") { + } else if ( + script.last_deployed_from === "api" && + !props.skipLastDeployedFromApiCheck + ) { logger.warn( `You are about to publish a Workers Service that was last updated via the script API.\nEdits that have been made via the script API will be overridden by your local code and config.` ); diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts index af38688035..6ffcd766a0 100644 --- a/packages/deploy-helpers/src/deploy/versions-upload.ts +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -118,7 +118,10 @@ export default async function versionsUpload( workerTag, }; } - } else if (script.last_deployed_from === "api") { + } else if ( + script.last_deployed_from === "api" && + !props.skipLastDeployedFromApiCheck + ) { logger.warn( `You are about to upload a Workers Version that was last updated via the API.\nEdits that have been made via the API will be overridden by your local code and config.` ); diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index 4939a449c8..b390d32722 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -94,6 +94,8 @@ export type SharedDeployVersionsProps = { sendMetrics: boolean; /** Resolved from getFlag("RESOURCES_PROVISION"). Controls whether bindings are auto-provisioned before upload. */ resourcesProvision: boolean; + /** temporary hack - cf is not yet a recognised deploy source, so any deploys from cf comes back normalised to 'api'*/ + skipLastDeployedFromApiCheck: boolean; }; export type DeployProps = SharedDeployVersionsProps & { diff --git a/packages/wrangler/AGENTS.md b/packages/wrangler/AGENTS.md index 1b30a1d73f..d0cfa73d23 100644 --- a/packages/wrangler/AGENTS.md +++ b/packages/wrangler/AGENTS.md @@ -12,16 +12,16 @@ Main CLI for Cloudflare Workers. ~2k-line yargs command tree in `src/index.ts`. - `src/__tests__/` — Unit tests, helpers in `src/__tests__/helpers/` - `e2e/` — E2E tests, requires Cloudflare credentials - `bin/wrangler.js` — Shim that spawns Node with `--experimental-vm-modules` -- `bin/cf-wrangler.js` — `cf-wrangler` delegate entrypoint. Owns verb dispatch, argv parsing (`parseCfWranglerArgs`), and the `StartDevOptions` literal; hands off to `runCfWranglerDev` from `wrangler-dist/cli.js` in-process (no re-spawn — the parent tool owns the Node runtime) +- `bin/cf-wrangler.js` — `cf-wrangler` delegate entrypoint. Owns verb dispatch and argv parsing (`parseCfWranglerArgs`, `parseCfWranglerBuildArgs`); hands off to `runCfWranglerDev` / `runCfWranglerBuild` from `wrangler-dist/cli.js` in-process (no re-spawn — the parent tool owns the Node runtime) - `src/cf-wrangler/` — The `cf-wrangler` delegate entrypoint (see below) - `templates/` — Worker templates ## Entry Points -- `src/cli.ts` — Build entry AND library API surface (dual-purpose). Calls `main()` when run directly; re-exports `./api` when imported as library. Also re-exports `parseCfWranglerArgs`, `ArgParseError`, and `runCfWranglerDev` for the `cf-wrangler` bin to call in-process. +- `src/cli.ts` — Build entry AND library API surface (dual-purpose). Calls `main()` when run directly; re-exports `./api` when imported as library. Also re-exports `parseCfWranglerArgs`, `parseCfWranglerBuildArgs`, `ArgParseError`, `runCfWranglerDev`, and `runCfWranglerBuild` for the `cf-wrangler` bin to call in-process. - `src/index.ts` — Yargs CLI tree builder (large file). Exports `main()`. NOT the package entry point despite the name. - `src/api/index.ts` — Public programmatic API barrel. -- `src/cf-wrangler/` — The `cf-wrangler` delegate entrypoint, an experimental escape hatch for projects that can't use `@cloudflare/vite-plugin`. It sits directly on the internal `startDev` (the exact function `wrangler dev` runs) for byte-for-byte parity, exposing only `dev` + four flags (`--mode`, `--port`, `--host`, `--local`); the wrangler config file is found via wrangler's standard discovery (no `--config` flag). It is NOT a separate package and does NOT use the `unstable_dev` test harness. It shares its spawn contract (verb dispatch, flag vocabulary, exit-2 feature detection) with the sibling `cf-vite` delegate in `@cloudflare/vite-plugin`. `bin/cf-wrangler.js` owns verb dispatch, argv parsing, and the `StartDevOptions` literal; `src/cf-wrangler/dev.ts` (exported as `runCfWranglerDev`) wraps `startDev` in the experimental-flags context and waits for teardown. `src/cf-wrangler/args.ts` (exported as `parseCfWranglerArgs` / `ArgParseError`) does the strict argv parse. The "unknown subcommand" error doubles as a feature-detection signal for the parent CLI. +- `src/cf-wrangler/` — The `cf-wrangler` delegate entrypoint, an experimental escape hatch for projects that can't use `@cloudflare/vite-plugin`. It exposes `dev` + four flags (`--mode`, `--port`, `--host`, `--local`) and `build` + `--mode`; the wrangler config file is found via wrangler's standard discovery (no `--config` flag). It is NOT a separate package and does NOT use the `unstable_dev` test harness. It shares its spawn contract (verb dispatch, flag vocabulary, exit-2 feature detection) with the sibling `cf-vite` delegate in `@cloudflare/vite-plugin`. `bin/cf-wrangler.js` owns verb dispatch, argv parsing, and the `StartDevOptions` literal; `src/cf-wrangler/dev.ts` (exported as `runCfWranglerDev`) wraps `startDev` in the experimental-flags context and waits for teardown. `src/cf-wrangler/build.ts` (exported as `runCfWranglerBuild`) runs the Build Output API path used by `wrangler build --experimental-new-config --experimental-cf-build-output`, producing `.cloudflare/output/v0`. `src/cf-wrangler/args.ts` (exported as `parseCfWranglerArgs`, `parseCfWranglerBuildArgs`, and `ArgParseError`) does the strict argv parse. The "unknown subcommand" error doubles as a feature-detection signal for the parent CLI. ## Conventions (Wrangler-Specific) diff --git a/packages/wrangler/bin/cf-wrangler.js b/packages/wrangler/bin/cf-wrangler.js index 6351dd3101..b8f786c276 100755 --- a/packages/wrangler/bin/cf-wrangler.js +++ b/packages/wrangler/bin/cf-wrangler.js @@ -1,30 +1,44 @@ #!/usr/bin/env node -// `cf-wrangler` delegate binary. Runs wrangler's bundled dev server -// in-process; the parent tool owns the Node runtime (version, flags). +// `cf-wrangler` delegate binary. Runs wrangler delegate verbs in-process; +// the parent tool owns the Node runtime (version, flags). // -// Dispatches on the leading verb. Only `dev` exists today; an unknown -// or missing verb exits 2, which the parent uses to feature-detect -// support. +// Dispatches on the leading verb. An unknown or missing verb exits 2, which +// the parent uses to feature-detect support. const { ArgParseError, + parseCfWranglerBuildArgs, parseCfWranglerArgs, + runCfWranglerBuild, runCfWranglerDev, } = require("../wrangler-dist/cli.js"); const argv = process.argv.slice(2); const verb = argv[0]; -if (verb !== "dev") { +const verbHandlers = { + dev: { + parse: parseCfWranglerArgs, + run: runCfWranglerDevFromArgs, + }, + build: { + parse: parseCfWranglerBuildArgs, + run: runCfWranglerBuild, + }, +}; + +const verbHandler = verbHandlers[verb]; + +if (!verbHandler) { process.stderr.write( `Error: unknown subcommand "${verb ?? ""}".\n` + - `Usage: cf-wrangler dev [args]\n` + `Usage: cf-wrangler <${Object.keys(verbHandlers).join("|")}> [args]\n` ); process.exit(2); } let parsed; try { - parsed = parseCfWranglerArgs(argv.slice(1)); + parsed = verbHandler.parse(argv.slice(1)); } catch (err) { if (err instanceof ArgParseError) { process.stderr.write(`Error: ${err.message}\n`); @@ -33,83 +47,86 @@ try { throw err; } -// Build wrangler dev's full (yargs-derived) options object. Every field -// must be present; we set the four accepted flags plus `wrangler dev`'s -// defaults and leave everything else `undefined`, which makes wrangler's -// ConfigController resolve those exactly as `wrangler dev` would (config -// discovery, containers, inspector port, interactive hotkeys, ...). -// `--local` forces local execution; left unset it preserves per-resource -// `remote = true` bindings. There is no whole-worker remote dev (no -// `--remote`). -const options = { - _: [], - $0: "", - env: parsed.mode, - port: parsed.port, - host: parsed.host, - local: parsed.local, - remote: false, - latest: true, - noBundle: false, - testScheduled: false, - processEntrypoint: false, - experimentalAutoCreate: false, - types: false, - disableDevRegistry: false, - config: undefined, - script: undefined, - name: undefined, - accountId: undefined, - forceLocal: undefined, - compatibilityDate: undefined, - compatibilityFlags: undefined, - ip: undefined, - inspectorPort: undefined, - inspectorIp: undefined, - v: undefined, - cwd: undefined, - localProtocol: undefined, - httpsKeyPath: undefined, - httpsCertPath: undefined, - assets: undefined, - site: undefined, - siteInclude: undefined, - siteExclude: undefined, - persist: undefined, - persistTo: undefined, - routes: undefined, - localUpstream: undefined, - upstreamProtocol: undefined, - var: undefined, - define: undefined, - alias: undefined, - jsxFactory: undefined, - jsxFragment: undefined, - tsconfig: undefined, - minify: undefined, - legacyEnv: undefined, - logLevel: undefined, - showInteractiveDevSession: undefined, - liveReload: undefined, - bundle: undefined, - additionalModules: undefined, - enablePagesAssetsServiceBinding: undefined, - d1Databases: undefined, - experimentalProvision: undefined, - enableIpc: undefined, - nodeCompat: undefined, - enableContainers: undefined, - dockerPath: undefined, - containerEngine: undefined, - tunnel: undefined, - tunnelName: undefined, - envFile: undefined, - onReady: undefined, -}; - -runCfWranglerDev(options) +verbHandler + .run(parsed) .then((code) => process.exit(code)) .catch((err) => { process.stderr.write(`${(err && err.stack) || err}\n`); process.exit(1); }); + +function runCfWranglerDevFromArgs(parsedArgs) { + return runCfWranglerDev(createDevOptions(parsedArgs)); +} + +function createDevOptions(parsedArgs) { + // Build wrangler dev's full (yargs-derived) options object. Every field + // must be present; we set the four accepted flags plus `wrangler dev`'s + // defaults and leave everything else `undefined`, which makes wrangler's + // ConfigController resolve those exactly as `wrangler dev` would. + return { + _: [], + $0: "", + env: parsedArgs.mode, + port: parsedArgs.port, + host: parsedArgs.host, + local: parsedArgs.local, + remote: false, + latest: true, + noBundle: false, + testScheduled: false, + processEntrypoint: false, + experimentalAutoCreate: false, + types: false, + disableDevRegistry: false, + config: undefined, + script: undefined, + name: undefined, + accountId: undefined, + forceLocal: undefined, + compatibilityDate: undefined, + compatibilityFlags: undefined, + ip: undefined, + inspectorPort: undefined, + inspectorIp: undefined, + v: undefined, + cwd: undefined, + localProtocol: undefined, + httpsKeyPath: undefined, + httpsCertPath: undefined, + assets: undefined, + site: undefined, + siteInclude: undefined, + siteExclude: undefined, + persist: undefined, + persistTo: undefined, + routes: undefined, + localUpstream: undefined, + upstreamProtocol: undefined, + var: undefined, + define: undefined, + alias: undefined, + jsxFactory: undefined, + jsxFragment: undefined, + tsconfig: undefined, + minify: undefined, + legacyEnv: undefined, + logLevel: undefined, + showInteractiveDevSession: undefined, + liveReload: undefined, + bundle: undefined, + additionalModules: undefined, + enablePagesAssetsServiceBinding: undefined, + d1Databases: undefined, + experimentalProvision: undefined, + enableIpc: undefined, + nodeCompat: undefined, + enableContainers: undefined, + dockerPath: undefined, + containerEngine: undefined, + tunnel: undefined, + tunnelName: undefined, + envFile: undefined, + onReady: undefined, + }; +} diff --git a/packages/wrangler/src/__tests__/cf-wrangler/args.test.ts b/packages/wrangler/src/__tests__/cf-wrangler/args.test.ts index dc1f26f928..ef7c81c1d8 100644 --- a/packages/wrangler/src/__tests__/cf-wrangler/args.test.ts +++ b/packages/wrangler/src/__tests__/cf-wrangler/args.test.ts @@ -1,5 +1,9 @@ import { describe, it } from "vitest"; -import { ArgParseError, parseArgs } from "../../cf-wrangler/args"; +import { + ArgParseError, + parseArgs, + parseBuildArgs, +} from "../../cf-wrangler/args"; describe("cf-wrangler parseArgs", () => { describe("happy paths", () => { @@ -149,3 +153,53 @@ describe("cf-wrangler parseArgs", () => { }); }); }); + +describe("cf-wrangler parseBuildArgs", () => { + describe("happy paths", () => { + it("returns an empty object for no flags", ({ expect }) => { + expect(parseBuildArgs([])).toEqual({}); + }); + + it("parses --mode", ({ expect }) => { + expect(parseBuildArgs(["--mode", "production"])).toEqual({ + mode: "production", + }); + }); + + it("parses --mode=value", ({ expect }) => { + expect(parseBuildArgs(["--mode=production"])).toEqual({ + mode: "production", + }); + }); + }); + + describe("rejections", () => { + it("rejects --env (must use --mode)", ({ expect }) => { + expect(() => parseBuildArgs(["--env", "production"])).toThrow( + ArgParseError + ); + expect(() => parseBuildArgs(["--env", "production"])).toThrow( + /Unknown option/ + ); + }); + + it("rejects --config (config comes from wrangler's discovery)", ({ + expect, + }) => { + expect(() => parseBuildArgs(["--config", "wrangler.json"])).toThrow( + /Unknown option/ + ); + }); + + it("rejects unknown flags", ({ expect }) => { + expect(() => parseBuildArgs(["--definitely-not-a-flag"])).toThrow( + /Unknown option/ + ); + }); + + it("rejects positional arguments", ({ expect }) => { + expect(() => parseBuildArgs(["./worker.ts"])).toThrow(ArgParseError); + expect(() => parseBuildArgs(["./worker.ts"])).toThrow(/positional/); + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/cf-wrangler/build.test.ts b/packages/wrangler/src/__tests__/cf-wrangler/build.test.ts new file mode 100644 index 0000000000..04ce890036 --- /dev/null +++ b/packages/wrangler/src/__tests__/cf-wrangler/build.test.ts @@ -0,0 +1,62 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it, vi } from "vitest"; +import { runCfWranglerBuild } from "../../cf-wrangler/build"; +import { mockConsoleMethods } from "../helpers/mock-console"; + +vi.mock("@cloudflare/config", async (importOriginal) => { + const actual = (await importOriginal()) as Record; + + async function loadConfig(configPath: string) { + const source = await fs.promises.readFile(configPath, "utf8"); + const mod = (await import( + `data:text/javascript;base64,${Buffer.from(source).toString("base64")}` + )) as { default: unknown }; + return { + config: mod.default, + dependencies: new Set([path.resolve(configPath)]), + }; + } + + return { + ...actual, + loadConfig, + }; +}); + +describe("cf-wrangler build", () => { + runInTempDir(); + mockConsoleMethods(); + + it("emits the Build Output API tree", async ({ expect }) => { + await seed({ + "cloudflare.config.ts": `export default { + name: "cf-wrangler-build-worker", + compatibilityDate: "2026-05-18", + entrypoint: "./src/index.js", + };`, + "src/index.js": `export default { + async fetch() { return new Response("hello"); } + };`, + }); + + const exitCode = await runCfWranglerBuild({}); + + expect(exitCode).toBe(0); + expect( + fs.existsSync( + path.resolve( + ".cloudflare/output/v0/workers/cf-wrangler-build-worker/worker.config.json" + ) + ) + ).toBe(true); + expect( + fs.existsSync( + path.resolve( + ".cloudflare/output/v0/workers/cf-wrangler-build-worker/bundle/index.js" + ) + ) + ).toBe(true); + }); +}); diff --git a/packages/wrangler/src/cf-wrangler/args.ts b/packages/wrangler/src/cf-wrangler/args.ts index 57e318f63c..c2fe4a873b 100644 --- a/packages/wrangler/src/cf-wrangler/args.ts +++ b/packages/wrangler/src/cf-wrangler/args.ts @@ -1,7 +1,6 @@ /** - * Strict argv parser for `cf-wrangler dev`. Only four flags are accepted; - * unknown flags throw. The config file comes from wrangler's standard - * discovery, not a flag. + * Strict argv parsers for `cf-wrangler` delegate verbs. The config file comes + * from wrangler's standard discovery, not a flag. */ import { parseArgs as nodeParseArgs } from "node:util"; @@ -12,6 +11,10 @@ export interface DevArgs { local?: boolean; // force local even if a resource sets `remote = true` } +export interface BuildArgs { + mode?: string; // maps to wrangler's `env` (named environment) +} + export class ArgParseError extends Error { constructor(message: string) { super(message); @@ -20,6 +23,10 @@ export class ArgParseError extends Error { } export function parseArgs(argv: string[]): DevArgs { + return parseDevArgs(argv); +} + +export function parseDevArgs(argv: string[]): DevArgs { let parsed; try { parsed = nodeParseArgs({ @@ -64,3 +71,26 @@ export function parseArgs(argv: string[]): DevArgs { return out; } + +export function parseBuildArgs(argv: string[]): BuildArgs { + let parsed; + try { + parsed = nodeParseArgs({ + args: argv, + options: { + mode: { type: "string" }, + }, + strict: true, + allowPositionals: false, + }); + } catch (err) { + throw new ArgParseError(err instanceof Error ? err.message : String(err)); + } + + const out: BuildArgs = {}; + if (parsed.values.mode !== undefined) { + out.mode = parsed.values.mode; + } + + return out; +} diff --git a/packages/wrangler/src/cf-wrangler/build.ts b/packages/wrangler/src/cf-wrangler/build.ts new file mode 100644 index 0000000000..7f42f6e0d7 --- /dev/null +++ b/packages/wrangler/src/cf-wrangler/build.ts @@ -0,0 +1,13 @@ +/** + * `build` verb runtime for the `cf-wrangler` delegate entrypoint. + * + * Runs the same Build Output API path as + * `wrangler build --experimental-new-config --experimental-cf-build-output`. + */ +import { runBuildOutput } from "../build/run-build-output"; +import type { BuildArgs } from "./args"; + +export async function runCfWranglerBuild(args: BuildArgs): Promise { + await runBuildOutput({ env: args.mode }); + return 0; +} diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index 2f5f25afe2..ba6d9fb3dc 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -104,9 +104,11 @@ export { resolveNamedTunnel as unstable_resolveNamedTunnel } from "./tunnel/clie // Entries for the `cf-wrangler` delegate binary (see `bin/cf-wrangler.js`), // which calls these in-process. Not a stable public API. +export { runCfWranglerBuild } from "./cf-wrangler/build"; export { runCfWranglerDev } from "./cf-wrangler/dev"; export { ArgParseError, + parseBuildArgs as parseCfWranglerBuildArgs, parseArgs as parseCfWranglerArgs, } from "./cf-wrangler/args"; diff --git a/packages/wrangler/src/deployment-bundle/merge-config-args.ts b/packages/wrangler/src/deployment-bundle/merge-config-args.ts index 7249dfd64e..ea957a5199 100644 --- a/packages/wrangler/src/deployment-bundle/merge-config-args.ts +++ b/packages/wrangler/src/deployment-bundle/merge-config-args.ts @@ -95,6 +95,7 @@ async function mergeSharedConfigArgs( accountId, sendMetrics, resourcesProvision: getFlag("RESOURCES_PROVISION") ?? false, + skipLastDeployedFromApiCheck: false, }; const buildProps: BuildProps = {