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

cli

Node-only. build, serve, preview and deploy as app methods, with the branded terminal panel.

cli.mdllms.txt

What it does

Node-only. cli turns the build and the deploy into five methods at app.cli. Four of them back one script each: build, serve, preview, deploy. The fifth, update, is a headless incremental rebuild for an external dev loop, such as a @moku-labs/worker dev session that serves this site.

There is no argument parser, no run() dispatcher and no binary. A script names its command by calling it.

How it works

cli depends on build and deploy and calls them at run time. It renders live progress by hooking their events: every build:phase becomes a row, build:complete becomes the BUILD box, deploy:complete the DEPLOYED box.

On a terminal the panel is animated: the ▟▙ moku web lockup with the version and runtime, a live phase tree, boxed results and a breathing "live" line while the dev server waits. In CI and pipes, or with NO_COLOR, it prints plain lines: one per finished phase, no animation.

The dev server, the preview server and the file watcher live inside serve() and preview() and close on Ctrl+C. The plugin itself holds no resource, so it has no onStart or onStop.

Config

pluginConfigs.cli. Every field has a default.

OptionTypeDefaultDescription
outDirstring"dist"The build output: served by preview, checked by build, rebuilt by serve.
portnumber4173Port for serve() and preview(). Overridable per call.
watchDirsstring[]["content", "src"]Folders serve() watches, recursively.
debounceMsnumber150Window that merges a burst of file events into one rebuild.
notFoundFilestring"404.html"The file build() requires at the root of outDir.
liveReloadbooleantrueInject the live-reload client into pages during serve(). Never during preview().

createApp fails with code ERR_CLI_CONFIG when port is not an integer from 1 to 65535, outDir or notFoundFile is empty, watchDirs is empty or debounceMs is negative.

API

Mounted at app.cli, reachable with ctx.require(cliPlugin).

MethodSignatureDescription
build(options?: { assertNotFound?: boolean }) => Promise<BuildSummary>Runs build.run(), then checks <outDir>/<notFoundFile>. Throws ERR_CLI_NOT_FOUND when it is missing.
update(changes: readonly string[], options?: { og?, sitemap?, feeds? }) => Promise<BuildSummary>Incremental rebuild for the changed paths. No header, no 404 check. OG images, sitemap and feeds stay off unless turned on.
serve(options?: { port?, open?, og?, sitemap?, feeds? }) => Promise<void>Builds once, serves outDir with live reload, watches watchDirs, rebuilds and reloads. Resolves on Ctrl+C. open is reserved.
preview(options?: { port? }) => Promise<void>Serves the built outDir like Cloudflare Pages: /a/ → a/index.html, a miss → the nearest 404.html with status 404.
deploy(options?: { branch?, yes?, guided? }) => Promise<DeployOutcome>guided: true runs the setup wizard. Otherwise: deploy.init({ ci: true }), a y/N prompt on an interactive terminal only, then deploy.run().

BuildSummary is { outDir, pageCount, durationMs }. DeployOutcome is { deployed: true, url, deploymentId, branch, durationMs } or { deployed: false, reason: "declined" | "blocked" | "failed" }.

Events

Emits

None.

Listens to

EventSourceWhat it renders
build:phasebuildA phase row: spinner while running, ✓ and the time when done.
build:completebuildThe BUILD box with the page count, time and output folder.
deploy:completedeployThe DEPLOYED box with the branch, time and URL.

Depends on

  • build: run() for build, update and serve.
  • deploy: init() and run() for deploy.

Both are Node-only, so cli is too. List them before it: plugins: [buildPlugin, deployPlugin, cliPlugin].

Example

src/app.ts
import { buildPlugin, cliPlugin, createApp, defineRoutes, deployPlugin, processEnv, route } from "@moku-labs/web";
import { h } from "preact";

const routes = defineRoutes({ home: route("/").render(() => h("h1", null, "My Blog")) });

export const app = createApp({
  plugins: [buildPlugin, deployPlugin, cliPlugin],
  pluginConfigs: {
    site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes." },
    router: { routes },
    build: { outDir: "dist", notFound: true },
    cli: { outDir: "dist", port: 3000, watchDirs: ["content", "src", "public"] },
    env: { providers: [processEnv()] }
  }
});

const summary = await app.cli.build();
console.log(summary.pageCount);
  1. Watch public/ too, so a changed image triggers a rebuild.
  2. In a real project this line is the whole of scripts/build.ts.
build
bun run build
▟▙ moku web  build                                       v2.4.11
✓ bundle · 5ms
✓ content · 1239ms
✓ pages · 5ms
✓ not-found · 1ms
✓ sitemap · 1ms
✓ cache-headers · 0ms
✓ root-index · 0ms
✓ BUILD · 9 pages                                1252ms · ./dist/
  • build() needs the 404 page. Set build.notFound, or pass { assertNotFound: false }. Cloudflare Pages switches a project without a top-level 404.html to SPA mode.
  • The direct deploy() writes a workflow. It calls deploy.init({ ci: true }), so .github/workflows/deploy.yml appears when it is missing.
  • The prompt never blocks CI. The y/N confirm shows only on an interactive terminal with CI unset. { yes: true } skips it anywhere.
  • A failed direct deploy is re-thrown. The panel prints the cause and a hint, then the error propagates, so CI exits non-zero.
  • serve() skips expensive outputs. OG images, sitemap and feeds are off in the dev loop unless you pass og, sitemap or feeds.