Skip to content
Closed
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ To serve your config, do:

For more details about command-line usage, use `workerd --help`.

#### Running a file or a Wrangler project without a config

A single source file can be served with no config file at all:

```
workerd run app.js # http://localhost:8080, newest compatibility date, nodejs_compat
workerd run app.ts # TypeScript, with type stripping (experimental)
workerd run app.js --watch # reload when the file or its imports change
```

The file is the Worker's entry module, and every `.js`, `.mjs`, `.cjs`, `.ts`, `.json`, `.wasm`,
`.txt` and `.html` file in the same directory tree is available to relative imports
(`node_modules` is skipped, so bundle dependencies first).

A project with a `wrangler.json` / `wrangler.jsonc` can be served the same way:

```
workerd serve wrangler.jsonc
```

`main`, `compatibility_date`, `compatibility_flags`, `vars`, `durable_objects` (with
`migrations`), `services` and `dev.port` are honoured. Durable Objects are backed by SQLite and
persisted under `.workerd-state` next to the config (`--persist <dir>` chooses another place).
Bindings that need Cloudflare services (KV, R2, D1, Queues, ...) are reported and skipped.

`--print-config` prints the config `workerd` synthesized, as a schema file with `embed`s of your
project files. Save it and serve it with `workerd serve` when you outgrow what the shortcut
offers.

To validate a config without serving it, which reports every problem rather than stopping at the
first and never binds a port, do:

```
workerd check my-config.capnp # or wrangler.jsonc, or app.js
```

Prebuilt binaries are distributed via `npm`. Run `npx workerd ...` to use these. If you're running a prebuilt binary, you'll need to make sure your system has the right dependencies installed:

* On Linux:
Expand Down
1 change: 1 addition & 0 deletions samples/wrangler-project/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.workerd-state/
28 changes: 28 additions & 0 deletions samples/wrangler-project/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Serving a Wrangler project directly

This is an ordinary Wrangler project: a `wrangler.jsonc` with a Durable Object class (SQLite
backed), plain `vars`, a service binding to itself, and a KV namespace. It needs no `workerd`
config file:

```
workerd serve samples/wrangler-project/wrangler.jsonc
curl http://localhost:8787/room-a
curl http://localhost:8787/room-a
curl http://localhost:8787/self
```

The counter survives restarts: Durable Object storage is persisted under
`samples/wrangler-project/.workerd-state`. The KV namespace has no standalone implementation, so
`workerd` reports it and leaves that binding out.

To see (and keep) the config `workerd` synthesized:

```
workerd serve samples/wrangler-project/wrangler.jsonc --print-config > config.capnp
```

To validate the project without serving it, which reports every error at once:

```
workerd check samples/wrangler-project/wrangler.jsonc
```
26 changes: 26 additions & 0 deletions samples/wrangler-project/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec("CREATE TABLE IF NOT EXISTS hits (n INTEGER)");
}
async increment() {
this.sql.exec("INSERT INTO hits VALUES (1)");
return this.sql.exec("SELECT COUNT(*) AS n FROM hits").one().n;
}
}

export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/self") {
const r = await env.SELF.fetch("http://x/");
return new Response("via service binding: " + (await r.text()));
}
const id = env.COUNTER.idFromName(url.pathname);
const n = await env.COUNTER.get(id).increment();
return new Response(`${env.GREETING} (max ${env.LIMITS.max}): ${url.pathname} hit ${n} times\n`);
}
};
14 changes: 14 additions & 0 deletions samples/wrangler-project/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// A Worker with a Durable Object, exactly as wrangler would deploy it.
"name": "counter",
"main": "src/index.js",
"compatibility_date": "2026-09-01",
"compatibility_flags": ["nodejs_compat"],
"vars": { "GREETING": "Counter service", "LIMITS": { "max": 100 } },
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }],
"kv_namespaces": [{ "binding": "CACHE", "id": "0000" }],
"services": [{ "binding": "SELF", "service": "counter" }],
}
2 changes: 1 addition & 1 deletion src/workerd/server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

The `workerd` binary and its server. `:workerd` (`workerd.rs`, this package) is a Rust binary over three crates in this directory:

- `cli/` -- the `workerd-cli` crate (declared in this package's `BUILD.bazel`, sources globbed from `cli/`): parses the command line (clap), produces the encoded config (schema files are compiled by this package's `config-compiler.c++`), handles `compile`, `--watch` and re-exec, and hands `serve` and `test` (which `fuzzilli` and `make-pyodide-baseline-snapshot` are built on) to the server crate's `entry` module.
- `cli/` -- the `workerd-cli` crate (declared in this package's `BUILD.bazel`, sources globbed from `cli/`): parses the command line (clap), produces the encoded config (schema files are compiled by this package's `config-compiler.c++`), handles `compile`, `--watch` and re-exec, synthesizes a config from a source file or a Wrangler config (`project.rs`: `run`, `serve wrangler.jsonc`, `check`, `--print-config`), and hands `serve`, `check` and `test` (which `fuzzilli` and `make-pyodide-baseline-snapshot` are built on) to the server crate's `entry` module.
- `server/` -- the `workerd-server` crate: the single-tenant server. It interprets the config, builds the service graph, binds the sockets, runs the listeners, actors and dynamic workers, drains on SIGTERM, and runs `workerd test`. `Error` is `cxx::KjError`; `bridge.rs` is the one module that allows `unsafe`.
- `factory/` -- C++: `WorkerFactory` (`worker-factory*`), everything that needs the isolate, driven by the server through the cxx bridge in `server/bridge.rs`; and `bootstrap.{h,c++}`, the C++ half of a command. `with_process_context` sets up the process's logging (the JSON logger, when the config asks for structured logging) on the C++ stack around the Rust command; `bootstrap` sets up perfetto, autogates and V8 on the Rust-owned event loop and builds the `WorkerFactory`, which owns them, from the encoded config and the command-line options for `server/entry.rs`, which owns the loop (`kj_rs_tokio::Runtime`) and blocks on `server_serve` / `server_test`.

Expand Down
1 change: 1 addition & 0 deletions src/workerd/server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ wd_rust_crate(
"//src/workerd/server/server:workerd-server",
"@crates_vendor//:capnp",
"@crates_vendor//:clap",
"@crates_vendor//:serde_json",
"@crates_vendor//:socket2",
"@crates_vendor//:tokio",
] + select({
Expand Down
41 changes: 41 additions & 0 deletions src/workerd/server/cli/args-test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn serve_options() {
const_name,
serve_or_test,
serve,
..
} = parse(&[
"serve",
"--binary",
Expand Down Expand Up @@ -164,3 +165,43 @@ fn compiled_binary_has_no_config_arguments() {
assert_eq!(main.serve.socket_addr_overrides.len(), 1);
assert!(CompiledMain::try_parse_from(["workerd", "config.capnp"]).is_err());
}

#[test]
fn run_invocation() {
let Command::Run {
file,
project,
serve_or_test,
..
} = parse(&[
"run",
"app.ts",
"--port=9000",
"--compat-flag=foo",
"--compat-flag=bar",
"--persist",
"--watch",
])
.command
else {
panic!("expected run")
};
assert_eq!(file, "app.ts");
assert_eq!(project.port, Some(9000));
assert_eq!(project.compatibility_flags, ["foo", "bar"]);
assert_eq!(project.persist.as_deref(), Some(".workerd-state"));
assert!(!project.print_config);
assert!(serve_or_test.watch);
}

#[test]
fn check_invocation() {
let Command::Check {
config, project, ..
} = parse(&["check", "wrangler.jsonc", "--print-config"]).command
else {
panic!("expected check")
};
assert_eq!(config.config_file, "wrangler.jsonc");
assert!(project.print_config);
}
123 changes: 122 additions & 1 deletion src/workerd/server/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ pub enum Command {
/// run the server
#[command(
long_about = "Serve requests based on a config.",
after_long_help = "Serves requests based on the configuration specified in <config-file>."
after_long_help = "Serves requests based on the configuration specified in <config-file>: \
a Cap'n Proto schema file, an encoded config (--binary), or a Wrangler config \
(wrangler.json / wrangler.jsonc), from which a config is synthesized."
)]
Serve {
#[command(flatten)]
Expand All @@ -62,13 +64,61 @@ pub enum Command {
#[command(flatten)]
const_name: ConstNameArg,

#[command(flatten)]
project: ProjectArgs,

#[command(flatten)]
serve_or_test: ServeOrTestArgs,

#[command(flatten)]
serve: ServeArgs,
},

/// run a single source file as a Worker, no config needed
#[command(
long_about = "Runs one JavaScript, TypeScript or Python file as a Worker, with no config \
file.",
after_long_help = RUN_HELP,
)]
Run {
/// The Worker's entry module: a .js, .mjs, .cjs, .ts or .py file. Files beside it are
/// available to relative imports.
#[arg(value_name = "file")]
file: String,

#[command(flatten)]
project: ProjectArgs,

#[command(flatten)]
serve_or_test: ServeOrTestArgs,

#[command(flatten)]
serve: ServeArgs,
},

/// check a config and report every problem, without serving
#[command(
long_about = "Loads a config and reports every problem with it, without serving.",
after_long_help = "Accepts the same config files as `serve`: a Cap'n Proto schema file, \
an encoded config (--binary), a Wrangler config (wrangler.json / wrangler.jsonc) or a \
single source file as for `run`. \
Every service is constructed and every Worker compiled, so errors in a Worker's top \
level are reported too. Exits with status 1 if any error was found."
)]
Check {
#[command(flatten)]
config: ConfigFileArgs,

#[command(flatten)]
const_name: ConstNameArg,

#[command(flatten)]
project: ProjectArgs,

#[command(flatten)]
serve_or_test: ServeOrTestArgs,
},

