Upgrading to 0.5
Fuzor 0.5 removes compatibility code and turns several silent failures into build errors. Work through the sections that apply to your app, then run every build profile you ship.
router.navigate() takes options and returns an outcome
The second argument is an options object, and the result describes what happened instead of a boolean:
| Before | After |
|---|---|
router.navigate(href, true) | router.navigate(href, { history: "replace" }) |
if (await router.navigate(href)) | if ((await router.navigate(href)).status === "completed") |
false for blocked navigation | { status: "blocked" } |
Error for URLs outside the app | NavigationError with reason: "InvalidUrl" |
The default history mode is "auto", which already replaced the entry for the current URL before. Adapters implementing NavigationAdapter need a canTraverse(delta) method, and NavigationIntent.history accepts "auto".
With the History fallback, a pushed entry no longer copies other properties of the previous entry's history.state; replacements still keep them.
Package layout and scaffolding
Install fuzor for the framework and CLI. Starter templates live in the separate create-fuzor package:
npm create fuzor@latest my-app
fuzor create still works and runs create-fuzor at the same version as your installed fuzor.
The ssr target was removed
Replace --target ssr with --target spa or --target mpa, and add --server for server delivery:
| Before | After |
|---|---|
fuzor build --target ssr | fuzor build --target mpa --server |
History hosts and createUrlAtom were removed
HistoryHost, PlatformHistory, browserHistory(), memoryHistory(), BrowserHistoryLive, VirtualHistoryLive, normalizeHref(), createUrlAtom() and the router's host option no longer exist. Navigation goes through the Navigation service and its adapters.
For tests or non-browser hosts, inject a memory adapter:
import * as Effect from "effect/Effect";
import { createRouter, makeMemoryNavigation } from "fuzor/runtime";
const navigation = await Effect.runPromise(makeMemoryNavigation({ baseUrl: "https://app.test/" }));
const router = createRouter(outlet, { navigation, islands });
await router.start();
Replace URL atoms with makeNavigationUrlAtom, which runs in an Effect scope. Use router.navigation() after start() to follow the router:
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import { makeNavigationUrlAtom } from "fuzor/runtime";
await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
const theme = yield* makeNavigationUrlAtom(router.navigation()!, "theme", Schema.Literals(["light", "dark"]), "light");
yield* theme.set("dark");
// …keep the scope open while the atom is in use
})));
Differences from createUrlAtom: invalid values fail with UrlAtomError unless you pass { invalid: "default" }, set replaces the history entry by default (pass "push" to add one), remove() deletes the parameter, and repeated parameters are invalid.
Markdown links are checked at build time
A Markdown page that links to a missing route, a missing anchor or a missing file in public/ now fails compilation, listing every problem. To update an app:
Fix links that are really broken.
For same-site paths served outside Fuzor, list their prefixes:
fuzorPlugin({ markdown: { ignorePaths: ["/api/", "/downloads/"] } })To migrate gradually, report problems as warnings with
markdown: { linkCheck: "warn" }, or turn checking off with"off".
Route URLs are matched after decoding
URL segments are decoded, Unicode-normalized and re-encoded before matching, so /a+b/ now reaches pages/a+b/, and //about// reaches /about/. If two folders resolve to the same URL (for example composed and decomposed spellings of café), compilation fails; rename or merge one of them.
Folder names with brackets are dynamic routes
A folder named like [slug] or [...path] is now a dynamic route and needs a paths module. Rename folders that should keep a literal bracket in their URL. Files named not-found.html, error.html, server.ts and paths.ts inside pages/ now have special meaning; rename unrelated files with those names.
New reserved names
api/ at the project root now holds API endpoints, and any endpoint file makes builds server-backed. Pages under pages/api/ cannot coexist with it. HTML pages that start with a --- line are now read as frontmatter; indent or escape such a line if it is meant as content.
Markdown lines with Fuzor elements
A Markdown line that contains only a Fuzor element (<fuzor-island>, <fuzor-toc>, …) or a project component is now rendered as HTML instead of escaped text. If you meant to show such a line literally, put it in a code block or backticks.
Build profile configuration
- Profiles are selected by CLI flags and environment variables. Keep
target,serverandinlineout of the static plugin configuration. FUZOR_SERVERandFUZOR_INLINEmust be1,0,trueorfalse.- The plugin chooses the output directory, so
vite buildwithout the CLI writesdist-spa(or the matching profile directory) instead ofdist. Setbuild.outDirin your Vite config to keep a custom directory. fuzor devandfuzor buildreject extra arguments. With pnpm, writepnpm build:spa --inline, notpnpm build:spa -- --inline.
Generated servers
Server entries now export a manifest that can include base. If you copied the starter server.mjs, update it so it serves assets below a deployment base and shuts down gracefully:
const { handler, manifest } = await import(`./${directory}/server/entry.mjs`);
const server = createNodeServer({ handler, base: manifest.base, assetsDir: path.join(here, directory, "client") });
handleShutdownSignals(server);
fuzor build now stages output and swaps it in on success. Add .fuzor-stage-*, .fuzor-build.lock and *.previous-* to .gitignore if your output directories are not already ignored.
Server builds now also write the prerendered documents to client/ (previously it only held assets). If you upload client/ to a CDN, it will serve those pages; otherwise nothing changes.
Documents are now sent with Cache-Control: no-cache and an ETag. If a CDN or proxy in front of Fuzor overrides caching, make sure it still revalidates HTML.
UI component prefix
Official UI Web Components now use fuzor-*: change <ui-tabs> to <fuzor-tabs> and <ui-tabs-trigger> to <fuzor-tabs-trigger>, and update matching CSS selectors. Component CSS custom properties use --fuzor-* instead of --ui-*. There are no legacy tag aliases. Imports such as fuzor/ui/client, engine APIs, and data-fuzor-ui behavior markers keep their existing names.