# Lumiose > Lumiose is a web framework for everything from static sites and SPAs to full-stack apps: TSX with scoped CSS built in, signals, and 0 kB of JavaScript until a page needs it. These docs match lumiose 0.4.0. Installed projects have them in `node_modules/lumiose/docs`; `lumiose docs ` prints one and `lumiose explain ` explains a diagnostic. # Lumiose docs > Lumiose is a web framework for everything from static sites and SPAs to full-stack apps: TSX with scoped CSS built in, signals, and 0 kB of JavaScript until a page needs it. **Feels like React. Runs like vanilla JS.** Components live in `.stsx` files (TSX plus raw-CSS ` ``` - The function body runs **once**. `count()` reads the signal; JSX that reads it updates on its own. - The ` ``` Both components share the ` ``` `Counter` and `Reset` share the block: `button` matches the buttons of both, and of no other file. **Placement decides scope.** | Placement | Scope | | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | File top level (by convention, one block at the end of the file) | Every template in the file. | | Inside a component's JSX, as a child of an intrinsic element or fragment (any depth) | Its **enclosing element and everything written inside it** in that component, including `.map` callbacks and JSX passed as children there. A block directly under the component's root element therefore covers the whole component. Sibling subtrees are not affected, and it never reaches other components' own templates. | | ` {/* only the

of this

*/}
{/* reads the component's `gap` */} ); } ``` Everything else goes in the file-level block. Moving a block from a component's root to the end of the file widens its scope to every component in the file: check that its selectors (bare element selectors in particular) do not start matching another component's elements. **How scoping works.** Each scope with styles gets a token `data-l-`, a stable hash of the file path plus the component name, with one token per style-bearing element. Every element in the scope carries the token: the whole file for top-level blocks, and the enclosing element's lexical subtree for blocks inside a component. The last compound of each selector gets `[data-l-]`, a +0,1,0 specificity bump. Dynamic classes always work, because no element is pruned. - `:global(.sel)` leaves a selector unscoped; `:deep(.sel)` scopes only the part before it. - `@keyframes` names are scoped, and `animation` references are rewritten. A block sees its own keyframes, then those of the blocks of enclosing elements (innermost first), then the file's, so a nested block can animate with keyframes defined under the component's root. Custom properties are not scoped. - **Class passthrough.** When a parent passes `class` to a child component, the parent's scope tokens at that call site travel with it: the file token plus every enclosing style subtree. So `.cta { … }` in the parent styles ` ``` Or keep the Markdown as content and put the behaviour in a component that renders it, such as an island in a `.stsx` route: ```stsx import Post from "../content/post.md"; import { Counter } from "../components/counter.stsx"; export default function Page() { return (
); } ``` ## Related - [Markdown and HTML imports](../concepts/content.md) - [Islands](../concepts/islands.md) # LUM1009: Content nested too deeply **Severity:** error · **Category:** file format (1xxx) > A Markdown or HTML import nests elements or block quotes deeper than 512 levels. **Reported by:** the Vite plugin (`lumiose({ md, html })`), in `lumiose dev` and builds, at the first element or line past the limit. ## What it means The HTML tree builder and the Markdown block parser get slower with every level of nesting, and a file with tens of thousands of unclosed `
`s or `>` markers would take seconds to compile (or stall the dev server on every save). No real document nests this deep, so Lumiose checks the depth before parsing and stops at 512 levels: elements that are opened and never closed, or block quote markers on one line. Elements whose end tag is optional (`