/// create a self-contained binary
#[command(
long_about = "Builds a self-contained binary from a config.",
Expand Down Expand Up @@ -136,6 +186,21 @@ pub enum Command {
},
}

const RUN_HELP: &str = "\
Serves <file> as a Worker on http://localhost:8080 (see --port). The file is the Worker's entry \
module; every .js, .mjs, .cjs, .ts, .json, .wasm, .txt and .html file in the same directory tree \
is available to relative imports (node_modules is skipped: bundle dependencies first). The Worker \
gets the newest compatibility date this workerd release supports and nodejs_compat. Example:
export default {
async fetch(request, env) {
return new Response('Hello from workerd');
}
}
A .ts entry module enables TypeScript type stripping (experimental). A .py entry module runs as a \
Python Worker. --print-config prints the synthesized config as a schema file, which can be saved \
next to the code and served with `workerd serve`.
";

const TEST_HELP: &str = "\
Runs tests for services defined in <config-file>. <filter>, if given, specifies exactly which \
tests to run. It has one of the following formats:
Expand Down Expand Up @@ -187,6 +252,62 @@ pub struct ConfigFileArgs {
pub config_file: String,
}

/// Options for a config synthesized from a project: `run`, or `serve` / `check` of a Wrangler
/// config. Ignored for a schema-file config.
#[derive(Args, Debug, Default, Clone, PartialEq, Eq)]
pub struct ProjectArgs {
/// Listen on <port> (default: 8080 for `run`; a Wrangler config's dev.port, or 8787).
#[arg(long, value_name = "port", help_heading = "Project options")]
pub port: Option<u16>,

/// The Worker's compatibility date (default: the Wrangler config's, or this workerd release's
/// date).
#[arg(
long = "compat-date",
value_name = "date",
help_heading = "Project options"
)]
pub compatibility_date: Option<String>,

/// Add a compatibility flag. May be repeated.
#[arg(
long = "compat-flag",
value_name = "flag",
help_heading = "Project options"
)]
pub compatibility_flags: Vec<String>,

/// Persist Durable Object storage to <dir> (default: .workerd-state) instead of memory.
#[arg(
long,
value_name = "dir",
num_args = 0..=1,
default_missing_value = ".workerd-state",
help_heading = "Project options"
)]
pub persist: Option<String>,

/// The Worker's name (default: the Wrangler config's `name`, or the file's stem).
#[arg(long, value_name = "name", help_heading = "Project options")]
pub name: Option<String>,

/// Print the synthesized config as a Cap'n Proto schema file and exit, instead of running it.
#[arg(long, help_heading = "Project options")]
pub print_config: bool,
}

impl From<ProjectArgs> for crate::project::Overrides {
fn from(args: ProjectArgs) -> Self {
Self {
name: args.name,
port: args.port,
compatibility_date: args.compatibility_date,
compatibility_flags: args.compatibility_flags,
persist: args.persist.map(std::path::PathBuf::from),
}
}
}

#[derive(Args, Debug)]
pub struct ConstNameArg {
#[arg(value_name = "const-name")]
Expand Down
Loading
Loading