zfb
GitHub repository

Type to search...

to open search from anywhere

Troubleshooting

Known failure modes with concrete fixes — install errors, bundle config, import queries, module workers, out-of-root imports, and missing esbuild binaries.

What this page covers

A symptom-first index of failure modes that come up often enough to be worth naming directly, each with the exact message you'll see and the fix. If your problem isn't here, the linked concept/guide pages cover the full behavior each feature is built on.

curl | sh install fails to resolve a release tag

Symptom: the curl installer exits with one of:

error: could not resolve the latest release tag from GitHub API
error: could not resolve the latest prerelease tag from GitHub API (no prerelease found in the last 30 releases)

Cause: install.sh resolves a tag from the GitHub Releases API before downloading anything.

  • With ZFB_VERSION unset, it asks for the latest non-prerelease release. This normally resolves; it fails only if the API is unreachable, rate-limited, or the request is otherwise blocked. See Install without Node.

  • With ZFB_VERSION=latest-prerelease, it scans the most recent 30 releases for one flagged "prerelease": true. If none of the last 30 is a prerelease (for example, several stable releases have since shipped and prereleases have rolled off that window), resolution fails the same way.

Fix:

  • If you are rate-limited or behind a proxy: pin an exact tag from the releases page, e.g. ZFB_VERSION=v1.0.0, which skips the tag-resolution request entirely.

  • If you specifically want a prerelease and latest-prerelease fails to resolve (none of the last 30 releases is a prerelease): pin the exact prerelease tag instead, e.g. ZFB_VERSION=v0.1.0-next.99.

musl/Alpine is not supported yet

Symptom:

[zfb] musl/Alpine is not supported yet — no @takazudo/zfb-linux-*-musl package exists.
      Use a glibc-based Linux image (e.g. Debian/Ubuntu) instead of Alpine.

Cause: zfb ships prebuilt, glibc-linked platform binaries only — no @takazudo/zfb-linux-*-musl package exists. The npm wrapper (packages/zfb/bin/detect-musl.mjs) detects a musl runtime (Alpine's /lib/ld-musl-* dynamic loader, or the absence of a glibc version in Node's own runtime report) and fails with this friendly message instead of letting the glibc binary crash with an opaque dynamic-loader error. The standalone curl/PowerShell installers can't detect musl this way (they only check OS/CPU) and will download a glibc binary that then fails to execute outright.

Fix: use a glibc-based Linux base image (Debian, Ubuntu) instead of Alpine — in a Dockerfile, a CI runner, or any container image. There is no workaround that runs zfb natively on musl today; see Installation and Install without Node for the supported platform matrix.

ERR_PNPM_TRUST_DOWNGRADE installing 2.12.0

Symptom: installing or upgrading any @takazudo/zfb* package at 2.12.0 fails under pnpm's opt-in trustPolicy set to no-downgrade:

ERR_PNPM_TRUST_DOWNGRADE High-risk trust downgrade for "@takazudo/zfb@2.12.0" (possible package takeover)

Trust checks are based solely on publish date, not semver. A package cannot be
installed if any earlier-published version had stronger trust evidence. Earlier
versions had provenance attestation, but this version has no trust evidence.

trustPolicy is opt-in (default off) and available since pnpm 10.21. Whether an existing lockfile saves you depends on your exact version:

Installpnpm 10.21 – 11.1pnpm 11.2+
fresh / forced resolvefailsfails
pnpm install --frozen-lockfilesucceedsfails

pnpm 11.2 added a verification pass that re-applies the active supply-chain policies to every entry in an already-resolved lockfile, so a committed lockfile pinning 2.12.0 installs fine on 11.1 and breaks on 11.2.

Cause: the 2.12.0 packages carry no npm provenance attestation, while 2.11.0 does. This is not a package takeover. 2.12.0 was published through zfb's published-release recovery path — a workflow_dispatch run from main that rebuilds an already-published tag. GitHub's OIDC identity in that run describes the main workflow commit, not the older tag commit the artifacts were built from, so the release deliberately publishes with no attestation rather than one that misidentifies the source. Recorded in issue #2623.

Five of the six packages a default install pulls are affected. @takazudo/zfb-darwin-x64 was not flagged only because it had no attestation through 2.12.0, so there was nothing for pnpm to compare against. Starting with 2.13.0, it is attested like the rest.

Fix: npm versions are immutable, so 2.12.0 itself can never gain an attestation. Prefer, in order:

  1. Upgrade past it to 2.13.0 or later. Those releases go out through the normal tag/release path with provenance restored, which clears the downgrade. This is the real fix.

  2. Stay on 2.11.0 if upgrading is not yet possible — it remains attested and avoids the affected 2.12.0 release.

  3. Exempt just these packages, if you need 2.12.0 now, with trustPolicyExclude in pnpm-workspace.yaml — it keeps the policy active for every other dependency:

    trustPolicy: no-downgrade
    trustPolicyExclude:
      - "@takazudo/zfb@2.12.0"
      - "@takazudo/zfb-darwin-arm64@2.12.0"
      - "@takazudo/zfb-linux-arm64-gnu@2.12.0"
      - "@takazudo/zfb-linux-x64-gnu@2.12.0"
      - "@takazudo/zfb-win32-x64-msvc@2.12.0"

    Pin the version in each entry so the exemption expires on its own when you upgrade. A name pattern cannot be combined with a version ("@takazudo/zfb*@2.12.0" is rejected with ERR_PNPM_INVALID_TRUST_POLICY_EXCLUDE); use exact name@version entries as above, or an unversioned glob such as "@takazudo/zfb*" if you accept exempting all versions.

