Skip to content

server: workerd run, workerd check, and Wrangler configs for serve - #8065

Closed
bartlomieju wants to merge 2 commits into
cloudflare:mainfrom
bartlomieju:spike/workerd-run
Closed

bartlomieju wants to merge 2 commits into
cloudflare:mainfrom
bartlomieju:spike/workerd-run

Conversation

@bartlomieju

Copy link
Copy Markdown
Collaborator

Standalone workerd needs a Cap'n Proto config for anything, which is the
complaint self-hosters raise most often, and a config error ends the
process at the first problem. This adds a front end that synthesizes the
config in-process. workerd run app.js serves one JavaScript, TypeScript
or Python file with no config: files beside it are available to relative
imports, the compatibility date defaults to the newest this release
supports, and --watch reloads on edits. workerd serve wrangler.jsonc
reads a Wrangler config (main, compatibility date and flags, vars, Durable
Objects with migrations, service bindings, dev.port), persists Durable
Objects under .workerd-state, and reports the bindings standalone
workerd cannot provide. --print-config prints the synthesized config as
a schema file with embeds of the project files, so a project can
graduate to a hand-written config. workerd check accepts any config or a
bare source file, builds every service and compiles every Worker without
binding a socket, reports every error rather than the first, and exits 1
if there was one. Errors about an unknown service name add a "Did you
mean" hint.

workerd run app.js
workerd run app.ts --port 9000 --watch
workerd serve wrangler.jsonc
workerd serve wrangler.jsonc --print-config > config.capnp
workerd check config.capnp
workerd check wrangler.jsonc
workerd check app.js

$ workerd check config.capnp
Worker "main"'s binding "OTHER" refers to a service "mian", but no such
service is defined. Did you mean "main"?
Socket "http" refers to a service "mainn", but no such service is
defined. Did you mean "main"?
2 config errors found.

The config synthesis lives in the CLI crate (cli/project.rs); check is a
third command of the Rust server next to serve and test. The Wrangler
half overlaps with #8051, which also bundles with esbuild and rejects
unknown fields; whichever lands first, the other should be rebased so
run, check and --print-config share one Wrangler reader. wrangler.toml
and node_modules resolution are not covered here.

Standalone workerd needed a Cap'n Proto config for anything, which is the
most-cited complaint from people trying to self-host it. This adds a front
end that synthesizes the config in-process:

- `workerd run app.js` serves a single JavaScript, TypeScript or Python
  file with the newest compatibility date this release supports. Files
  beside it are available to relative imports. `--watch` reloads on
  change; `--port`, `--compat-date`, `--compat-flag` and `--persist`
  adjust it.
- `workerd serve wrangler.jsonc` (and `check`) reads a Wrangler config:
  main, compatibility date and flags, vars, durable_objects with
  migrations (SQLite classes), services and dev.port. Durable Objects are
  persisted under .workerd-state beside the config, since the in-memory
  storage has no SQLite. Sections that need Cloudflare services are
  reported and skipped.
- `--print-config` prints the synthesized config as a schema file with
  `embed`s of the project files, so a project can graduate to a hand
  written config.
- `workerd check <config>` starts every service without binding a port,
  reports every config error rather than exiting at the first, and exits
  1 if any. Worker compile errors (syntax errors) are reported too.
- Config errors about an unknown service now add a 'Did you mean' hint.

The CLI side is Rust (cli/project.rs); the C++ side adds Server::check()
and a check mode to the driver.
workerd check and the 'Did you mean' hint move from server.c++ and
cli-main.c++, which main deleted, to the Rust server: run::check and
entry::check build the service graph without binding sockets, and
config.rs adds the hint to both service lookups.
@bartlomieju
bartlomieju requested review from a team as code owners October 28, 2026 10:17
@clanky-the-clanker

Copy link
Copy Markdown

🛑 Significant concerns — One blocking bug: collect_modules embeds every sibling file, so any .py file under the project root makes runtime treat a JavaScript Worker as a Python Worker without the python_workers flag. There are also warnings about binding to all interfaces, the unbounded directory scan and the untested workerd check path.

Findings: 1 blocking · 4 warnings · 1 info · 1 suggestion

