This guide walks through migrating an existing Litro project from one adapter to another. The adapter is a per-project choice — you pick one framework for the entire project.
When switching adapters, you need to update:
app.ts — different hydration/runtime importsnitro.config.ts — different externals and esbuild settingspackage.json — different framework dependenciesWhat does not change:
server/api/ handlersdefinePageData() exportsroutes.generated.ts (auto-generated)litro.recipe.json / content directorylitro build, litro dev, litro preview)pnpm remove lit @lit-labs/ssr @lit-labs/ssr-client
pnpm add @microsoft/fast-element @microsoft/fast-ssr
app.ts// Before (Lit)
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
import '@beatzball/litro/runtime/LitroOutlet.js';
import '@beatzball/litro/runtime/LitroLink.js';
// After (FAST)
import '@microsoft/fast-element/install-element-hydration.js';
import '@beatzball/litro/adapter/fast/runtime';
// Before (Lit)
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { LitroPage } from '@beatzball/litro/runtime';
@customElement('page-home')
export class HomePage extends LitroPage {
render() {
const data = this.serverData as { message: string } | null;
return html`<h1>${data?.message ?? 'Loading...'}</h1>`;
}
}
// After (FAST)
import { observable, html, css } from '@microsoft/fast-element';
import { LitroPage } from '@beatzball/litro/adapter/fast/page';
export class HomePage extends LitroPage {
@observable override serverData: { message: string } | null = null;
}
HomePage.define({
name: 'page-home',
template: html<HomePage>`
<h1>${x => x.serverData?.message ?? 'Loading...'}</h1>
`,
});
nitro.config.tsRemove Lit-specific externals and esbuild settings:
// Remove these Lit-specific settings:
externals: { inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'] },
esbuild: {
options: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
useDefineForClassFields: false,
},
},
},
},
FAST packages stay external by default (no externals config needed).
pnpm remove lit @lit-labs/ssr @lit-labs/ssr-client
pnpm add @elenajs/core
app.ts// Before (Lit)
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
import '@beatzball/litro/runtime/LitroOutlet.js';
import '@beatzball/litro/runtime/LitroLink.js';
// After (Elena)
import '@beatzball/litro/adapter/elena/runtime';
// Before (Lit)
import { html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import { LitroPage } from '@beatzball/litro/runtime';
@customElement('page-home')
export class HomePage extends LitroPage {
static styles = css`:host { display: block; }`;
render() {
const data = this.serverData as { message: string } | null;
return html`<h1>${data?.message ?? 'Loading...'}</h1>`;
}
}
// After (Elena)
import { html } from '@elenajs/core';
import { LitroPage } from '@beatzball/litro/adapter/elena/page';
export class HomePage extends LitroPage {
static override tagName = 'page-home';
render() {
const data = this.serverData as { message: string } | null;
return html`<h1>${data?.message ?? 'Loading...'}</h1>`;
}
}
HomePage.define();
@scopeShadow DOM styles become @scope rules in your global stylesheet:
/* Before: inside the component (Shadow DOM) */
static styles = css`
:host { display: block; padding: 2rem; }
h1 { color: #1a1a2e; }
`;
/* After: in a global CSS file */
@scope (page-home) {
:scope { display: block; padding: 2rem; }
h1 { color: #1a1a2e; }
}
nitro.config.tsRemove all Lit-specific settings (externals, esbuild decorator config). Elena has no special Nitro requirements.
Follow the same pattern as Lit to Elena, but replace FAST-specific code:
@microsoft/fast-element imports become @elenajs/coreComponentClass.define({ name, template, styles }) becomes static tagName + render() method + .define()${x => x.prop} to ${this.prop}@observable decorators, use Elena's static propsunsafeHTML() in Lit, use Elena's unsafeHTML() from @elenajs/core@observable and jiti — if your Lit pages used @property with SSR, switch to Observable.defineProperty() in FAST (jiti cannot process @observable)definePageData stays unchanged — it's adapter-agnostic, don't touch itfetchData() stays unchanged — same method signature across all adapters