Disabling trustPolicy outright also works, but drops the guard for every dependency rather than these five — prefer the scoped exemption.

import.meta.glob(...) build error

Symptom, from zfb build (or a warning + skipped rebundle from zfb dev):

zfb islands: `import.meta.glob(...)` cannot be safely shipped from one or more
files reachable from a "use client" island. zfb can currently expand eager
string-literal globs only when the glob call is in an island-reachable module
that is written as a real shadow copy; unsupported forms and globs found only in
raw-mirrored glob target/subtree files would ship to the browser unexpanded and
throw at hydration: <file> — ... Follow the remediation above for each file,
use the eager string-literal form where applicable, replace the glob with
explicit static imports, or move the usage to a server-only (non-"use client")
module. Tracked at https://github.com/Takazudo/zudo-front-builder/issues/1385
and https://github.com/Takazudo/zudo-front-builder/issues/1412.

Cause: import.meta.glob is a Vite-only build-time macro; esbuild (which bundles islands) has no native understanding of it, so zfb expands the one supported shape itself before esbuild ever sees the file. Only import.meta.glob("<string-literal>", { eager: true }), with the pattern resolving under the importing file's own directory, is supported when the glob call is in an island-reachable module that zfb writes as a real expanded shadow-copy file. Everything else — the default lazy form, { eager: false }, a non-literal pattern, the import/query/as options, or a ../-escaping pattern — is rejected. zfb also rejects a glob inside a JS-like file that is reachable only as a raw-mirrored glob target or subtree companion, because that file would otherwise ship to the browser unexpanded and throw during hydration.

Fix: see Islands: import.meta.glob support for the full support matrix and alternatives. Use the eager string-literal form where applicable, hoist nested globs into island-reachable modules, move raw-mirrored glob target/subtree files out of the globbed subtree, or replace the glob with explicit static imports. Note that client scripts (*.client.ts) don't support import.meta.glob at all, and — unlike islands — that failure is not caught at build time; see Client Scripts: import.meta.glob is not supported.

bundle.loaders or bundle.define is rejected

Symptom: config loading fails with a message such as:

bundle.loaders key ".asset" uses unsupported loader "file"; inline-only v1 accepts: ...
bundle.define key "import.meta.env.DEV" is reserved by zfb's bundle mode ...

An invalid raw define expression can instead surface as an esbuild parse error when the first affected bundle runs.

Cause: loader keys must start with ., use one of text, json, base64, dataurl, binary, or empty, and cannot override .css, .module.css, .mdx, or .md. The asset-emitting file and copy loaders are not supported. zfb also owns import.meta.env.DEV, import.meta.env.PROD, and process.env.NODE_ENV, so user defines cannot replace them. Define values are raw esbuild expressions, not automatically quoted strings.

Fix: choose an inline loader and an unreserved extension/key. For a string define, include JSON quotes in the replacement (for example, __APP_NAME__: '"my-app"'). See defineConfig: Bundle settings for the full contract and the browser-exposure warning.

A ?raw import fails

Symptom: the build, or a browser rebundle during dev, reports unsupported import query form, does not resolve to an existing file, or is not valid UTF-8 text.

Cause: zfb supports exactly a static default import with a literal, project-local relative target and the exact ?raw suffix. The target is a terminal UTF-8 text file. Dynamic, named, namespace, side-effect, type-only, or re-export forms; ?url; extra query parameters; non-literal paths; out-of-root/symlink escapes; and non-UTF-8 targets are rejected.

Fix: reduce the import to this shape and make sure the exact file exists inside the project root:

import text from "./file.ext?raw";

The extension can be anything and does not need a bundle.loaders entry. See Islands: Importing text with ?raw or Client Scripts.

A module worker is not emitted, or SharedWorker fails

Symptom: a worker constructor remains unchanged and no worker-*.js companion appears, or the build reports unsupported SharedWorker.

Cause: zfb discovers the unshadowed global Worker only in the literal module form below. The URL must point to an exact project-local relative JS/TS file without a query or fragment. Non-literal and classic-worker shapes are outside the contract. Discovery deliberately does not traverse installed node_modules. A SharedWorker using the same literal URL shape is rejected loudly because leaving that apparently supported URL unrewritten would make it 404 at runtime.

Fix: for a first-party dedicated worker, use:

const worker = new Worker(
  new URL("./worker.ts", import.meta.url),
  { type: "module" },
);

Island workers appear under /assets/worker-*.js; client-script workers use /assets/client/worker-*.js. The rewritten URL keeps that stable name and adds an exactly eight-character lowercase hexadecimal ?v= graph hash. For unsupported shapes or third-party transitive workers, bundle them with package-specific tooling. See Islands: Module workers.

A relative import "Could not resolve" outside the project root

Symptom: zfb build (or zfb dev) fails with an esbuild Could not resolve "../../../somewhere/outside.ts" error, and the accompanying zfb note explains a "shadow-copy build boundary."

Cause: zfb builds by shadow-copying the project root into a temporary directory before running esbuild. A relative import that walks above the project root (e.g. ../../../../packages/ui/src/button.tsx) resolves outside that shadow copy and cannot be found. This is diagnose-only, not a resolvable bug — the underlying escape stays unsupported; zfb only makes the reported error name the real (non-shadow) path and explain the workaround instead of leaking a meaningless temp-directory path.

Fix: expose the out-of-root target as a package import instead of a relative one — add a wildcard exports entry to the target package's package.json (e.g. "./src/*": "./src/*") and import it by package specifier (e.g. @scope/pkg/src/button.tsx). node_modules (including file:/workspace-linked packages) IS included in the shadow copy, so package-specifier imports resolve normally. Full details: defineConfig: Watching paths outside the project root; tracked at issue #1385.

esbuild binary not found from an embedded zfb-server

Symptom: a Rust host embedding zfb-server (see Embed as library) fails with an "esbuild binary not found" error when config_path points at a zfb.config.ts file.

Cause: evaluating a TypeScript config first bundles it with esbuild, and an embedded zfb-server does not ship its own esbuild binary the way the zfb CLI does.

Fix: either switch config_path to a zfb.config.json file (no esbuild needed at all — the simplest fix), or set the ZFB_ESBUILD_BIN environment variable to a usable esbuild CLI binary before calling build(). See Embed as library: config format for the full explanation.

A prerender = false route always returns 405

Symptom: an SSR route's POST/PUT/DELETE handler always returns 405 Method Not Allowed, even for requests that use the expected method and are otherwise correct.

Cause: the route's default export declares a parameter meant to receive the incoming Request:

export default async function Handler(request: Request) {
  if (request.method !== "POST") {
    return new Response("Method Not Allowed", { status: 405 });
  }
  // ...
}

zfb calls a page's default export with the page's props object, not the Request — see The default export receives props, not the Request for the full contract. So request above is actually {} (or { params }, or the route's paths()/getStaticProps() props), request.method reads undefined, undefined !== "POST" holds, and the handler 405s unconditionally. Nothing throws, and Request is a valid TypeScript annotation for a parameter that receives props, so tsc --noEmit passes — this compiles cleanly and fails silently at runtime.

Detect it directly with:

grep -rn 'export default \(async \)\?function \w\+(\s*request\s*:' pages/

Fix: drop the parameter (or destructure the props you actually need — { params }, etc.) and read the real Request through getCloudflareContext() instead:

import { getCloudflareContext } from "@takazudo/zfb-adapter-cloudflare";

export default async function Handler() {
  const { request } = getCloudflareContext();
  if (request.method !== "POST") {
    return new Response("Method Not Allowed", { status: 405 });
  }
  // ...
}

zfb check flags this shape with a non-zero exit; zfb dev and zfb build warn about it without failing the build. If you landed here from one of those warnings rather than a live 405, the cause and fix above are the same explanation from the other side.

I need to inspect zfb's shadow build directory

Symptom: zfb build or zfb dev fails, or behaves unexpectedly, in a way that points at what zfb generates before handing off to esbuild — a generated injected-route shim, the resolved node_modules layout, a synthetic tsconfig.json. By the time the process exits (even on a failing build), the temporary directory that held all of that is already gone.

Cause: zfb builds by materialising a shadow copy of the relevant tree into a tempfile-managed directory and deleting it once the process exits — normal operation, and the reason you can't just find it afterward under /tmp.

Fix: set one of two opt-in environment variables to keep the shadow tree on disk instead of deleting it. Each covers a different set of tempdirs and neither auto-reaps the dir it keeps — clean it up by hand once you're done inspecting it.

VariableScopeWhat it keeps
ZFB_KEEP_BUILD_SHADOW=1zfb build onlyThe bundler's shadow root, its exact-node_modules isolation root, and the package-routes overlay (when the project registers package routes). Survives a failing build too. Each kept path is printed once to stderr. See zfb build: Environment variables.
ZFB_KEEP_SHADOW_SESSION=1zfb dev onlyThe dev server's one persistent shadow session tempdir for the whole process. A later zfb dev boot without the flag reaps a previously-kept dir once it passes an internal age floor — keep the flag exported for the whole debugging session, not just the boot that created the dir. See zfb dev: Environment variables.

Both are narrowly scoped: neither covers every zfb build/zfb dev temp producer (config loading, islands/esbuild, CSS, plugin resolution are outside both), and setting one never affects the other subcommand — ZFB_KEEP_BUILD_SHADOW leaves zfb dev untouched, and ZFB_KEEP_SHADOW_SESSION leaves zfb build untouched.

Revision History

CreatedUpdated