The decision is advisory: this comment never approves, blocks or requests changes on the pull request.

🔧 Fix with your agent

Paste the following into your coding agent to investigate and fix these issues.

Fix the following code review issues found in pull request #8065.

Before making changes, review each issue and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g., the fix would require disproportionate changes
  for minimal ROI, or the finding is factually incorrect), explain why
- If you need to clarify something before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order

## BLOCKING (1)

### [Code Quality] `collect_modules` embeds every module-like file under the project root. Any `.py` file sets `isPython` in workerd-api.c++:387-396, while `uses_python()` checks only `modules[0]` and never adds `python_workers`. A JavaScript Worker with a stray `.py` file beside it is then loaded as a Python Worker. `.ts`/`.d.ts` files likewise force TypeScript stripping and `--experimental`. A non-UTF-8 text-like file stops the run.
- **File:** src/workerd/server/cli/project.rs:897

## WARNING (4)

### [Security] The synthesized socket binds `*:{port}` for `workerd run` and `workerd serve wrangler.jsonc`, while the startup message prints `http://localhost:{port}`. A development Worker, with persisted Durable Object state, is exposed on every interface. Default to localhost.
- **File:** src/workerd/server/cli/project.rs:692

### [Code Quality] The new `workerd check` path (`run::check`, `entry::server_check`, the `Check` arm in lib.rs) has no tests: nothing covers the error counting, exit status 1 or the 'Config OK' path. `default_compatibility_date` and `civil_from_days` are untested too.
- **File:** src/workerd/server/server/run.rs

### [Performance] The recursive directory scan has no limit, and every file it matches is read into the synthesized config and watched under `--watch`. `workerd run` from a large directory pulls in build output, images and datasets.
- **File:** src/workerd/server/cli/project.rs:907

### [Documentation] The sample's `--print-config > config.capnp` writes the config outside the project directory, so its project-relative `embed` paths do not resolve.
- **File:** samples/wrangler-project/README.md:21

## INFO (1)

### [Code Quality] `ProjectArgs` options (`--port`, `--print-config`, ...) are silently ignored when `serve`/`check` gets a schema file. A warning would help.

## SUGGESTION (1)

### [Code Quality] `--persist` takes an optional value, so `workerd serve --persist wrangler.jsonc` reads the config path as the persist directory. Consider `require_equals = true`.
- **File:** src/workerd/server/cli/args.rs

📋 Code Quality: 🛑 1 blocking · 1 warning · 1 info · 1 suggestion

Module collection pulls in unrelated files, so a stray .py file breaks JavaScript Workers. workerd check is untested. There are two smaller usability points.

Recommendation: ⚠️ Major Comments

  • [BLOCKING] collect_modules embeds every module-like file under the project root. Any .py file sets isPython in workerd-api.c++:387-396, while uses_python() checks only modules[0] and never adds python_workers. A JavaScript Worker with a stray .py file beside it is then loaded as a Python Worker. .ts/.d.ts files likewise force TypeScript stripping and --experimental. A non-UTF-8 text-like file stops the run. (src/workerd/server/cli/project.rs:897)
  • [WARNING] The new workerd check path (run::check, entry::server_check, the Check arm in lib.rs) has no tests: nothing covers the error counting, exit status 1 or the 'Config OK' path. default_compatibility_date and civil_from_days are untested too. (src/workerd/server/server/run.rs)
  • [INFO] ProjectArgs options (--port, --print-config, ...) are silently ignored when serve/check gets a schema file. A warning would help.
  • [SUGGESTION] --persist takes an optional value, so workerd serve --persist wrangler.jsonc reads the config path as the persist directory. Consider require_equals = true. (src/workerd/server/cli/args.rs)
View full review

