name: morphe description: Build and deploy a Next.js, Bigfish (@alipay/bigfish), or Vite project to the Morphe service (https://morphe.zenmux.app), targeting a linux-x64-gnu runtime. Use when the user asks to deploy, ship, publish, or release a Next.js, Bigfish, or Vite app to Morphe, run "morphe deploy", or otherwise push a build to the Morphe / zenmux platform. Handles login, framework detection, Next.js standalone validation / config fixing, Bigfish static-server wrapping, Vite SPA static wrapping or custom-server (server.ts/js) esbuild bundling, building, zipping, OSS upload, CRC64 checksum, .morphe.json management, and the deploy API call.
Morphe Deploy
Overview
Deploy a Next.js, Bigfish (@alipay/bigfish), or Vite project to
Morphe (runtime custom.debian11, linux-x64-gnu). The runtime always starts
the function with node server.js, so every framework packages down to a zip
whose root is a server.js:
- Next.js → the standalone server (
.next/standalone/server.js) plus its tracednode_modules. Packaging prunes wrong-platform native bindings and repairs pnpm partial packages. - Bigfish → a static / SPA build (
dist/). Packaging generates a zero-dependency staticserver.js(Node built-ins only — nothing to install) that serves the build dir with SPA fallback toindex.html. - Vite → two modes. A pure SPA (
vite build→dist/, no server) reuses the same zero-dependency static wrapper as Bigfish. A custom-server app (aserver.ts/server.js, e.g. Express, that servesdist/and backend API routes) is esbuild-bundled into a self-containedserver.jsat the zip root, with the build dir alongside it.
The fragile, deterministic API work (auth, presign, upload, CRC64, .morphe.json,
deploy) lives in scripts/morphe.py. The build/config judgment steps are done by you.
Run all scripts/morphe.py commands from the project root. Replace SKILL_DIR
below with this skill's directory (the folder containing this file).
Workflow
Execute these steps in order. Stop and report if any step fails.
1. Ensure logged in
python3 SKILL_DIR/scripts/morphe.py check-auth
- Exit 0 → already logged in (valid
accessTokenin~/.morphe/auth.json). Continue. - Exit 1 → not logged in. Ask the user for their username and password, then:
python3 SKILL_DIR/scripts/morphe.py login --username "USER" --password "PASS"
Never echo the password back. On success the token is saved to ~/.morphe/auth.json.
If login fails (e.g. HTTP 401), report the error and stop.
2. Detect the framework
python3 SKILL_DIR/scripts/morphe.py detect-framework --project-root .
Prints nextjs, bigfish, vite, or unknown (exit 1). Detection is ordered
(first match wins):
- bigfish —
@alipay/bigfishinpackage.jsondeps, or aconfig/config.tsexists (checked first; Bigfish projects also have next-like build scripts and vite-style tooling). - nextjs —
nextin deps, or anext.config.{js,mjs,ts}exists. - vite —
vitein deps, or avite.config.{js,mjs,ts,mts,cjs}exists (checked last; it's the broadest).
If it prints unknown, tell the user "暂不支持该项目类型(仅支持 Next.js、Bigfish 与 Vite)"
and stop. Otherwise remember the framework — it selects the build and packaging
path below.
3. Resolve the function name
Ask the user what function name to deploy under. Tell them: 如果不知道填什么,
可以留空,会自动生成一个 user-xxxxxxxx 格式的随机函数名。
- If
.morphe.jsonalready has afunction_name, mention it as the current default and let the user keep it (just press enter) or override. - Persist the choice (empty input → keep existing or generate):
# user provided a name:
python3 SKILL_DIR/scripts/morphe.py set-function-name --name "NAME" --project-root .
# user left it blank:
python3 SKILL_DIR/scripts/morphe.py set-function-name --project-root .
The command prints the final function name and writes it to .morphe.json.
This name is reused on every redeploy, so the same function is updated.
4. Validate & fix the build config
Steps 4–6 differ by framework. Follow the branch for the framework detected in step 2.
4a. Next.js
The goal: the runtime target (linux-x64-gnu) binary of every native dep must be
installed on disk before the build, so Next's tracer can pick it up. The
morphe.py package step (step 6) handles top-level placement, pruning, and
zipping — but it can only ship a binary that the install actually downloaded.
Edit the config in place to ensure:
standalone output —
output: "standalone"(else there is no.next/standalone/to zip).Install the linux binaries on the build host. Native addons ship as per-platform optional deps; a macOS install only fetches the darwin one.
- pnpm (
pnpm-workspace.yaml) — addsupportedArchitecturesso the linux-x64-gnu binaries are fetched too. This is the single most important fix; with it, the tracer auto-includes the binding and you usually need NOoutputFileTracingIncludesat all:
Then re-runsupportedArchitectures: os: [current, linux] cpu: [current, x64] libc: [current, glibc]pnpm install. - npm/yarn — install the specific binding(s) for the target, e.g.
npm i --no-save @resvg/resvg-js-linux-x64-gnu --force --os=linux --cpu=x64 --libc=glibc.
- pnpm (
(Optional) trim the bundle further.
morphe.py packagealready prunes every non-linux-x64-gnu native binary, so you do NOT needoutputFileTracingExcludesfor those. Only add excludes for project data the server doesn't need at runtime (large fixtures, raw datasets, docs). Keep anything read at runtime viaprocess.cwd()(fonts, JSON the route reads).
See references/nextjs-config.md for details, per-package binding names, and
how to confirm the linux binary is on disk.
4b. Bigfish
No native-binding work — Bigfish builds a static / SPA bundle and morphe.py package generates a zero-dependency server.js (Node built-ins only). There
is nothing to install for the linux target and no config to patch in the common
case.
- Confirm the app builds to a static bundle. A default Bigfish
sitebuild emitsdist/(index.html+ hashed JS/CSS), which is what the generated static server serves.deployMode: 'render'(SSR) is not covered by this static wrapper — if the app truly needs server-side rendering at runtime, stop and tell the user the static path won't serve their SSR routes. - If the build output dir is not
dist/(Bigfish lets you override it), note the dir name; you'll pass it via--static-dirin step 6.
4c. Vite
Vite has two modes; morphe.py package (step 6) auto-detects which by probing
for a server entry (server.{ts,js,mjs,cjs}, also under src/).
Pure SPA (no server entry) — nothing to install or patch, exactly like Bigfish.
vite buildemitsdist/(index.html+ hashed assets) and the generated zero-dependency staticserver.jsserves it with SPA fallback. If the output dir isn'tdist/, note it for--static-dirin step 6.Custom-server (a
server.ts/server.jsservingdist/and API routes) —packagewill esbuild-bundle that entry into a self-containedserver.js. Make sure the server:- listens on
process.env.PORT || 3000and binds0.0.0.0(the runtime setsPORTand routes to all interfaces); - reads its static dir from
process.cwd()(e.g.path.join(process.cwd(), "dist")), since the zip ships the build dir alongsideserver.jsat the runtime working dir; - keeps any
viteimport dev-only.viteis always externalized from the bundle (it's huge and pulls in native.nodeaddons — esbuild/ lightningcss/fsevents — that can't be bundled and break the build). A production server must not load it. Put any dev-middleware vite usage behind a lazy, non-production branch, e.g.:if (process.env.NODE_ENV !== "production") { const { createServer } = await import("vite"); // lazy: never bundled // ... dev middleware }
Runtime secrets (API keys, etc.) belong in the function environment, not in the zip — the build never bundles your
.env.- If your server needs an extra package kept out of the bundle (e.g. a native
.nodeaddon esbuild can't inline), pass it via--external NAMEin step 6 — but then that package must be reachable at runtime (this is an edge case; most pure-JS deps bundle fine).
- listens on
5. Build
npm run build # or: pnpm build / yarn build (Bigfish: also `bigfish build`)
Do NOT hand-copy assets or hand-zip — step 6 does all assembly. (Vite: run only
the frontend build here to produce dist/. For a custom-server app you do
not need to bundle the server yourself — package runs esbuild for you in
step 6. If your build script already esbuilds the server, that's harmless;
package re-bundles into the staged zip regardless.)
6. Assemble the minimal deploy zip
python3 SKILL_DIR/scripts/morphe.py package --project-root .
package auto-detects the framework (override with --framework nextjs|bigfish|vite). It is idempotent (safe to re-run, but re-run the build
first if you changed code).
Next.js — assembles .next/standalone into a runnable zip:
- copies
.next/staticandpublic/into the bundle (not traced by the build); - repairs pnpm partial packages — top-level
node_modules/<pkg>dirs that hold onlypackage.jsonwhile the real files live in.pnpm(this shadowing is what crashesnode server.jswith e.g. Cannot find module@swc/helpers/cjs/_interop_require_default.cjs); - prunes every native binding that isn't
linux-x64-gnu(darwin/musl/arm64 — often 100M+) and symlinks the kept linux bindings to top-level node_modules, where the runtime resolves them with a barerequire("<pkg>-linux-x64-gnu"). Missing this is why SVG-style code paths work but anything hitting the native addon 500s with Cannot find module…-linux-x64-gnu; - zips with symlinks preserved (
zip -y) to./code.zipOUTSIDE the standalone dir (so it never nests a previouscode.zip). The runtime preserves symlinks, and pnpm's layout is mostly symlinks into.pnpm, so this roughly halves the zip.
Bigfish — assembles a static-server zip:
- locates the build output dir (auto-probes
dist,build,outfor one containingindex.html; override with--static-dir DIR); - generates a zero-dependency
server.js(Nodehttp/fs/pathonly) that serves the build dir and falls back toindex.htmlfor client-side routes; - stages
server.js+ the build dir and zips soserver.jsis at the zip root with the build dir alongside it. Nonode_modules, so the zip is small.
Vite — two modes, auto-selected by probing for a server entry:
- SPA (no server entry found) — identical to the Bigfish path above: a
generated zero-dependency static
server.js+ the build dir. - Custom-server (a
server.{ts,js,mjs,cjs}entry, auto-probed; force/override with--server-entry PATH) — esbuild-bundles the entry (--bundle --platform=node --format=cjs,vitealways external, plus any--external NAMEyou pass) into a self-containedserver.js, then stages it with the build dir (auto-probesdist/build/out, override--static-dir) and zips soserver.jsis at the zip root. Deps are inlined, so nonode_modulesships.
It prints the final path, size, and framework, e.g.
packaged: …/code.zip (25M) or packaged: …/code.zip (0.2M, framework=bigfish)
or packaged: …/code.zip (0.7M, framework=vite, mode=server, entry=server.ts).
7–11. Upload, checksum, and deploy
A single command does presign → curl PUT upload → CRC64 checksum →
update .morphe.json (writes checksum; uses the function_name resolved in
step 3, generating one only if somehow still absent) → call /api/deploy:
python3 SKILL_DIR/scripts/morphe.py deploy --zip code.zip --project-root .
On success it prints the deploy result JSON (including action, functionName,
and triggerUrl when available), then deletes the local code.zip (it's
already in OSS and is a large throwaway). Pass --keep-zip to retain it for
inspection. On failure the zip is kept so a redeploy can retry. Report the
outcome to the user — give them the triggerUrl if present. If it prints
not-logged-in or an HTTP error, go back to step 1 (the token may have expired)
or report the failure.
Notes
- The Morphe API advertises cookie auth but login returns an
accessToken; the script sends it as both amorphe_sessioncookie and a Bearer header. function_namein.morphe.jsonis generated ONCE and reused on every redeploy so the same function is updated rather than duplicated. Do not hand-edit or regenerate it.- All frameworks deploy with the default
command="node server.js",port=3000— no per-framework deploy flags. The Bigfish / Vite-SPA static server honorsprocess.env.PORT(defaults to 3000) so it matches; a Vite custom-server must do the same (step 4c). - (Next.js) Keeping
code.zipsmall is mostly automatic viamorphe.py package(binding pruning + symlink-preserving zip). The remaining large item is usually project data the build traced in (raw datasets, fixtures, generated outputs under a dir a server componentreaddirs). Trim those withoutputFileTracingExcludesin the Next config — but never exclude files read at runtime viaprocess.cwd()(fonts, JSON the route parses). (Bigfish zips are just the static build — already small, nonode_modules.) - (Next.js) If a native addon still 500s at runtime with Cannot find module
<pkg>-linux-x64-gnu: the binary wasn't installed on the build host. Fix the install (step 4a:supportedArchitecturesfor pnpm, ornpm i --os=linux --cpu=x64 --libc=glibc …), reinstall, rebuild, repackage.packagewarns when an expected linux binding is absent. - (Bigfish) A blank page or 404 on a client route means the SPA fallback
isn't reaching
index.html, or the build dir wasn'tdist/. Confirmindex.htmlexists in the build dir and pass--static-dirif it's not one ofdist/build/out. The generated static server is for static builds; an SSR (deployMode: 'render') app needs a real server entry, not this wrapper. - (Vite)
packageesbuild step fails (e.g. No loader is configured for ".node" files or a glob/require.resolveerror fromvite/lightningcss/fsevents): the server entry importsvite(or another native-addon dep) at the top level. Make that import dev-only / lazy (step 4c) so it's externalized, or pass the dep via--external NAME. - (Vite) SPA blank page / 404 on a client route — same as Bigfish: confirm
index.htmlis in the build dir and pass--static-dirif it isn'tdist/build/out. A custom-server route 500ing usually means a dep was externalized but actually needed at runtime, or the server reads its static dir from a path other thanprocess.cwd()— fix per step 4c. - (Vite) A 500 from a backend route at runtime but not locally is often a missing
runtime secret — set API keys etc. in the function environment (they're
never bundled into the zip from your
.env). - Full API reference:
references/api.md.