Skip to content

How deploy decides what to build

jiayang deploy looks at the files in a directory and decides what kind of app it is: a static site, a Worker or a container. It reads files only, runs the rules in a fixed order, and the first rule that matches decides.

jiayang detect runs the same rules and stops. It builds nothing, uploads nothing and needs no sign-in.

Terminal window
cd my-site
jiayang detect
a static site, served by Workers (Vite)
because package.json depends on vite, builds with `npm run build`, deploys what the build writes
install: npm ci
build: npm run build

The first line is the kind, with the framework it recognised in brackets. The because line lists every signal that decided it, in the order the rules checked them. The lines after it say what the deploy will run. A container’s port: line only shows when jiayang.json sets a port.

jiayang deploy --dry-run prints the same thing and stops:

Terminal window
jiayang deploy acme/api --dry-run
a container (Express)
because package.json depends on express, runs as a node container
start: npm start
Nothing deployed: --dry-run.

start: is what the container runs: the command the Dockerfile jiayang writes starts, or the Dockerfile's CMD when the project has a Dockerfile of its own.

Add --json to either command for the same answer as JSON, with kind, driver, framework, reasons, build, worker and container fields.

Before any rule runs, --kind on the command line decides, and then kind in jiayang.json. Otherwise the first match below wins.

  1. A Dockerfile and a wrangler config. It stops and asks which you meant.
  2. A Dockerfile. A container, built from your Dockerfile.
  3. A wrangler config (wrangler.jsonc, wrangler.json or wrangler.toml). A Worker, built with wrangler.
  4. A Cloudflare adapter in package.json. A Worker, whatever else the project depends on.
  5. A static site builder in package.json, and no Node server. A static site: the files the build writes.
  6. A Node server in package.json. A container.
  7. A start script in package.json. A container.
  8. requirements.txt, pyproject.toml or Pipfile. A Python container.
  9. go.mod. A Go container.
  10. Cargo.toml. A Rust container.
  11. index.html and no package.json. A static site, served as it is.
  12. An entry file: index.js, index.mjs, src/index.js, src/index.mjs, dist/index.js or dist/index.mjs. A Worker, uploaded as it is, or built with wrangler when it imports a package by name.
  13. A TypeScript entry: src/index.ts, index.ts, src/worker.ts or worker.ts. A Worker, built with wrangler.

Between rules 4 and 5, SvelteKit (@sveltejs/kit) with none of @sveltejs/adapter-cloudflare, @sveltejs/adapter-cloudflare-workers, @sveltejs/adapter-node or @sveltejs/adapter-static stops the deploy. sv create gives a project @sveltejs/adapter-auto, which builds only for hosts it recognises and writes nothing to serve here:

error: this is SvelteKit with @sveltejs/adapter-auto, which only builds for hosts it recognises, and this isn't one. Give it an adapter the platform runs, then deploy again:
as a Worker: npx sv add sveltekit-adapter="adapter:cloudflare+cfTarget:workers" && npm run gen
as a container: npx sv add sveltekit-adapter="adapter:node"
The first writes the wrangler.jsonc the build needs, and `npm run gen` the types its build checks.

A package counts if it’s in dependencies or devDependencies.

Rule Packages
Cloudflare adapter @opennextjs/cloudflare, @astrojs/cloudflare, @sveltejs/adapter-cloudflare, @sveltejs/adapter-cloudflare-workers, @react-router/cloudflare, @cloudflare/vite-plugin, nitro-cloudflare-dev, @remix-run/cloudflare, wrangler, hono, @hono/vite-build
Static site builder @docusaurus/core, vitepress, @11ty/eleventy, react-scripts, astro, vite, parcel
Node server express, fastify, koa, @nestjs/core, next, nuxt, @sveltejs/adapter-node, @react-router/node, @hapi/hapi

The same framework can land in different places. Next.js with @opennextjs/cloudflare is a Worker, and Next.js without it is a container. Workers and frameworks has the full table.

A Python project’s framework shows in brackets when its dependency files name fastapi, flask, django, streamlit or gradio. That choice decides the start command.

The deploy stops and lists what it looked for:

error: nothing here says what this is. Looked for:
- a Dockerfile
- a wrangler config (wrangler.jsonc, wrangler.json, wrangler.toml)
- package.json with a Cloudflare adapter, a static builder or a server
- requirements.txt, pyproject.toml or Pipfile
- go.mod
- Cargo.toml
- index.html
- an entry point (index.js, index.mjs, src/index.js, src/index.mjs, dist/index.js, dist/index.mjs, src/index.ts, index.ts, src/worker.ts, worker.ts)
Say which it is with --kind, or write it in jiayang.json.

A directory with both a Dockerfile and a wrangler config gets a question instead of a guess:

error: this has both a Dockerfile and wrangler.json, so it could be either kind. Say which with --kind=container or --kind=worker.

Pass --kind for one deploy:

Terminal window
jiayang deploy acme/api ./api --kind container

--kind takes static, worker or container. To keep the answer with the project, write it in jiayang.json instead:

{ "version": 1, "kind": "container" }

With either, the reason reads you said it's a container. jiayang.json covers the rest of the file.

Once the kind is settled, jiayang deploy runs the project’s own build on your machine, the way you would:

  1. Install, when there’s a build to run and the directory has a package.json but no node_modules. The install honours your lockfile and never changes it: npm ci for package-lock.json, pnpm install --frozen-lockfile for pnpm-lock.yaml, yarn install --immutable for yarn.lock, and bun install --frozen-lockfile for bun.lock or bun.lockb. With no lockfile, nothing is installed. The build still runs, and if it fails while package.json names packages that aren’t in node_modules here or in a directory above, the error ends with:

    error: package.json names packages that aren't installed, and there's no lockfile to install them from. Run `npm install` (or your package manager's install), then deploy again.
  2. Build, with npm run build when package.json has a build script. A Next.js app with OpenNext runs the adapter’s own build instead (details).

  3. Check what goes up, then upload it.

A container builds inside its image, so nothing runs on your machine before docker build unless jiayang.json names a build.command.

The build runs with CI=1 and with the app’s environment variables, because a site bakes some of them in. CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_BASE_URL, CF_API_TOKEN, CF_API_KEY and CF_ACCOUNT_ID are removed from its environment, so a build can’t reach your own Cloudflare account.

What the build prints goes to stderr. With --json, stdout carries only the result.

If a step fails, nothing is uploaded and nothing is created:

error: `npm run build` failed. Fix it where you'd normally run it, then deploy again.

The same goes for a failure after the build. If the upload fails, or a container’s image doesn’t build, an app that --create made for this deploy is deleted again.

--no-build skips the install and the build, and deploys what’s already in the directory.

A workspace keeps one lockfile and one node_modules at its root. When the app’s directory has no lockfile of its own, jiayang deploy looks up to eight directories above it for a root that declares a workspace (a pnpm-workspace.yaml, or a package.json with workspaces) and has a lockfile. It stops at the first directory holding .git. The install runs there, and jiayang detect says so:

because package.json depends on vite, builds with `npm run build`, deploys what the build writes, installs from the workspace at /Users/you/code/shop

Before anything is sent, jiayang deploy looks for credentials in what would go up: a site’s files, a Worker’s modules and a container’s build context. A file named like one (.env, .env.production, *.pem, *.key, *.p12, *.pfx, .dev.vars, id_rsa, id_ed25519, credentials.json) stops the deploy. .env.example, .env.sample, .env.template and .env.dist are allowed by name.

A site’s files are also checked for a key in their contents, such as a Stripe or GitHub token. So are those example files in a site or a container’s build context.

A Worker’s code is checked for keys by the platform, after it’s uploaded, and a key there refuses the deploy. With --create, the app is made before the upload, so a refused deploy leaves it behind with nothing deployed. Run the same command once the key is out, and it deploys to that app.

Keys belong in secrets, which your code never holds.

A static site and a Worker both run on Workers. A container runs on Containers. --create makes the app with whichever the directory turns out to be, and an app can’t move from one to the other later:

error: acme/api runs on Workers, but . is containers. An app can't change what runs it: make a new one, or deploy something of the same kind.

A static site can become a Worker, and a Worker a static site, because both run on Workers.