Module collection pulls in unrelated files (src/workerd/server/cli/project.rs:885-934)

  • collect_modules walks the whole tree under the entry's directory. It adds every file whose extension is in ModuleKind::from_extension as a module.
  • In src/workerd/server/workerd-api.c++:387-396, isPython becomes true if any module is a Python module. That registers the Python Worker modules.
  • uses_python() (project.rs:513-517) checks only modules[0], so the python_workers flag is not added.
  • As a result, a JavaScript entry next to scripts/*.py produces a Worker the runtime treats as Python, without the flag.
  • uses_typescript() has the opposite problem: it uses .any(...). A .ts or .d.ts file anywhere in the tree adds typescript_strip_types and forces --experimental for a plain JavaScript entry.
  • Any non-UTF-8 .md, .csv, .svg or .json file stops the whole run at project.rs:608.
  • Suggested fixes:
    • Collect only files compatible with the entry's language.
    • Decide the TypeScript and Python handling from the entry module.
    • Skip files that cannot be read, rather than failing.
    • Add tests for a .py file and a .d.ts file beside a JavaScript entry.

workerd check is untested

  • None of these has a test: run::check, entry::server_check, the Check arm in lib.rs, the error count and summary, exit status 1, and the "Config OK" path.
  • default_compatibility_date and civil_from_days have no tests either.

--persist consumes the following positional argument (src/workerd/server/cli/args.rs)

  • Because the option takes num_args = 0..=1, workerd serve --persist wrangler.jsonc reads the config path as the persist directory.
  • Consider require_equals = true.

Project options are silently ignored for schema-file configs

  • --port, --print-config and similar options passed with a .capnp config have no effect and print no warning.

🔒 Security: ⚠️ 1 warning

The synthesized development config listens on all interfaces, but the startup message says localhost.

Recommendation: 📝 Minor Comments

  • [WARNING] The synthesized socket binds *:{port} for workerd run and workerd serve wrangler.jsonc, while the startup message prints http://localhost:{port}. A development Worker, with persisted Durable Object state, is exposed on every interface. Default to localhost. (src/workerd/server/cli/project.rs:692)
View full review
  • src/workerd/server/cli/project.rs:692 sets the socket address to *:{port}, and the --print-config output uses the same address.
  • cli/lib.rs:331 announces http://localhost:{port}, so the user expects a local-only listener.
  • With zero-config workerd run, the Worker and any Durable Objects persisted to disk are reachable from the local network with no authentication.
  • Suggested fix: default to localhost:{port}, and add an explicit option to listen on other interfaces.

⚡ Performance: ⚠️ 1 warning

collect_modules scans the whole project directory with no limit, and to_config reads every matching file into memory.

Recommendation: 📝 Minor Comments

  • [WARNING] The recursive directory scan has no limit, and every file it matches is read into the synthesized config and watched under --watch. workerd run from a large directory pulls in build output, images and datasets. (src/workerd/server/cli/project.rs:907)
View full review
  • walk recurses through the entry's whole directory. It skips only node_modules, .git, .wrangler, .workerd and .workerd-state.
  • to_config then reads each file that matches, including images, data files and dist//build/ output, into the config message. Under --watch, each of those files is also watched.
  • Running workerd run app.js from a home directory or a repository root can make startup and memory use grow with the size of that whole tree.
  • Suggested fixes:
    • Cap the number of files and the total bytes.
    • Skip common build directories and hidden directories.
    • Prefer modules the entry can actually import.

📝 Documentation: ⚠️ 1 warning

In the sample README, the --print-config example saves the config where its relative embed paths cannot be resolved.

Recommendation: 💬 Optional Nits

  • [WARNING] The sample's --print-config > config.capnp writes the config outside the project directory, so its project-relative embed paths do not resolve. (samples/wrangler-project/README.md:21)
View full review
  • samples/wrangler-project/README.md:21 redirects the output to config.capnp in the repository root.
  • The embed paths in that output are relative to the project, for example src/index.js, so they will not resolve from the root.
  • Write the file to samples/wrangler-project/config.capnp, or run the command from the sample directory.

✅ All other checks passed

📖 AGENTS.md: ✅ LGTM

The src/workerd/server/AGENTS.md update is consistent with the change.


Reviewed commit: aa07d770

@bartlomieju

Copy link
Copy Markdown
Collaborator Author

Superseded now that #8051 has landed. The pieces are rebuilt on top of it: workerd check and the "Did you mean" hint in #8083, workerd run in #8084. The rest (the separate Wrangler reader, --persist/.workerd-state, services/dev.port, --print-config) is dropped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant