Migrate from 0.1.0 to 1.0
This guide collects breaking changes between 0.1.0 and 1.0. The full list, including additions and fixes, is in the CHANGELOG. The stability policy applies from 1.0 onward.
Recommended order#
- Update
@meekojs/meekoto the exact target version. If you use Meeko DevTools or the VS Code extension, update them to1.0.0too. - If
server/session.tsexists, replace it withserver/runtime.ts(see Server and runtime). While that file exists,meeko check,meeko devandmeeko buildstop. - Fix subpath imports and run
meeko checkto regenerate.meeko/and see type errors. - Run
meeko buildand update deployment scripts for the new output directory. - Review the sessions, Socket and rendering sections that affect your application.
Package and subpaths#
The root only exports the public API#
@meekojs/meeko exports exactly:
signal,computed,effect,batch,untrack,createRootandonCleanup;createContext,provideandinject;provideErrorHandlerandserializeJson;- their types.
DOM, SSR, HMR and DevTools helpers are only available in @meekojs/meeko/internal:
bind*,render*,keyToken,withStateSnapshot,escapeHtml,attributeValueandrenderAttribute;enableHotModule,registerHot*,replaceHot*,restoreSignalsandsnapshotSignals;registerDevtoolsInstance.
// Before
import { escapeHtml, signal } from "@meekojs/meeko";
// After
import { signal } from "@meekojs/meeko";
// escapeHtml is no longer public: use your own function or, accepting that it only works
// with the exact same package version, import it from "@meekojs/meeko/internal".Compiler-generated modules have imported @meekojs/meeko/internal since 0.0.2. Recompile any manually compiled and versioned .meeko modules (meeko compile) with the new version.
ComponentErrorHandler receives the error source#
The provideErrorHandler observer receives a second argument source: ErrorSource ("event", "effect", "cleanup", "navigation" or "socket"). Single-parameter handlers remain valid. Only code that calls a ComponentErrorHandler itself changes, for example in a test:
// Before
handler(new Error("boom"));
// After
handler(new Error("boom"), "event");@meekojs/meeko/server is portable: build and Node are separated#
@meekojs/meeko/server only uses Web APIs and node:async_hooks. Build tools and Node transport move to their own subpaths:
| Symbols | Before | After |
|---|---|---|
discoverRoutes, discoverConfiguredRoutes, composeFrameworkRoutes, loadMeekoConfig, readMeekoConfigSync, resolveConfigSources, discoverServerManifest, prerenderRoutes, prerenderProject |
@meekojs/meeko/server |
@meekojs/meeko/build |
toNodeListener, attachNodeWebSocketServer, createNodeSocketUpgradeHandler |
@meekojs/meeko/server |
@meekojs/meeko/node |
// Before
import { createServerDispatcher, discoverServerManifest, prerenderProject, toNodeListener } from "@meekojs/meeko/server";
// After
import { createServerDispatcher } from "@meekojs/meeko/server";
import { discoverServerManifest, prerenderProject } from "@meekojs/meeko/build";
import { toNodeListener } from "@meekojs/meeko/node";buildProject is also in @meekojs/meeko/build, and serveNode and createNodeAssetReader are new in @meekojs/meeko/node.
Explicit module loader#
createServerDispatcher, createLocalCalls, generateOpenApi and loadSocketChannels no longer import routes from disk: they require a load loader.
// Before
const dispatch = createServerDispatcher({ manifest, sessions, services });
const channels = await loadSocketChannels(manifest);
// After (Node)
import { pathToFileURL } from "node:url";
const load = (file: string) => import(pathToFileURL(file).href);
const dispatch = createServerDispatcher({ manifest, sessions, services, load });
const channels = await loadSocketChannels(manifest, load);Server and runtime#
server/session.ts becomes server/runtime.ts#
Meeko no longer loads server/session.ts. Move the SessionManager to the default export of server/runtime.ts and delete the old file:
// Before: server/session.ts
import { MemorySessionStore, SessionManager } from "@meekojs/meeko/server";
export function createSessionManager(production: boolean) {
if (production) throw new Error("Configure a persistent SessionStore in server/session.ts before production startup");
return new SessionManager({ store: new MemorySessionStore(), production: false });
}// After: server/runtime.ts
import { defineServerRuntime, SessionManager } from "@meekojs/meeko/server";
export default defineServerRuntime(({ env, production, prerendering }) => ({
sessions: new SessionManager({ store: createStore(env, production && !prerendering), production: production && !prerendering }),
services: (request) => ({ /* per-request services */ }),
}));The factory receives { env, production, platform, prerendering } and can also return services, socket, publicOrigin, onError and close; see Server runtime. meeko build calls it with prerendering: true to render prerendered pages. There is no client traffic at this stage, so an in-memory store is enough.
New build output#
meeko build writes a standalone build per adapter. Node is the default adapter:
| Before | After |
|---|---|
.meeko/output/server/entry.js |
.meeko/output/node/index.mjs, with chunks/ and client/ |
.meeko/output/prerender/ |
.meeko/output/node/prerender/, with a { version: 1, pages } index in manifest.json |
# Before: deployment depended on the previous output and sources
meeko start
# After: the directory does not read app/, server/ or meeko.config.ts
meeko build
node .meeko/output/node/index.mjs # or meeko startThe server listens on MEEKO_HOST/MEEKO_PORT (HOST/PORT before 1.2; or server.host/server.port in meeko.config.ts), defaulting to 127.0.0.1:4173. The Node build needs dependencies from node_modules. For Bun, Deno or Cloudflare Workers, use meeko build --adapter <bun|deno|cloudflare>; see Build, deployment and verification.
prerenderRoutes requires each document to contain <head> and also writes the navigation payload.
Public files only from the manifest#
Only public files listed in the build manifest are served. An unlisted file, including one added to public/ after the build, is treated like any application route (usually 404, previously 403). Rebuild after adding public files.
Entry points and virtual modules#
- Client routes. HTML no longer includes
<script id="meeko-routes">: the client entry point imports routes fromvirtual:meeko-manifest/client. - Server.
virtual:meeko-server-pagesandvirtual:meeko-server-routesare replaced byvirtual:meeko-server-app. - Client pages. The generated client entry point imports
virtual:meeko-client-routes, declared by.meeko/types/virtual-pages.d.ts. If your project has its own entry point, regenerate types withmeeko check.
These modules are not public API. A custom entry point importing them must pin the exact Meeko version (see Outside the contract).
Pages and rendering#
No renderLayout#
FrameworkHandlerOptions.renderLayout is removed. Layouts are always composed with the outlet:
// Before
createFrameworkHandler({ /* ... */ renderLayout: (file, children, context) => wrapInShell(children) });<!-- After: app/layouts/default.meeko -->
<script lang="ts">
import type { Component } from "@meekojs/meeko";
export interface Props {
outlet: Component<Record<string, unknown>>;
}
const props = defineProps<Props>();
</script>
<template>
<main><component is="props.outlet"></component></main>
</template>render must be a literal#
meeko build reads render from the sidecar without executing it. It must be a string literal in definePage({ ... }), in an exported object or in a local constant in the same file. A computed value, computed key, spread without a subsequent explicit render, or re-export stops the build.
// Before
const mode = process.env.STATIC ? "prerender" : "ssr";
export const route = definePage({ render: mode });
// After
export const route = definePage({ render: "prerender" });New page modes and types#
PageRender. Now includes"stream"and"client", so an exhaustiveswitchmust cover them or have a default branch.PageDefinition.render. Accepts"ssr","stream"and"prerender".- Client pages. Use
ClientPageDefinition: itsloadreceivesClientPageLoadContext(url,params,search,signal,state), withoutrequestorcontext.
// Before
switch (render) { case "ssr": /* ... */ break; case "prerender": /* ... */ break; }
// After
switch (render) {
case "ssr": case "stream": /* ... */ break;
case "prerender": /* ... */ break;
case "client": /* ... */ break;
}The server checks the build's render#
The server compares the sidecar's render with the manifest's, even if the sidecar omits it. A manifest with render: "prerender" and a sidecar without render, or with another value, returns an error until rebuilt. Run meeko build (or restart meeko dev) whenever you change a route's mode.
State is sealed after rendering#
Each request's StateStore is sealed after synchronous rendering and head, in every rendering mode. From then on, useState with a new key, seed or a write throws STATE_SEALED. A detached promise writing state rejects; if nobody catches it, Node terminates the process under its default configuration.
// Before: the promise writes state after rendering
load: ({ state }) => {
void loadStats().then((stats) => { state.get("stats", () => 0).value = stats; });
return {};
},// After: await it in the loader...
load: async ({ state }) => {
state.get("stats", () => 0).value = await loadStats();
return {};
},
// ...or return it as a deferred value
load: () => ({ stats: defer(loadStats()) }),Loader signal and prerendering with defer#
PageRequestContext.signal. No longerrequest.signal. Also aborted by session revocation, stream cancellation,HEADor a request error. If your code distinguished client disconnection by reading this signal, checksignal.reason.deferwithrender: "prerender". A rejected value fails the request with500, andmeeko buildstops with the affected route, instead of storing the error in HTML.prerenderRoutes. Takes the navigation payload from the document itself (#meeko-payload).
Sessions and Socket#
RequestContext.signal includes revocation#
RequestContext.signal combines the request signal with session revocation. It can abort with SessionRevokedError (SESSION_REVOKED, 401) even if the client remains connected. Pass it to fetch and queries, and call await context.session.verify() before an irreversible effect.
handler: async ({ context }) => {
await context.session.verify();
return payments.charge(order, { signal: context.signal });
},Session changes in a stream#
In a "stream" response whose headers have already been sent, SessionHandle.create, rotate and revoke reject with SESSION_LOCKED (500) without touching the store. Make session changes in the critical loader, not deferred work.
Broadcast contract#
hub.broadcast() resolves when the broker accepts the message and no longer rejects for delivery failures. It only rejects in these cases:
- the event does not exist;
- the payload fails its schema;
- the message, serialized with
serializeJson, exceedsmaxMessageBytes(SOCKET_LIMIT_EXCEEDED,413); - the broker does not accept publication.
When a peer fails, Meeko closes it with 1008, 1009, 1011 or 1013 and sends the error to onDeliveryError. An authorizeSend returning false skips that peer without closing it. LocalSocketPubSub.publish sends listener errors to onListenerError instead of aggregating them.
// Before: broadcast rejection reported failed peers
try {
await hub.broadcast("chat", "general", "message", payload);
} catch (error) {
log(error);
}// After: server/runtime.ts observes deliveries
export default defineServerRuntime(() => ({
sessions,
socket: { onDeliveryError: (error, { channel, room, event }) => log(error, channel, room, event) },
}));Without socket.onDeliveryError, delivery failures reach the runtime's onError.
SocketPubSub.subscribe can be asynchronous#
subscribe can return Unsubscribe | Promise<Unsubscribe>, and Unsubscribe can return a promise. Your own broker can adopt that signature, and code calling subscribe directly must await the result:
// Before
const unsubscribe = pubsub.subscribe("chat", "general", listener);
unsubscribe();
// After
const unsubscribe = await pubsub.subscribe("chat", "general", listener);
await unsubscribe();Protocol#
The hydration and navigation payload moves to v: 2, with these changes:
- adds
render,headand thedeferredlist, withnullat each deferredpath; "stream"routes respond to navigation withapplication/meeko+ndjson;- the client explicitly rejects a payload without a version or with a different one (including
v: 1).
Server and browser must come from the same build. Regenerate prerendered index.meeko.json files with meeko build and invalidate the CDN cache on deployment if they are cached there.
DevTools#
The DevTools protocol moves to version 1:
-
DevtoolsHookandDevtoolsSnapshot. Both requireprotocolVersion, and the hook also requiressetSignal. A manually constructed hook or snapshot, for example in tests, must add them:tsconst hook: DevtoolsHook = { protocolVersion: 1, snapshot: () => ({ protocolVersion: 1, instances: [], events: [], selectedId: null }), subscribe: () => () => {}, select() {}, highlight() {}, setBreakpoint() {}, setSignal: () => ({ ok: false, error: "Not supported in this test" }), }; -
DevtoolsReadSite.kind. Also acceptscomputed,if,eachandprop. An exhaustiveswitchovertext/attributemust cover them. -
Panels. The built-in panel and Chrome extension 1.0.0 reject runtimes without
protocolVersion. Use extension 1.0.0 with Meeko 1.0, and the previous extension with earlier runtimes.
Editor#
- VS Code. The
meeko-languageextension 1.0.0 is a language server client. It uses the project's@meekojs/meekoserver when installed (meeko.languageServer.preferWorkspace), and its bundled server as a fallback or in restricted mode. Install extension 1.0.0 and add@meekojs/meekoas a project dependency so the editor and CLI use the same version. - Other editors. Can use the same server with
meeko lsp --stdioormeeko-language-server --stdio; editors.md explains configuration for Neovim, Helix, Zed, JetBrains and Sublime Text. - Module resolution.
meeko checknow applies module resolution options from the package root'stsconfig.json(paths,baseUrl,types,typeRoots,customConditions,resolveJsonModule,allowImportingTsExtensions), just like the editor. An alias that previously failed withTS2307in the CLI now resolves, and type diagnostics hidden by that error may appear.