Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cf-wrangler-build-delegate.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/fancy-crabs-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/deploy-helpers": patch
---

add `skipLastDeployedFromApiCheck` to override deploy source check
5 changes: 4 additions & 1 deletion packages/deploy-helpers/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
);
Expand Down
5 changes: 4 additions & 1 deletion packages/deploy-helpers/src/deploy/versions-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
);
Expand Down
2 changes: 2 additions & 0 deletions packages/deploy-helpers/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down
6 changes: 3 additions & 3 deletions packages/wrangler/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
183 changes: 100 additions & 83 deletions packages/wrangler/bin/cf-wrangler.js
Original file line number Diff line number Diff line change
@@ -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`);
Expand All @@ -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,
};
}
56 changes: 55 additions & 1 deletion packages/wrangler/src/__tests__/cf-wrangler/args.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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/);
});
});
});
Loading
Loading