Skip to content
Moku LabsDocs
@moku-labs/web pages

deploy

Node-only. Ships the built site to Cloudflare Pages through wrangler, and scaffolds wrangler.jsonc and a CI workflow.

deploy.mdllms.txt

What it does

Node-only. deploy ships outDir to Cloudflare Pages by running wrangler pages deploy in a subprocess. It also writes the local scaffolding: wrangler.jsonc, and on request a GitHub Actions workflow. Nothing happens until you call it: app.deploy.run() or app.deploy.init().

How it works

The Pages project name is not config. It is site.name() as a slug: My Cool Blog! deploys to my-cool-blog. run() and init() both use it, so the project and the scaffold cannot drift.

Every run() goes through the same checks, cheap first, and stops at the first failure:

  1. Preflight. wrangler.jsonc exists, outDir exists and is not empty, at most 20,000 files, no file over 25 MiB.
  2. Guards. outDir resolves inside the project. The branch matches /^[a-zA-Z0-9/_.-]+$/, so it cannot be read as a flag.
  3. Token. CLOUDFLARE_API_TOKEN is read with ctx.env.require and passed to the subprocess environment only: never in the arguments, never logged.
  4. Deploy. bunx wrangler pages deploy <outDir> --project-name <slug> --branch <branch>, spawned with an argument list, no shell.
  5. Report. All subprocess output is scrubbed before it is logged: long, high-entropy tokens become ***. On success the result is stored and deploy:complete is emitted.

A failure throws an Error with a code: ERR_DEPLOY_NO_WRANGLER_CONFIG, ERR_DEPLOY_EMPTY_OUTDIR, ERR_DEPLOY_TOO_MANY_FILES, ERR_DEPLOY_FILE_TOO_LARGE, ERR_DEPLOY_PATH_TRAVERSAL, ERR_DEPLOY_INVALID_BRANCH, ERR_DEPLOY_NO_TOKEN, ERR_DEPLOY_PROJECT_NOT_FOUND, ERR_DEPLOY_AUTH_EXPIRED, ERR_DEPLOY_AUTH, ERR_DEPLOY_NETWORK, ERR_DEPLOY_WRANGLER_FAILED or ERR_DEPLOY_CONFIG.

Config

pluginConfigs.deploy. Every field has a default.

OptionTypeDefaultDescription
target"cloudflare-pages""cloudflare-pages"The only target in 2.4.11.
outDirstring"dist"The built site, relative to the project root. Checked again on every run().
productionBranchstring"main"The branch run() deploys when you pass none.
scrubAllowliststring[]["CLOUDFLARE_ACCOUNT_ID"]Substrings that are never masked in logged output.
compatibilityDatestring"2024-01-01"Written into the generated wrangler.jsonc. YYYY-MM-DD.
cibooleanfalseWhether init() also writes .github/workflows/deploy.yml.

createApp fails with ERR_DEPLOY_CONFIG on an unknown target, an empty outDir, a scrubAllowlist that is not a list of strings or a malformed compatibilityDate. The file cap can be raised toward the paid-plan limit with the MOKU_DEPLOY_MAX_FILES environment variable.

API

Mounted at app.deploy, reachable with ctx.require(deployPlugin).

MethodSignatureDescription
run(options?: { branch?: string }) => Promise<DeployResult>Deploys outDir. Resolves to { url, deploymentId, branch, durationMs }.
init(options?: { ci?, check?, workflowTrigger? }) => Promise<InitResult>Writes wrangler.jsonc (never overwrites it) and, with ci, the workflow. check: true writes nothing and reports drift. Resolves to { written, skipped, drifted }.
getLastDeployment() => Readonly<DeployResult> | nullThe last successful deploy of this process, or null.
projectName() => stringThe Pages project slug, from site.name().
createProject() => Promise<{ name, branch }>Runs wrangler pages project create for the slug and the production branch. Fails when the project exists.

workflowTrigger is "auto" (push to main, the default), "versioned-tag" (a v* tag) or "dispatch" (the manual button only). The generated workflow pins every action to a commit SHA and reads the secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.

Events

Emits

EventPayloadWhen
deploy:complete{ url: string; deploymentId: string; branch: string; durationMs: number }Once, after a successful deploy. A failed one emits nothing.

Listens to

None.

Depends on

  • site: name(), for the project slug.
  • The core env plugin: ctx.env.require("CLOUDFLARE_API_TOKEN"). Wire Node env providers in pluginConfigs.env, for example [processEnv(), dotenv(".env")].

Example

scripts/deploy.ts
import { createApp, deployPlugin, dotenv, processEnv } from "@moku-labs/web";

const app = createApp({
  plugins: [deployPlugin],
  pluginConfigs: {
    site: { name: "My Cool Blog!", url: "https://blog.dev", author: "Ada", description: "Notes." },
    deploy: { outDir: "dist", productionBranch: "main" },
    env: { providers: [processEnv(), dotenv(".env")] }
  }
});

console.log(app.deploy.projectName()); // "my-cool-blog"

const scaffold = await app.deploy.init({ ci: true, workflowTrigger: "versioned-tag" });
console.log(scaffold.written); // ["wrangler.jsonc", ".github/workflows/deploy.yml"] on the first run

const result = await app.deploy.run({ branch: "preview/landing" });
console.log(result.url);
  1. Run it once. Later runs skip files that exist.
  2. Ships whatever is in dist/. Build first.
deploy
bun scripts/deploy.ts
my-cool-blog
[ "wrangler.jsonc", ".github/workflows/deploy.yml" ]
https://….pages.dev

The URL is the one wrangler printed for the deployment.

  • deploy does not build. Run app.build.run() or app.cli.build() first.
  • init() does not create the Pages project. It only writes local files. The first deploy to a new project fails with ERR_DEPLOY_PROJECT_NOT_FOUND: create it in the dashboard, with createProject(), or let the cli wizard do it.
  • The token comes through env. A token in your shell is invisible to the plugin until an env provider such as processEnv() is composed.
  • Renaming the site renames the project. The slug follows site.name. Rename both on purpose.
  • Runs under Bun. The default spawner is Bun.spawn. Without Bun, run() throws ERR_DEPLOY_WRANGLER_FAILED.