`, `

  • `, ``…), void elements and self-closing tags do not count. The component is empty until the file is fixed. ## Example ```html
    …
    ``` ## How to fix it Close the elements you open, or generate flatter markup. For a quote of a quote of a quote, a few levels are plenty. ## Related - [Markdown and HTML imports](../concepts/content.md) - [Security](../concepts/security.md) # LUM1010: Source nested too deeply **Severity:** error · **Category:** file format (1xxx) > Brackets in a `.stsx`/`.tsx` file are nested more than 1000 levels deep, so the compiler refuses the file. **Reported by:** the compiler, in `@lumiose/vite`, `lumiose check` and the language server. An error. ## What it means Parentheses, brackets, braces and template substitutions (`${…}`) are counted while the file is read; strings and comments do not count. Past 1000 open levels the file is not compiled: the compiler's passes recurse once per level, and a file nested tens of thousands of levels deep would exhaust the stack of the process that compiles it (the dev server, the language server). Real code stays far below the limit; generated code (a serialized tree, a minified data blob) may not. ## Example ```ts // Generated: 5000 nested arrays. export const tree = [[[[[[/* … */]]]]]]; ``` ## How to fix it Split the expression into named parts, or load deeply nested data as JSON (`import data from "./tree.json"`, or `fetch`) instead of writing it as code. ## Related - [LUM1001](LUM1001.md): syntax error. # LUM2001: Stale derived value **Severity:** warning · **Category:** reactivity (2xxx) > A value derived from a signal is computed once in setup and never updates. **Reported by:** the compiler. A warning. Does not apply to async components, whose body re-runs. ## What it means A component's body runs once. `const doubled = count() * 2` reads the signal at setup time and stores a number, so JSX that uses `doubled` shows the first value forever. The compiler treats a zero-argument call of a local or imported binding (`count()`, `user.name()`) as a signal read, and so does a read of the component's props. Calls that can only run on the server are not reads, so server pages can store request data in constants: - `lumiose`'s request APIs: `url()`, `params()`, `locals()`, `cookies()`, `request()`, `response()` and `publicEnv()`. Inside SPA routes, `url()` and `params()` from `lumiose/router` are reactive and still count. - `lumiose`'s control-flow APIs, which never return: `notFound()`, `redirect()` and `error()`, so `const project = getProject(slug) ?? notFound()` is fine. - Anything imported from a server-only module (`*.server.ts`, `src/server/**`), such as `queries().getBook(isbn)`. Data functions from a workspace package (`getDashboard()` from `@acme/data`) are not known to be server-only: re-export them from a module under `src/server/` (`export { getDashboard } from "@acme/data";`) and import them from there. ```tsx // src/routes/books/[isbn]/page.stsx (a server page): no warning const user = locals().user ?? null; const next = url().searchParams.get("next"); const featured = queries().featuredBooks(6); ``` Pages and layouts whose route mode is `static` or `server` render once per request (or build), so their own props are plain values too: deriving from them is fine. Routes in `spa` mode, islands and other components re-render with new props and still get the warning. `@lumiose/vite`, `lumiose check` and the language server read the route modes the same way (`export const mode`, the nearest layout's, then `lumiose({ mode })`). ```tsx // src/routes/books/[slug]/page.stsx (mode `static` or `server`): no warning export default function Book(props: { params: { slug: string } }) { const title = props.params.slug.replaceAll("-", " "); return

    {title}

    ; } ``` ## Example ```tsx export function Counter() { const count = signal(1); const doubled = count() * 2; return ; } ``` ## How to fix it Derive with a function or `computed`, and call it where you need the value: ```tsx const doubled = () => count() * 2; // cheap derivations const total = computed(() => expensive(count())); // cached return ; ``` If you really want the initial value only, read it with `count.peek()` or `untrack(count)` to make that explicit. ## Related - [Signals and reactivity](../concepts/signals.md) - [Components](../concepts/components.md) # LUM2002: Signal write during render **Severity:** error · **Category:** reactivity (2xxx) > A signal is written while a component or computed is rendering. **Reported by:** the compiler. ## What it means Component setup and computeds run during render. Writing a signal there (`count.set(…)`, `items.push(…)` on a store) re-triggers bindings mid-render, can loop, and makes the output depend on render order. ## Example ```tsx export function Profile(props: { name: string }) { const visits = signal(0); visits.set(visits.peek() + 1); return

    {props.name}

    ; } ``` ## How to fix it Initialize the signal with the value instead (`signal(initial)`), derive with `computed`, or write in an event handler, `onMount` or an `effect`. ## Related - [Signals and reactivity](../concepts/signals.md) # LUM2003: Reactive early return **Severity:** error · **Category:** reactivity (2xxx) > A sync component returns early based on reactive state. **Reported by:** the compiler. ## What it means The body of a sync component runs once, so `if (!user()) return ` is decided once and never re-evaluated: the component keeps showing whichever branch it took first. ## Example ```tsx export function Account(props: { user: () => User | null }) { if (!props.user()) return ; return ; } ``` ## How to fix it Move the condition into the JSX, where it becomes a memoised branch: ```tsx export function Account(props: { user: () => User | null }) { return <>{props.user() ? : }; } ``` Early returns on props or values that never change are fine; async components may return early, because their body re-runs. ## Related - [Components](../concepts/components.md) - [JSX: compiled idioms](../concepts/jsx.md) # LUM2004: Component called as a function **Severity:** error · **Category:** reactivity (2xxx) > A component is called as a function instead of rendered with JSX. **Reported by:** the compiler. ## What it means `{Card({ title })}` runs `Card` inside the caller: it gets no owner of its own, its props are not reactive getters, and cleanups, context and boundaries attach to the wrong component. ## Example ```tsx export function List(props: { items: string[] }) { return
      {props.items.map((item) => Item({ label: item }))}
    ; } ``` ## How to fix it Render it with JSX: ``. To choose a component at runtime, use ``. ## Related - [Components](../concepts/components.md) # LUM2005: Children inserted twice **Severity:** warning · **Category:** reactivity (2xxx) > `children` is inserted more than once. **Reported by:** the compiler. A warning. ## What it means `children` is lazy: each insertion evaluates it, creating a second set of nodes (or moving the first). Components below are instantiated twice, with separate state. ## Example ```tsx export function Tabs(props: { children?: JSX.Element }) { return (
    {props.children}
    ); } ``` ## How to fix it Insert `children` once. To render content several times, accept a render function and call it in each place: ```tsx export function Tabs(props: { renderTab: () => JSX.Element }) { return (
    {props.renderTab()}
    ); } ``` ## Related - [Components](../concepts/components.md) # LUM2006: Reactive index escapes its callback **Severity:** warning · **Category:** reactivity (2xxx) > The reactive index of a `.map()` row is used outside its row. **Reported by:** the compiler. A warning. ## What it means In `{items().map((item, i) => …)}` the compiler makes `i` a reactive index that belongs to the row and updates when the row moves. Storing it in an outer variable, or capturing it in a callback that outlives the row, keeps a stale index. ## Example ```tsx let lastIndex = 0; export function List(props: { items: string[] }) { return (
      {props.items.map((item, i) => { lastIndex = i; return
    • {item}
    • ; })}
    ); } ``` ## How to fix it Use the index only inside the row's JSX and handlers. To remember a position, store the item (or its key) and look it up when needed. **Not reported** when the list maps a module-level `const` array literal (also with `as const`) that the module never changes: no reassignment, no `X[i] = …`, no `push`, `sort`, `splice` or other mutating method. Its rows never move, so reading the index once is safe: ```tsx const CHANNELS = [{ name: "pulse" }, { name: "wave" }] as const; export function Mixer() { return (
      {CHANNELS.map((info, i) => { const ch = i; // stable: CHANNELS never changes return (
    • {info.name} {ch}
    • ); })}
    ); } ``` ## Related - [JSX: compiled idioms](../concepts/jsx.md) # LUM2007: Own signal read in an async body **Severity:** warning · **Category:** reactivity (2xxx) > An async component reads a signal it created in its own body. **Reported by:** planned (not reported by this version). A warning. ## What it means Signals read in an async component's body are tracked, and a change re-runs the component. Signals created in the body reset on each run. Reading such a signal in the body therefore re-runs the component whenever it changes, and the re-run re-creates the signal with its initial value: the update is lost. ## Example ```tsx export async function Book({ id }: { id: string }) { const book = await fetchJSON(`/api/books/${id}`); const open = signal(false); if (open()) console.log("open"); return ; } ``` ## How to fix it Read the signal in JSX, handlers or effects, which are tracked separately from the body. If the value should survive re-runs, create the signal in a parent and pass it down. ## Related - [Async components](../concepts/async.md) # LUM3005: Unused scoped selector **Severity:** warning · **Category:** styles (3xxx) > A selector in a scoped `
  • ); } ``` ## How to fix it Fix the selector or delete the rule. To style an element rendered by a child component, pass it a `class` (class passthrough carries your scope) or use `:deep(.child)`. For classes added from outside the scope at runtime, use `:global(.name)`. JSX returned by a helper function is not part of a nested block's subtree, even when the helper is declared inside the component and called from inside the styled element: ```stsx export function Form() { const field = (name: string) => ; return (
    {field("Title")}
    ); } ``` Move the block to the top level of the file (a file-level block styles every element of the file), or write the `