| name | sol-bootstrap |
| description | Use when running `sol new` for the first time, when `sol <subcommand>` fails with `failed to resolve path mizchi/sol/cmd/sol_js` / `Run this command inside a MoonBit project that depends on mizchi/sol.`, or when moving from `sol dev` to `sol build` / `sol serve` for the first time. Covers (a) the bootstrap rules â `sol new <name> --user <ns>` works in an empty directory as of sol 0.22.2, but `--cloudflare` / `--doc` / `--dev` still need a host moon project, and every non-new subcommand needs `.mooncakes/mizchi/sol/` locally (= `moon install` after `sol new`) â (b) the sol 0.22.x scaffold layout (`app/server/routes.mbt` holds routes + page handlers; `app/layout/` is a separate package; `/` and `/about` are pre-registered) and (c) the production flow gotchas (`sol build` writes to two directories, `sol serve` does NOT rebuild, dev & prod both default to :7777). |
sol-bootstrap
Purpose
The sol native CLI ($MOON_HOME/bin/sol) is a thin launcher. Most subcommands (dev / build / serve / generate / doctor / ...) delegate to moon run --target js mizchi/sol/cmd/sol_js, which only resolves inside a moon project with .mooncakes/mizchi/sol/ populated. Two friction points remain even on sol 0.22.2:
sol new without flags (the common path) is now handled natively in the launcher and works in an empty directory. However, sol new --cloudflare / --doc / --dev still go through the JS delegate, so they need a host moon project. If you hit failed to resolve path mizchi/sol/cmd/sol_js from a flagged variant of sol new, that is why.
- Right after
sol new myapp --user <ns> succeeds, the generated myapp/ has no .mooncakes/. Running sol dev immediately fails with failed to resolve path mizchi/sol/cmd/sol_js. The pnpm install step alone does not fetch MoonBit deps; moon install is the missing step (sol new now prints this in its Next steps).
The native-new entrypoint lives in sol/src/cmd/sol/main.mbt::try_native_new and the underlying templates moved to sol/src/scaffold_templates/. The delegate fallback (delegate_to_project_js_cli) is still where every other subcommand goes through; it branches its error message on moon.mod.json / .mooncakes/mizchi/sol presence so the diagnostic points at the right next action.
When to use
- Starting a brand-new sol project (no existing moon project in CWD)
sol <anything> errors with failed to resolve path mizchi/sol/cmd/sol_js even though the binary is installed and on PATH
- Documenting / explaining sol's bootstrap flow to another agent or contributor
- Reviewing whether the gap is still present after a sol release (the launcher logic may change)
Golden path (from zero to a working 2-route SSR app)
moon install mizchi/sol/cmd/sol
mkdir -p /tmp/sol-myapp && cd /tmp/sol-myapp
sol new myapp --user myorg
cd myapp
pnpm install
moon update && moon install
sol dev
When sol new still needs a host moon project
--cloudflare / --doc / --dev go through the JS delegate, so they require .mooncakes/mizchi/sol/ resolvable from the cwd. Use the host-project pattern in that case:
mkdir -p /tmp/sol-host && cd /tmp/sol-host
moon new hostproject --user myorg
cd hostproject
moon add mizchi/sol
sol new myapp --user myorg --cloudflare
The HMR WebSocket binds to :7877 (used by the loader, not for your curl). After step 5, the scaffolded routes already work:
curl -s http://localhost:7777/
curl -s http://localhost:7777/about
curl -s http://localhost:7777/api/health
When scripting startup (CI / Playwright), grep stdout for Server running at http://localhost:7777 as the ready marker rather than polling the port.
Generated project layout (sol 0.22.x)
sol new myapp --user <ns> produces:
| Path | Role |
|---|
app/server/main.mbt | async fn main â entrypoint for the SSR server |
app/server/routes.mbt | Route registrations and page handlers in the same file (no separate pages.mbt) |
app/server/alias.mbt | using @server_dom { div, span, h1, p, text, button, ... } â add tags here before referencing them in handlers |
app/server/moon.pkg | Package manifest for the server-side mbt sources |
app/layout/layout.mbt | Shared HTML layout â separate package under app/layout/, not app/server/layout.mbt |
app/layout/alias.mbt | DOM tag aliases for the layout package |
app/client/counter.mbt, app/client/api_tools.mbt | Island components scaffolded by default (delete or keep) |
app/__gen__/ | Generated by sol generate â never edit; client/ and server/ are git-ignored, types/ is committed for first-build |
Two routes (/ and /about) and one API (/api/health) are already registered in routes.mbt at scaffold time. The home handler is a heavy demo that uses both island components.
Adapting the scaffold to a minimal 2-page app
Use case: replace the demo with a custom / and /about (the typical first edit).
The Routes API used by sol new is the @sol.SolRoutes family, not the lower-level @router.page. The scaffolded routes.mbt shape is:
pub fn routes() -> Array[@sol.SolRoutes] {
[
@sol.wrap("", @layout.root_layout, [
@sol.route("/", home, title="Home"),
@sol.route("/about", about, title="About"),
]),
@sol.api_get("/api/health", api_health),
]
}
async fn home(_props : @sol.PageProps) -> @server_dom.ServerNode { ... }
async fn about(_props : @sol.PageProps) -> @server_dom.ServerNode { ... }
Minimum diff:
- Rewrite the
home body in app/server/routes.mbt with @sol.nodes([...]) returning your DOM. Drop every @server_dom.client(...) call so the counter / api_tools islands disappear from /.
- Rewrite the
about body in the same file. The /about route is already registered at scaffold time â no need to add the registration.
- In
app/server/alias.mbt, extend using @server_dom { ... } with any tags you newly reference (e.g. ul, li). Tags you stop using produce harmless unused_value warnings â remove them from alias.mbt to reach 0 warnings.
app/client/counter.mbt and app/client/api_tools.mbt can stay as orphans; they only matter if home calls them. Delete the files (and their lines in app/client/moon.pkg) for a true minimum project.
sol dev runs sol generate internally â no explicit regenerate needed. If HMR misses an edit (sometimes happens for the first edit made within a second of startup), kill and restart sol dev. sol generate --mode dev is the manual fallback.
@router.page / @router.register_routes exist for embedding sol into a host Worker â see sol/docs/routing.md for the API selection matrix. New apps stay on @sol.SolRoutes / @sol.route because the scaffold uses them.
Common failure modes
| Symptom | Root cause | Fix |
|---|
sol new <name> --cloudflare in /tmp/empty/ â Run this command inside a MoonBit project that depends on mizchi/sol. | Flagged variants of sol new still go through the JS delegate, which needs .mooncakes/mizchi/sol/ | Create a host moon project (see "When sol new still needs a host moon project" above) |
sol new <name> â Error: --user option is required | --user is mandatory; sol --help now annotates this on the new row | Pass --user <namespace> (5-39 chars) |
Right after sol new, sol dev â failed to resolve path mizchi/sol/cmd/sol_js | Generated project has no .mooncakes/ yet â pnpm install doesn't fetch MoonBit deps | moon update && moon install in the project dir, then retry. moon install alone fails when the registry index hasn't seen the freshly published mooncake versions; moon update refreshes the index first. The sol new Next steps now print this pair explicitly. |
moon new bootstrap --user me â Username must be between 5 and 39 characters long | Moon's username validator, not a sol issue | Use a âĽ5-char namespace (e.g. myorg, dogfood) |
| Route edit applied but curl returns the old page | Earlier versions of the HMR watcher only listened for "change" events and missed atomic-save / rename writers. Fixed (watcher now also accepts "rename" and re-verifies existence). | If still observed, kill and restart sol dev; report which editor / write pattern was used. |
| Want to bypass the launcher entirely | Delegate fallback uses module ref form that moon's run cannot resolve as a path | Run moon run --target js .mooncakes/mizchi/sol/src/cmd/sol_js -- <subcommand> directly. Useful when binary path resolution fails for unknown reasons. |
Production build & serve
sol dev is for development only. The production path is sol build (one-shot bundling) followed by sol serve (Node HTTP server reading the build output).
Output layout
sol build writes to two directories that together form the production artifact:
| Path | Contents | Purpose |
|---|
.sol/prod/server/main.js | Tiny loader (âKB) | Entry that sol serve boots â re-exports the bundle below |
.sol/prod/static/<island>.js | Minified island bundles (~5Ă smaller than dev) | Served as client modules |
.sol/prod/__sol__/{loader,lib,sol-nav,wc-loader}.js | Sol runtime helpers | Hydration / island loader / CSR navigation |
.sol/prod/manifest.json | Island registry | Used by sol serve and the loader |
_build/js/release/build/server/server.js | The real ~1.6 MB SSR bundle | What .sol/prod/server/main.js ends up importing |
Trap: _build/ looks like a MoonBit intermediate directory but in release mode is part of the deployable. A naive rsync .sol/prod/ <host> ships a broken bundle. Either ship both .sol/prod/ and _build/js/release/, or use the deploy adapter (Cloudflare path in sol-cloudflare-deploy).
Commands
sol build
sol build --skip-minify
sol build --skip-bundle
sol build --skip-generate
sol build --clean
sol serve --port 8888
sol serve does not rebuild on source changes. After editing .mbt files, re-run sol build && sol serve. There is no --watch flag.
dev vs prod cheat sheet
| Aspect | sol dev | sol serve |
|---|
| Prerequisite | none beyond moon install | sol build must succeed first |
| Default port | 7777 (HTTP) + 7877 (HMR WS) | 7777 |
| Port flag | (not exposed in --help) | -p, --port <port> |
| Output | .sol/dev/ (~312 KB) | .sol/prod/ (~128 KB) + _build/js/release/build/ (~1.6 MB SSR bundle) |
| Bundle minified | no | yes (5Ă smaller per island) |
| HMR | enabled, WS on :7877 | disabled (fixed in 0.22.2: SOL_DEV is no longer set when mode = "prod", so prod HTML no longer embeds the dev HMR <script>) |
| File watch | yes (Watching for .mbt file changes...); accepts both "change" and "rename" events since 0.22.2 â atomic-save editors are no longer dropped | no |
| Cold-start time | ~15s (generate + moon build + rolldown) | ~3s after sol build (4â5s) |
| Ready marker | Server running at http://localhost:7777 | Server running at http://localhost:<port> |
Known production gotchas (sol 0.22.2+)
sol build exits 0 even when generate is stale. By default sol build runs sol generate --mode prod first, so this only matters when --skip-generate is passed â in that case 0.22.2 prints a yellow warning. sol build --clean is the safe option when in doubt.
_build/js/release/ is part of the deployable. .sol/prod/server/main.js re-exports _build/js/release/build/server/server.js (~1.6 MB). A naive rsync .sol/prod/ <host> ships a broken bundle. Use the platform adapter (sol-cloudflare-deploy skill) or ship both directories.
Source of truth
- Launcher:
sol/src/cmd/sol/main.mbt â try_native_new handles sol new natively (flag-less form), delegate_to_project_js_cli handles every other subcommand with branched diagnostics
- Native-shared templates:
sol/src/scaffold_templates/ (supported_targets = "js + native", zero deps, pure string fns)
- JS-side
sol new wrapper: sol/src/cli/new.mbt (still owns --cloudflare / --doc / --dev)
- JS-side templates re-export + cloudflare/doc extras:
sol/src/cli/templates.mbt
sol build pipeline: sol/src/cli/build.mbt (rolldown invocation + manifest emission)
sol serve entry: sol/src/cli/serve.mbt (--port default 7777, reuses run_server(cwd, port, "prod"))
- Reference app matching the current scaffold:
sol/examples/sol_todo/app/{server,layout}/
- Routing API reference:
sol/docs/routing.md â @sol.SolRoutes (scaffold default) vs @router.register_routes (host-worker embedding)
- Quickstart:
sol/docs/quickstart.md
- Cloudflare deploy follow-up:
sol-cloudflare-deploy skill (in this repo's .claude/skills/)