@ecopages/ecopages-jsx
Integration plugin for @ecopages/jsx templates in Ecopages. Use it when Ecopages JSX should own .tsx routes, Radiant-backed web components, or MDX compiled against the @ecopages/jsx runtime.
Installation
bun add @ecopages/ecopages-jsx @ecopages/jsx @ecopages/radiant
@ecopages/jsx and @ecopages/radiant are required peer dependencies for this package.
Usage
Register the ecopagesJsxPlugin in your eco.config.ts.
import { defineConfig } from '@ecopages/core/config';
import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
export default defineConfig({ integrations: [ecopagesJsxPlugin()] });
What This Integration Owns
.tsxroute files by default. Useextensionsto change the JSX route suffix list.- Optional Radiant SSR/runtime wiring through the public
@ecopages/radiantentrypoints. - Optional
.mdxroutes compiled against the@ecopages/jsxruntime.
Route Extensions
Use extensions when JSX routes should use a custom suffix instead of the default .tsx.
ecopagesJsxPlugin({
extensions: ['.page.tsx'],
});
Development HMR
In development, editing a registered Ecopages JSX page refreshes the current page through the navigation runtime. The shared Layout stays mounted where the router can preserve it; elements marked with data-eco-persist retain their DOM state. Declared client scripts continue to use script HMR.
Radiant Support
Radiant support is enabled by default. When radiant: true, the plugin keeps the ownership split explicit:
- Ecopages JSX owns page-level JSX SSR and container hydration.
- Radiant SSR is activated on the server through
@ecopages/radiant/server/render-component. - Core installs Radiant's complete light-DOM surface at Integration setup, before any Page renders or defines an element, so production emit can import Radiant element modules. Lit's repairs run after that complete surface. The JSX Integration does not install DOM globals as an import side effect or on first render.
- Radiant host hydration is activated on the client through an explicit head bootstrap that imports
@ecopages/radiant/client/install-hydratorbefore intrinsic custom-element modules load.
That means server-rendered RadiantElement hosts hydrate in place only when both the SSR markers and the explicit client hydrator are present. Without the client hydrator, Radiant intentionally falls back to a fresh client render on first connect.
Hydration Semantics: RadiantElement vs. RadiantController
RadiantElement(DOM Preservation): Custom elements extendingRadiantElementsupport true in-place hydration. When@ecopages/radiant/client/install-hydratorexecutes before elements connect, existing light-DOM SSR nodes are preserved, retaining DOM node identity, form input state, active focus, and media playback.RadiantController(Activation / Template Re-mount): Controllers attached via[data-controller]activate whenstartControllers(document)is called. Activation executes the controller'srender()method and mounts its template fresh into the host container, replacing server-rendered child nodes. Reactive properties and signal bindings remain fully interactive, but individual child DOM nodes are reconstructed.- Guidance: Use
RadiantElementwhen in-place preservation of SSR nodes (e.g., focused inputs, media elements, or CSS transitions) is required. UseRadiantControllerfor Stimulus-style progressive enhancement on arbitrary markup where child DOM reconstruction is acceptable.
ecopagesJsxPlugin({
radiant: true,
});
Set radiant: false when your JSX pages do not need Radiant SSR or the Radiant browser runtime on a given app.
The plugin bootstrap is intentionally explicit rather than depending on custom-element modules to install the Radiant hydrator opportunistically.
For server-side custom-element examples, prefer explicit SSR modes over the older boolean-only contract.
new MyElement().renderHostToString({ mode: 'plain' });
new MyElement().renderHostToString({ mode: 'hydrate' });
The legacy hydrate: true option remains compatible, but mode is the current SSR contract. Registered intrinsic tags that contain a dash are SSR candidates, and framework-specific host rendering should be adapted through the server custom-element render hook instead of hardcoding framework branches into JSX page code.
The integration loads Radiant host serialization through the public @ecopages/radiant/server/radiant-element-ssr export after Core installs the shared server DOM at Integration setup. Applications do not need a separate Radiant SSR side-effect import.
Registered Radiant scripts (ssr: true)
Declare custom-element registration once in dependencies.scripts. Ecopages JSX preloads entries marked ssr: true on the server before render so hosts can SSR without a redundant import './my-element.script.ts' in the component file. Preload deduplication and invalidation state belongs to the app's AppRuntime, so separate apps keep separate preload history.
export const ThemeToggle = eco.component({
dependencies: {
scripts: [{ src: './theme-toggle.script.ts', ssr: true, lazy: { 'on:idle': true } }],
},
render: () => <theme-toggle />,
});
Keep import type when you only need props from the script module. Do not add ssr: true to browser-only scripts that touch window or document without guards.
MDX Support
Add an mdx object to treat .mdx files as JSX routes compiled against the @ecopages/jsx runtime. Omit the property to disable MDX. Put remarkPlugins, rehypePlugins, and recmaPlugins next to compilerOptions, not inside it.
import { defineConfig } from '@ecopages/core/config';
import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
export default defineConfig({
integrations: [
ecopagesJsxPlugin({
mdx: {
extensions: ['.mdx', '.md'],
},
}),
],
});
Mixed Rendering
Ecopages JSX can own the outer page shell or just a nested foreign subtree. When another integration reaches a JSX-owned foreign child, Ecopages hands that foreign subtree back to the JSX renderer so it can serialize the correct output before the outer renderer resumes.
Important:
- Cross-integration shell stacks should use
EcoEmbedfrom@ecopages/ecopages-jsx/eco-embed(or the matching integration adapter). Plain opaque objects fail fast at core foreign-subtree queue boundaries; they are not coerced withString(object). Pass already-serialized HTML whenEcoEmbedis not needed. - Components that may render foreign children must declare those children in
config.dependencies.components. - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not treat rendered HTML alone as the source of truth.
- Route-level dependency resolvers can supply Components at request time; Ecopages JSX includes those resolved roots when deciding whether a Foreign Subtree needs handoff.
- Ecopages JSX keeps raw-markup preservation inside the JSX renderer.
- JSX renderers use the required core
AssetProcessingServiceto process declared component dependencies.