Migrating from 1.6 to 1.7
Version 1.7 replaces the SPA router layer, switches from a flat route manifest to a tree structure, and separates bundling from the Runtime. Your routes, pages, widgets, and companion files stay exactly the same — the changes are in server setup and SPA initialization.
Quick summary
| 1.6 | 1.7 |
|---|---|
SpaHtmlRouter / SpaHashRouter | EmrouteApp via bootEmrouteApp() |
RoutesManifest (flat array) | RouteNode (tree) |
runtime.bundle() | buildClientBundles() |
RuntimeConfig.entryPoint | BuildOptions.entryPoint |
emroute:routes / emroute:widgets virtual modules | JSON manifests fetched at boot |
createSpaHtmlRouter(manifest) | bootEmrouteApp() |
SsrHtmlRouter.render(pathname) | SsrHtmlRouter.render(url, signal) |
Server setup
Before (1.6)
import { Emroute } from '@emkodev/emroute/server';
import { BunFsRuntime } from '@emkodev/emroute/runtime/bun/fs';
const runtime = new BunFsRuntime('.', {
routesDir: '/routes',
widgetsDir: '/widgets',
entryPoint: '/main.ts', // bundling config lived on runtime
});
const emroute = await Emroute.create({
spa: 'root',
markdownRenderer: { render },
}, runtime);
Bun.serve({ fetch: (req) => emroute.handleRequest(req) ?? new Response('Not Found', { status: 404 }) });
After (1.7)
import { Emroute } from '@emkodev/emroute/server';
import { buildClientBundles } from '@emkodev/emroute/server/build';
import { BunFsRuntime } from '@emkodev/emroute/runtime/bun/fs';
const runtime = new BunFsRuntime('.', {
routesDir: '/routes',
widgetsDir: '/widgets',
// entryPoint removed — bundling is a separate step
});
// Build client bundles (transpile+merge modules, bundle consumer main.ts)
await buildClientBundles({
runtime,
root: import.meta.dirname!,
spa: 'root',
// entryPoint: '/main.ts', // optional, defaults to '/main.ts'
});
const emroute = await Emroute.create({
spa: 'root',
markdownRenderer: { render },
}, runtime);
Bun.serve({ fetch: (req) => emroute.handleRequest(req) ?? new Response('Not Found', { status: 404 }) });
Key changes:
entryPointmoves fromRuntimeConfigtobuildClientBundles()- Call
buildClientBundles()beforeEmroute.create() - The build step transpiles
.tsmodules to.js, inlines companion files, and updates manifests — the server reads the processed.jsfiles
Consumer main.ts
Before (1.6)
import { routesManifest } from 'emroute:routes';
import { widgetsManifest } from 'emroute:widgets';
import { ComponentElement, createSpaHtmlRouter } from '@emkodev/emroute/spa';
for (const widget of widgetsManifest.widgets) {
ComponentElement.register(widget, widgetsManifest.moduleLoaders);
}
await createSpaHtmlRouter(routesManifest);
After (1.7)
import { bootEmrouteApp, MarkdownElement } from '@emkodev/emroute/spa';
import { renderMarkdown } from '@emkodev/emkoma/render';
MarkdownElement.setRenderer({ render: renderMarkdown });
await bootEmrouteApp();
Key changes:
- No more
emroute:routes/emroute:widgetsvirtual modules — manifests are fetched as JSON at boot time - No manual widget registration —
bootEmrouteApp()handles everything - Set up your markdown renderer before calling
bootEmrouteApp() - If you don't provide a
main.ts, one is auto-generated (without markdown renderer — add your own if you have.mdpages)
Route format
The route manifest changed from a flat array to a tree. If you only use Emroute.create() and let it read manifests from the runtime, this is transparent — the runtime produces the new format automatically.
If you construct manifests programmatically:
Before (1.6)
const manifest: RoutesManifest = {
routes: [
{ pattern: '/', type: 'page', modulePath: '/routes/index.page.ts', files: {} },
{ pattern: '/about', type: 'page', modulePath: '/routes/about.page.ts', files: { md: '...' } },
{ pattern: '/users/:id', type: 'page', modulePath: '/routes/users/[id].page.ts', files: {} },
],
errorBoundaries: [{ pattern: '/', modulePath: '/routes/index.error.ts' }],
};
const emroute = await Emroute.create({ routesManifest: manifest }, runtime);
After (1.7)
const routeTree: RouteNode = {
files: { ts: '/routes/index.page.ts' },
errorBoundary: '/routes/index.error.ts',
children: {
about: { files: { md: '/routes/about.page.md' } },
},
dynamic: {
param: 'id',
child: { files: { ts: '/routes/users/[id].page.ts' } },
},
};
const emroute = await Emroute.create({ routeTree }, runtime);
Base paths
BasePath gained an app field for the SPA shell endpoint:
Before (1.6)
const emroute = await Emroute.create({
basePath: { html: '/html', md: '/md' },
}, runtime);
After (1.7)
const emroute = await Emroute.create({
basePath: { html: '/html', md: '/md', app: '/app' },
}, runtime);
The default is { html: '/html', md: '/md', app: '/app' }. In root and only modes, bare paths (e.g. /, /about) now redirect to /app/* instead of /html/*.
Runtime changes
Removed from Runtime
| Method | Replacement |
|---|---|
runtime.bundle() | buildClientBundles({ runtime, root, spa }) |
Runtime.compress() | Removed (use native compression) |
Runtime.stopBundler() | esbuild.stop() is called internally |
runtime.writeShell() | Handled by buildClientBundles() |
Removed from RuntimeConfig
| Field | Replacement |
|---|---|
entryPoint | BuildOptions.entryPoint |
bundlePaths | BuildOptions.bundlePaths |
spa | BuildOptions.spa |
Added to Runtime
| Method | Purpose |
|---|---|
transpile(source) | TypeScript → JavaScript (instance method, not static) |
If you have a custom Runtime, implement transpile() to enable per-file module merging during the build step.
Removed exports
These are no longer available from @emkodev/emroute/spa:
SpaHtmlRouter,createSpaHtmlRouter(),SpaHtmlRouterOptionsSpaHashRouter/HashRouter,createHashRouter(),HashRouterOptions,HashRouteConfigRoutesManifest,RouteConfig,RouteInfo,ErrorBoundary,RouteFileType
These are no longer available from @emkodev/emroute:
RoutesManifestprefixManifest()
New exports
From @emkodev/emroute/spa:
EmrouteApp,createEmrouteApp(),EmrouteAppOptionsbootEmrouteApp(),BootOptions
From @emkodev/emroute:
RouteNode,RouteFilesRouteResolver,ResolvedRouteRouteTrie
From @emkodev/emroute/server/build:
buildClientBundles(),BuildOptions
From @emkodev/emroute/runtime/fetch:
FetchRuntime— browser-compatible Runtime for thin client
Hash router
SpaHashRouter / createHashRouter() is removed. If you used hash routing for embedded apps in leaf mode, use a lightweight hash router library or implement your own with window.addEventListener('hashchange', ...). emroute's hash router was 200 lines — most apps need far less.
SSR renderer signature
If you call htmlRouter.render() or mdRouter.render() directly:
Before (1.6)
const result = await emroute.htmlRouter.render('/about');
After (1.7)
const url = new URL('/about', 'http://localhost');
const result = await emroute.htmlRouter.render(url, AbortSignal.timeout(5000));
The render method now takes a full URL (for query string access) and an AbortSignal (for cancellation). If you only use handleRequest(), this change is transparent.
What didn't change
- Route files —
*.page.ts,*.page.html,*.page.md,*.page.csswork exactly the same - Widget files —
*.widget.tswith companion files, same lifecycle - PageComponent / WidgetComponent — same interface, same
getData,renderHTML,renderMarkdown,hydrate <mark-down>element — same API<widget-*>elements — same SSR hydration withssrattribute- Error boundaries —
*.error.tsfiles work the same - Redirects —
*.redirect.tsfiles work the same - Markdown rendering — same
MarkdownRendererinterface - Context provider — same
extendContexton server config handleRequest()composability — sameResponse | nullpattern