@mateosuarezdev/flash
v0.0.54
Published
Custom jsx runtime
Downloads
4,848
Maintainers
Readme
Flash
Version 0.0.46 — Fine-grained reactive JSX framework with zero VDOM overhead.
Flash is a lightweight JSX framework built on Legend State observables for fine-grained reactivity. Write familiar JSX, get direct DOM updates with no virtual DOM diffing. Ships with first-class SSR, hydration, SEO, MPA build utilities, and exit animations.
Table of contents
- Installation
- Quick Start
- Observables
- Signals API
- Reactive Boundaries
- Lifecycle Hooks
- Controlled Inputs
- Keyed Lists
- Context API
- Router
- Head Management
- Server-Side Rendering
- Bun Utilities
- Animations & Performance
- Package Entry Points
- API Reference
Installation
npm install @mateosuarezdev/flashConfigure tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@mateosuarezdev/flash"
}
}@legendapp/state is a bundled dependency — no separate install needed.
Quick Start
import { render, onMount } from "@mateosuarezdev/flash";
import { observable } from "@mateosuarezdev/flash";
const count = observable(0);
function Counter() {
onMount(() => console.log("mounted"));
return (
<div>
<h1>Count: {() => count.get()}</h1>
<button onClick={() => count.set(count.peek() + 1)}>Increment</button>
</div>
);
}
render(<Counter />, document.getElementById("app")!);Observables
Flash re-exports Legend State's observable primitives from the main entry point and from @mateosuarezdev/flash/signals (same module, two import paths).
import {
observable, // create an observable
observe, // run a reaction (re-runs when deps change)
isObservable, // type guard
OpaqueObject, // type wrapper — prevents Legend State from deep-transforming a value
} from "@mateosuarezdev/flash";Creating observables
const name = observable("World"); // Observable<string>
const count = observable(0); // Observable<number>
const user = observable<{ name: string; age: number } | null>(null);Reading and writing
name.get(); // tracked read — subscribes the current observer
name.peek(); // untracked read — no subscription
name.set("Flash"); // write
// Nested objects are automatically observable
user.set({ name: "Alice", age: 25 });
user.name.get(); // "Alice"
user.age.set(26); // granular update — only age subscribers re-runReactions
import { observe } from "@mateosuarezdev/flash";
const dispose = observe(() => {
console.log("name changed to:", name.get());
});
dispose(); // stop observingOpaqueObject
Prevents Legend State from deeply transforming a type (useful for storing DOM event objects or URL instances inside observables):
import { observable, OpaqueObject } from "@mateosuarezdev/flash";
type ChangeEvent = {
oldURL: OpaqueObject<URL>;
newURL: OpaqueObject<URL>;
};
const lastChange = observable<ChangeEvent | null>(null);Signals API
Flash ships a small compatibility layer in @mateosuarezdev/flash (re-exported from the state module) for handling props that may be static values, observables, or accessor functions.
Hybrid<T> — the unified prop type
import type { Hybrid } from "@mateosuarezdev/flash";
type Hybrid<T> = T | Observable<T> | (() => T);Any Flash prop can accept all three forms. The JSX runtime normalizes them automatically when binding to DOM elements — no adapter required at the call site.
const label = observable("Click me");
// All three are valid — runtime handles the rest:
<button>{() => label.get()}</button> // accessor
<button>{label}</button> // Observable directly
<button>{"Click me"}</button> // static stringReactiveProps<T> — reactive component props
Makes every prop on a custom component accept Hybrid<T>. Skips children, key, and ref.
import type { ReactiveProps } from "@mateosuarezdev/flash";
type ButtonProps = { label: string; disabled: boolean };
function Button(props: ReactiveProps<ButtonProps>) {
// props.label → string | Observable<string> | (() => string)
// props.disabled → boolean | Observable<boolean> | (() => boolean)
return <button disabled={props.disabled}>{props.label}</button>;
}
// Caller can mix static, observable, and accessor freely:
<Button label="Save" disabled={false} />
<Button label={labelObs} disabled={() => form.get().submitting} />$() — read inside reactive boundaries
When a Hybrid prop is an Observable, passing it into a () => reactive boundary as-is would pass the Observable object, not its value. $() converts it to () => obs.get() so the enclosing boundary tracks it.
import { $ } from "@mateosuarezdev/flash";
function Toggle(props: ReactiveProps<{ disabled: boolean; label: string }>) {
// $(props.disabled) is () => obs.get() if it's an Observable,
// or the raw value/function if it isn't.
return () => $(props.disabled)
? null
: <button>{$(props.label)}</button>;
}Static values and accessor functions pass through $() unchanged — always safe to call unconditionally.
peek() — non-tracking read
Read any Hybrid<T> exactly once without creating a subscription. Safe in event handlers and one-off logic.
import { peek } from "@mateosuarezdev/flash";
function Form(props: ReactiveProps<{ initialValue: string }>) {
let saved = "";
onMount(() => {
saved = peek(props.initialValue); // no subscription
});
return <input value={props.initialValue} />;
}| Utility | Observable | () => T | Static value |
| -------------------- | ----------------- | ------------------- | ------------ |
| $(value) | () => obs.get() | passed through | passed through |
| peek(value) | obs.peek() | calls it once | returns as-is |
Reactive Boundaries
Reactive boundaries are plain () => functions in JSX. Flash subscribes to any .get() calls made inside them and re-runs only that boundary when a dep changes.
const name = observable("World");
function Greeting() {
return (
<div>
{/* Re-runs when name changes */}
<h1>Hello {() => name.get()}!</h1>
{/* Conditional rendering — entire branch swaps on change */}
{() =>
name.get() === "World"
? <p>Welcome!</p>
: <p>Hello, {name.get()}!</p>
}
</div>
);
}Reactive props on DOM elements are also boundaries — Flash wraps them automatically:
const isDark = observable(false);
<button
class={() => isDark.get() ? "dark" : "light"}
disabled={() => !isDark.get()}
>
Toggle
</button>Lifecycle Hooks
onMount
Runs once after the component's DOM is attached.
function Component() {
let el: HTMLDivElement;
onMount(() => {
console.log("mounted:", el);
});
return <div ref={(node) => (el = node)}>Content</div>;
}onUnmount
Runs when the component is removed from the DOM.
function Component() {
onUnmount(() => {
clearInterval(timer);
});
return <div>Content</div>;
}onBeforeExit
Runs before unmounting and pauses the entire unmount tree until the async callback resolves. Use this for exit animations, saving data, or confirming before removal.
import { animate } from "framer-motion";
function FadeBox() {
let ref: HTMLElement;
onBeforeExit(async (token) => {
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 });
// Handle rapid re-mount: cancel and reverse the animation
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1 }, { duration: 0.15 });
});
await animation.finished;
});
return <div ref={(el) => (ref = el)}>Fading content</div>;
}DOM resurrection — if the component is toggled back on while exiting, Flash cancels the onBeforeExit callback via token.onCancel and reuses the existing DOM, giving you smooth reversals for free.
Controlled Inputs
For <input> elements with a reactive value or checked, Flash sets the DOM property (not the HTML attribute), keeping the input controlled:
const text = observable("hello");
<input
value={text}
onInput={(e) => text.set((e.target as HTMLInputElement).value)}
/>Keyed Lists
Use the key prop for efficient list rendering. Flash handles insert, remove, reorder, and update without re-rendering unaffected items.
const items = observable([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
]);
function List() {
return (
<ul>
{() => items.get().map((item) => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Context API
Share values across the component tree without prop drilling.
import { createContext, useContext } from "@mateosuarezdev/flash";
const ThemeContext = createContext<"light" | "dark">("light");
function App() {
ThemeContext.provide("dark");
return <Child />;
}
function Child() {
const theme = useContext(ThemeContext); // "dark"
return <div class={theme}>Content</div>;
}provide() sets the value for the subtree rendered below the calling component. useContext() walks up to the nearest provider.
Router
Import from @mateosuarezdev/flash/router. The router is client-side only — wrap your app in <Router> to start tracking navigation.
import {
Router,
pathname,
push,
replace,
back,
onUrlChange,
} from "@mateosuarezdev/flash/router";Setup
function App() {
return (
<Router>
{() => {
switch (pathname.get()) {
case "/": return <Home />;
case "/about": return <About />;
default: return <NotFound />;
}
}}
</Router>
);
}Reactive pathname
pathname is an Observable<string> — read it inside a () => boundary to react to navigation:
import { pathname } from "@mateosuarezdev/flash/router";
function NavLink({ href, label }: { href: string; label: string }) {
return (
<a
href={href}
class={() => pathname.get() === href ? "active" : ""}
onClick={(e) => { e.preventDefault(); push(href); }}
>
{label}
</a>
);
}Programmatic navigation
push("/dashboard"); // new history entry
replace("/login"); // replace current entry
back(); // history.back()URL change events
import { onUrlChange } from "@mateosuarezdev/flash/router";
function UnsavedGuard() {
onUrlChange((event) => {
if (event && hasUnsavedChanges()) {
// event.preventDefault() blocks the navigation
event.preventDefault();
}
});
return <form>...</form>;
}The router intercepts pushState, replaceState, popstate, and beforeunload and dispatches a unified urlchangeevent. onUrlChange auto-disposes when the component unmounts.
Head Management
Import head utilities from @mateosuarezdev/flash/framework.
setSeo()
Works in both environments:
- Server-side (SSR): writes into the
SeoCollectorprovided byWithModules— collected data is later injected byinjectFlashInternals. - Client-side: upserts
<meta>,<link>, and<title>tags directly in the DOM.
import { setSeo } from "@mateosuarezdev/flash/framework";
function ProductPage({ product }: { product: Product }) {
setSeo({
title: product.name,
description: product.description,
canonical: `https://example.com/products/${product.slug}`,
og: {
type: "product",
image: product.imageUrl,
},
jsonLd: {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
},
});
return <main>...</main>;
}Merging vs replacing:
// Merge with existing tags (default)
setSeo({ title: "Updated title" });
// Replace all managed tags (use when navigating to a completely new page)
setSeo({ title: "New page", description: "..." }, { merge: false });Full SeoData shape:
type SeoData = {
title?: string;
description?: string;
keywords?: string;
author?: string;
robots?: string;
language?: string;
canonical?: string;
alternate?: { hrefLang?: string; href?: string }[];
revisitAfter?: string;
geo?: { region?: string; placename?: string; position?: string; ICBM?: string };
og?: {
type?: string; url?: string; title?: string; description?: string;
image?: string; imageAlt?: string; locale?: string; siteName?: string;
};
twitter?: {
card?: string; site?: string; creator?: string;
url?: string; title?: string; description?: string; image?: string;
};
jsonLd?: JsonLdData;
};Seo / Styles / Scripts
Server-only JSX components — emit collected data during SSR and produce no output on the client (hydration skips them).
import { Seo, Styles, Scripts } from "@mateosuarezdev/flash/framework";
function EntryServer() {
return (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Seo /> {/* → collected <meta>, <title>, <link>, JSON-LD */}
<Styles /> {/* → <link rel="stylesheet" href="__FLASH_STYLES__" /> */}
<Scripts /> {/* → <script type="module" src="__FLASH_SCRIPTS__"></script> */}
</head>
<body>...</body>
</html>
);
}__FLASH_STYLES__ and __FLASH_SCRIPTS__ are placeholder strings replaced by injectFlashInternals.
WithModules
Root SSR wrapper that provides language and SEO context to the render tree. Must wrap renderToString / renderToStringAsync / renderToStream calls.
import {
WithModules,
createSeoCollector,
} from "@mateosuarezdev/flash/framework";
import { renderToString } from "@mateosuarezdev/flash/server";
const seoCollector = createSeoCollector();
const rawHtml = renderToString(
<WithModules lang="en" seoCollector={seoCollector}>
<EntryServer pathname={pathname} />
</WithModules> as any,
);After rendering, seoCollector.current contains all SEO data accumulated by setSeo() calls in the tree.
injectFlashInternals()
Replaces the __FLASH_STYLES__, __FLASH_SCRIPTS__, and __FLASH_SEO__ placeholder strings in the rendered HTML with the actual asset paths and collected SEO tags. In development mode it also injects a window.process polyfill so browser-served node_modules (like Legend State) have access to process.env.NODE_ENV.
import { injectFlashInternals } from "@mateosuarezdev/flash/framework";
const finalHtml = injectFlashInternals(rawHtml, {
stylesPath: "/assets/styles.css",
scriptsPath: "/src/entry-client.tsx",
seoData: seoCollector?.current,
});| Option | Type | Description |
| ------------ | --------- | -------------------------------------------------------- |
| stylesPath | string | Replaces __FLASH_STYLES__ in <Styles /> |
| scriptsPath| string | Replaces __FLASH_SCRIPTS__ in <Scripts /> |
| seoData | SeoData?| Replaces __FLASH_SEO__ placeholder from <Seo /> |
Server-Side Rendering
Import from @mateosuarezdev/flash/server.
renderToString() — synchronous
import { renderToString } from "@mateosuarezdev/flash/server";
const html = renderToString(<App />);
// Returns: complete HTML string, no async supportrenderToStringAsync() — waits for async components
import { renderToStringAsync } from "@mateosuarezdev/flash/server";
const html = await renderToStringAsync(<App />);
// Returns: complete HTML with all async components resolvedrenderToStream() — progressive
import { renderToStream } from "@mateosuarezdev/flash/server";
const stream = renderToStream(<App />);
for await (const chunk of stream) {
response.write(chunk);
}Full dev server example
// index.tsx
import { renderToString } from "@mateosuarezdev/flash/server";
import {
WithModules,
createSeoCollector,
injectFlashInternals,
} from "@mateosuarezdev/flash/framework";
import {
buildSrcFileMap,
buildRouteMap,
matchRoute,
createTranspiler,
resolveImports,
} from "@mateosuarezdev/flash/bun";
import EntryServer from "./src/entry-server";
const transpiler = createTranspiler();
const srcFiles = await buildSrcFileMap("src");
const routes = await buildRouteMap("src/routes");
const server = Bun.serve({
port: 5173,
routes: {
"/*": async (req) => {
const url = new URL(req.url);
const pathname = url.pathname;
const match = matchRoute(routes, pathname);
if (match) {
const seoCollector = createSeoCollector();
const rawHtml = renderToString(
<WithModules lang="en" seoCollector={seoCollector}>
<EntryServer pathname={pathname} params={match.params} />
</WithModules> as any,
);
const finalHtml = injectFlashInternals(rawHtml, {
stylesPath: "/assets/styles.css",
scriptsPath: "/src/entry-client.tsx",
seoData: seoCollector?.current,
});
return new Response(finalHtml, {
headers: { "Content-Type": "text/html" },
});
}
// Serve transpiled source files
const filePath = srcFiles.get(pathname);
if (!filePath) return new Response("Not Found", { status: 404 });
const code = await Bun.file(filePath).text();
const loader = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? "tsx" : "ts";
let transpiled = transpiler.transformSync(code, loader);
transpiled = await resolveImports(transpiled, filePath);
return new Response(Bun.gzipSync(transpiled), {
headers: {
"Content-Type": "application/javascript",
"Content-Encoding": "gzip",
},
});
},
},
});Bun Utilities
Import from @mateosuarezdev/flash/bun. These utilities require a Bun runtime (optional peer dependency).
Dev Server Helpers
createTranspiler()
Creates a Bun.Transpiler pre-configured for Flash JSX, browser target, and public env defines.
import { createTranspiler } from "@mateosuarezdev/flash/bun";
const transpiler = createTranspiler({
jsxImportSource: "@mateosuarezdev/flash", // default
define: { "MY_CONSTANT": JSON.stringify("value") },
});
const js = transpiler.transformSync(tsxSource, "tsx");getPublicEnvDefines()
Returns a Bun-compatible define map with process.env.NODE_ENV plus any PUBLIC_* environment variables. Safe to pass to Bun.build or Bun.Transpiler.
import { getPublicEnvDefines } from "@mateosuarezdev/flash/bun";
const define = getPublicEnvDefines();
// { "process.env.NODE_ENV": '"development"', "process.env.PUBLIC_API_URL": '"..."' }buildSrcFileMap()
Scans a source directory and builds a Map<urlPath, filePath> for serving files from the dev server. Registers paths with extension, without extension, and directory-index shorthands.
import { buildSrcFileMap } from "@mateosuarezdev/flash/bun";
const files = await buildSrcFileMap("src");
// "/src/routes/_route.tsx" → "src/routes/_route.tsx"
// "/src/routes/_route" → "src/routes/_route.tsx"resolveImports()
Rewrites bare-specifier and relative imports in transpiled JS to browser-fetchable URLs. Resolves node_modules exports maps and adds file extensions to extensionless relative imports.
import { resolveImports } from "@mateosuarezdev/flash/bun";
let code = transpiler.transformSync(source, "tsx");
code = await resolveImports(code, filePath);
// from "@mateosuarezdev/flash" → from "/node_modules/@mateosuarezdev/flash/dist/index.js"
// from "./utils" → from "./utils.ts"generateHash()
Hashes a file's contents using Bun's built-in hasher — useful for cache-busting asset filenames.
import { generateHash } from "@mateosuarezdev/flash/bun";
const hash = await generateHash("./public/assets/styles.css");
// "a3f8b2c1"distPathToHref()
Converts an absolute output file path into a web-relative href from the dist folder.
import { distPathToHref } from "@mateosuarezdev/flash/bun";
distPathToHref("C:/project/dist/assets/index-abc123.js");
// → "/assets/index-abc123.js"Route Map
File-system-based route scanning. Directory names wrapped in [brackets] become :params.
src/routes/
_route.tsx → "/"
review/_route.tsx → "/review"
blog/[slug]/_route.tsx → "/blog/:slug"
shop/[id]/edit/ → "/shop/:id/edit"buildRouteMap()
Scans for _route.{tsx,jsx} files and returns a sorted route manifest. More specific routes (fewer dynamic segments) sort first, so /products/featured always beats /products/:id.
import { buildRouteMap } from "@mateosuarezdev/flash/bun";
const routes = await buildRouteMap("src/routes");
// [
// { pattern: "/products/featured", filePath: "..." },
// { pattern: "/products/:id", filePath: "..." },
// { pattern: "/review", filePath: "..." },
// { pattern: "/", filePath: "..." },
// ]Specificity score: staticSegments × 1000 + totalSegments × 10 − dynamicSegments
matchRoute()
Matches a pathname against the route manifest, returning the first match and any extracted params.
import { matchRoute } from "@mateosuarezdev/flash/bun";
const match = matchRoute(routes, "/blog/hello-world");
// { route: { pattern: "/blog/:slug", ... }, params: { slug: "hello-world" } }
const miss = matchRoute(routes, "/does-not-exist");
// nullbuildMpa()
Orchestrates a full multi-page app build: cleans dist, copies public, runs the setup hook (CSS build, etc.), bundles client JS with Bun.build, discovers routes, renders each page to HTML, and writes index.html files.
import { buildMpa } from "@mateosuarezdev/flash/bun";
import {
WithModules,
createSeoCollector,
injectFlashInternals,
} from "@mateosuarezdev/flash/framework";
import { renderToString } from "@mateosuarezdev/flash/server";
import EntryServer from "../src/entry-server";
await buildMpa({
entrypoints: ["./src/entry-client.tsx"],
outDir: "./dist",
publicDir: "./public",
routesDir: "./src/routes",
// Runs after public copy, before JS bundling — good for CSS
async setup({ outDir }) {
await Bun.build({ entrypoints: ["./src/styles.css"], outdir: outDir });
},
// Called once per route — return the final HTML for that page
async renderPage(pathname, { bundleHref }) {
const seoCollector = createSeoCollector();
const rawHtml = renderToString(
<WithModules lang="en" seoCollector={seoCollector}>
<EntryServer pathname={pathname} />
</WithModules> as any,
);
return injectFlashInternals(rawHtml, {
stylesPath: "/assets/styles.css",
scriptsPath: bundleHref,
seoData: seoCollector?.current,
});
},
});Options:
| Option | Type | Default |
| ------------ | -------------------------------------------------------------------- | ------------------- |
| entrypoints| string[] | required |
| outDir | string | "./dist" |
| publicDir | string | "./public" |
| routesDir | string | "./src/routes" |
| define | Record<string, string> | getPublicEnvDefines() |
| setup | (ctx: { outDir: string }) => Promise<void> \| void | — |
| renderPage | (pathname: string, ctx: { bundleHref: string }) => Promise<string> \| string | required |
Animations & Performance
onBeforeExit with any animation library
Flash's exit animation model is library-agnostic:
// Framer Motion
import { animate } from "framer-motion";
onBeforeExit(async (token) => {
const anim = animate(ref, { opacity: 0 }, { duration: 0.3 });
token.onCancel(() => { anim.stop(); animate(ref, { opacity: 1 }); });
await anim.finished;
});
// CSS transitions
onBeforeExit(async () => {
ref.style.opacity = "0";
await new Promise((r) => setTimeout(r, 300));
});
// CSS classes
onBeforeExit(async () => {
ref.classList.add("exit");
await new Promise((r) => setTimeout(r, 300));
});Frame Scheduler
Batches DOM reads and writes into separate phases to prevent layout thrashing:
import { frame } from "@mateosuarezdev/flash";
// Separated phases
frame.read(() => { const h = el.offsetHeight; /* read */ });
frame.update(() => { position += velocity; });
frame.render(() => { el.style.transform = `translateY(${position}px)`; });
// Chained with data flow
frame.chain({
read: () => ({ h: el.offsetHeight, w: el.offsetWidth }),
render: ({ h, w }) => {
el.style.height = `${h * 2}px`;
el.style.width = `${w * 2}px`;
},
});
// Continuous animation loop
const stop = frame.render(() => {
el.style.transform = `rotate(${angle++}deg)`;
}, true);
frame.cancel(stop); // stop when doneFLIP Animations
Performant layout animations via the FLIP technique (animate transforms instead of layout properties):
import { flip, flipGroup } from "@mateosuarezdev/flash";
// Single element
flip(el, () => { el.classList.add("expanded"); }, { duration: 300 });
// Group reorder
flipGroup(
document.querySelectorAll(".item"),
() => { container.appendChild(items[2]); },
{ duration: 400, easing: "ease-out-cubic" },
);View Transitions API
import { startViewTransition } from "@mateosuarezdev/flash";
const expanded = observable(false);
<button onClick={() => startViewTransition(() => expanded.set(!expanded.peek()))}>
Toggle
</button>
<div
class={() => expanded.get() ? "expanded" : "collapsed"}
viewTransitionName="container"
>
Content
</div>Package Entry Points
| Entry point | Contents |
| ----------------------------------- | ---------------------------------------------------------------------------- |
| @mateosuarezdev/flash | Runtime, context API, lifecycle hooks, observables, frame scheduler, FLIP |
| @mateosuarezdev/flash/server | renderToString, renderToStringAsync, renderToStream |
| @mateosuarezdev/flash/framework | setSeo, createSeoCollector, Seo, Styles, Scripts, WithModules, injectFlashInternals |
| @mateosuarezdev/flash/router | Router, pathname, push, replace, back, onUrlChange |
| @mateosuarezdev/flash/bun | buildRouteMap, matchRoute, buildMpa, createTranspiler, buildSrcFileMap, resolveImports, getPublicEnvDefines, generateHash, distPathToHref |
| @mateosuarezdev/flash/utils | Shared utility types |
| @mateosuarezdev/flash/debug | Debug logging helpers |
API Reference
Core (@mateosuarezdev/flash)
Runtime
| Export | Description |
| --------------------------- | ----------------------------------------------------- |
| render(el, container) | Mount component tree to the DOM |
| hydrate(el, container) | Hydrate server-rendered HTML |
| Fragment | Render multiple children without a wrapper |
Lifecycle
| Export | Description |
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
| onMount(cb) | Runs after component's DOM is attached |
| onUnmount(cb) | Runs when component is removed from DOM |
| onBeforeExit(async (token) => {}) | Pauses unmount tree — use for exit animations, data saving |
Context
| Export | Description |
| --------------------------- | ----------------------------------------------------- |
| createContext(default) | Create a typed context |
| useContext(ctx) | Read nearest context value |
Observables (re-exported from Legend State)
| Export | Description |
| ------------------- | ----------------------------------------------------- |
| observable(value) | Create an Observable<T> |
| observe(fn) | Run a reactive effect, returns dispose function |
| isObservable(v) | Type guard for Observable<T> |
| OpaqueObject<T> | Type wrapper preventing deep Legend State transform |
Signals helpers
| Export | Description |
| -------------------- | ----------------------------------------------------- |
| Hybrid<T> | T \| Observable<T> \| (() => T) |
| HybridReactive<T> | T \| (() => T) — what applyProps understands |
| ReactiveProps<T> | Maps all props to Hybrid<T>, skips children/key/ref |
| $(value) | Convert Hybrid<T> → HybridReactive<T> for () => boundaries |
| peek(value) | Non-tracking read of any Hybrid<T> |
Animations & Performance
| Export | Description |
| ----------------------------------- | ---------------------------------------------------- |
| startViewTransition(cb) | Trigger View Transitions API |
| frame.read(cb) | Schedule DOM reads |
| frame.update(cb) | Schedule computations |
| frame.render(cb, keepAlive?) | Schedule DOM writes |
| frame.chain({ read, update, render }) | Chained phases with typed data flow |
| frame.cancel(cb) | Cancel a keep-alive render callback |
| flip(el, applyFn, opts) | FLIP animation for a single element |
| flipGroup(els, applyFn, opts) | FLIP animation for a list of elements |
| flipMove(el, newParent, opts) | Animate an element moving to a new container |
Special JSX props
| Prop | Description |
| ----------------------- | ----------------------------------------------------- |
| ref={(el) => ...} | DOM element reference callback |
| key={value} | Unique identifier for keyed list items |
| viewTransitionName | Named view transition target |
Framework (@mateosuarezdev/flash/framework)
| Export | Description |
| ------------------------------- | -------------------------------------------------------------------- |
| setSeo(data, options?) | Accumulate SEO data (SSR) or update <head> tags (client) |
| createSeoCollector() | Create an empty SeoCollector to pass to WithModules |
| <Seo /> | Server-only — renders collected SEO tags into HTML |
| <Styles /> | Server-only — renders <link rel="stylesheet" href="__FLASH_STYLES__" /> |
| <Scripts /> | Server-only — renders <script> tag with __FLASH_SCRIPTS__ |
| WithModules | Root SSR wrapper — provides lang and seoCollector context |
| injectFlashInternals(html, opts) | Replace Flash placeholders with real paths and inject SEO + process polyfill |
Router (@mateosuarezdev/flash/router)
| Export | Description |
| --------------------- | ------------------------------------------------ |
| Router | Provider component — starts tracking navigation |
| pathname | Observable<string> — current pathname |
| push(to) | history.pushState |
| replace(to) | history.replaceState |
| back() | history.back() |
| onUrlChange(cb) | Subscribe to URL changes, auto-disposes on unmount |
Bun (@mateosuarezdev/flash/bun)
| Export | Description |
| ----------------------------------- | ------------------------------------------------------- |
| getPublicEnvDefines() | define map for Bun.build / Bun.Transpiler |
| createTranspiler(opts?) | Bun transpiler pre-configured for Flash JSX |
| buildSrcFileMap(dir) | URL → file path map for dev server |
| resolveImports(code, filePath) | Rewrite imports to browser-fetchable URLs |
| generateHash(filePath) | 8-char content hash for cache-busting |
| distPathToHref(filePath) | Absolute dist path → /-relative web href |
| buildRouteMap(routesDir?) | Scan _route.tsx files → sorted Route[] manifest |
| matchRoute(routes, pathname) | Match pathname → { route, params } or null |
| buildMpa(options) | Full MPA build pipeline |
License
MIT © Mateo Suarez
