# @aweftjs/build

The transforms. It compiles markup and JSX to `h` calls, replaces a static subtree with a
template made once and cloned per use, removes assert calls from a release build, and, when
asked, finds every string a page shows so it can be looked up in the reader's language. It
decides nothing else: not which bundler you use, not whether you write JSX, markup or `h` by
hand, not what a custom `h` does, and not when source that arrives at run time is compiled.

Nothing here is required. A page that writes `h` and markup runs with no build step at all;
this makes it faster and smaller.

## Quickstart

One implementation, two ways in.

```ts
// A bundler, rollup's shape, vite among them.
import { aweft } from '@aweftjs/build';

export default { plugins: [aweft({ release: process.env.NODE_ENV === 'production' })] };
```

The plugin also sets the bundler's own JSX handling to `preserve`, so vite's dependency scan and
its own transform leave a `.tsx` page to this plugin rather than reading its JSX as another
library's.

```ts
// Anywhere, a browser included, for source that did not exist at build time.
import { transform } from '@aweftjs/build';

const { code, map } = transform(source, { filename: 'page.tsx', release: true, defaultH: '@aweftjs/ui' });
```

```
# A Node process, which has no bundler config to put an option in.
AWEFT_DEFAULT_H=@aweftjs/ui AWEFT_TEXT=1 node --import @aweftjs/build/loader build-site.ts
```

Both produce the same bytes for the same input, and there is a fixture suite that says so
(`tests/modes.test.ts`). That equality is not a nicety: source validated by one transform and
executed by another can validate, be stored, and break when it renders.

## What it does to a file

### Markup in a template literal

A `` html`...` `` tag imported from `@aweftjs/dom` becomes `h` calls, so the page carries no
parser.

```ts
import { html } from '@aweftjs/dom';
const Note = (tone, text) => html`<p class="note ${tone}">${text}</p>`;
```

becomes calls to `h`, with the mixed attribute joined by `joined` from `@aweftjs/dom`, which
is the same function the runtime parser uses for it.

The dialect is `@aweftjs/dom`'s, unchanged. A tag may be an expression (`<${Component}>`), `</>`
closes the innermost open element, `<!-- ... -->` is a comment and goes, and `${value}` between
tags is a child. Inside a tag, these are all the forms there are:

| written | means |
| --- | --- |
| `hidden` | the attribute set to `true` |
| `id=plain` | the unquoted text up to the next space or `>` |
| `class="row ${tone}"` | the quoted parts as one value, joined by `joined` |
| `title=${t}`, `$onclick=${fn}` | the expression itself |
| `=${props}` or `${props}` | a spread: every key of the object becomes a prop |

A spread is the one form with two spellings. `=${props}` is the explicit one; a hole on its own,
with no name in front of it, means the same thing. Both compile to `{ ...props }`.

What compiling changes is when a mistake is reported: every fault the parser would have thrown at
render time is thrown here instead, as a `TransformError` carrying `at`, the offset in the file,
and the `reason` and `fix` every refusal in the stack carries. `errors.txt` lists
every reason.
One check does not survive compilation, and it is named rather than hidden. The parser asserts
that a spread is an object; a compiled template writes `{ ...expr }`, which spreads whatever it
is handed. A string spreads as one attribute per character, so `=${'not a tag'}` renders
`<div 0="n" 1="o" 2="t" ...>` where the parser would have refused, and a number, `null` or
`undefined` spreads as nothing at all.

### JSX

JSX compiles to a plain `h(...)` call resolved by ordinary lexical scope.

```tsx
const Card = ({ title }) => <section class="card"><h1>{title}</h1></section>;
```

A lowercase tag with no dot in it is an element name; anything else is an expression, so
`<Item/>` and `<ns.Part/>` call whatever the surrounding code calls `Item` and `ns`. A
fragment, `<>...</>`, becomes an array of items, which is what `mount` takes.

`h` is imported from `@aweftjs/dom` **only when the file has no `h` of its own**. A file that
declares its own `h`, or imports one from elsewhere, keeps it, and its JSX compiles to that
one. That is what lets a component library ship its own `h`, its own theming and a wholly
separate definition without this package knowing anything about it.

Which package supplies that import is `defaultH`. A bundler takes it as a plugin option,
`aweft({ defaultH: '@aweftjs/ui' })`. A Node process running the loader has no config to put
one in, so it says the same thing in the environment: `AWEFT_DEFAULT_H=@aweftjs/ui`, read once
when the loader starts, and refused by name if it is set to anything but the two package names.
`--import` is resolved by Node from the working directory, so run it from the application
root, where `node_modules` is; from anywhere else the failure reads as a missing package. Set them to
the same value when one `.tsx` is rendered on a server and bundled for a browser, or the two
sides compile it differently and the page will not hydrate (design 147).

### Static hoisting

The shape a source fixes becomes a template made once per document and instanced per use.
Element names, attributes whose value is a literal, and text go in the template; everything
else is applied to the instance.

```ts
const Row = (label, click) => h('tr', { class: 'row' },
	h('td', { class: 'a' }, label),
	h('td', { class: 'b' }, h('a', { $onclick: click }, 'go')));
```

becomes one `template(...)` at the top of the file and one call to it per row. Measured by
[`bench/hoist.ts`](https://github.com/torrinworx/aweft/blob/9a5bb24770dc7555257f8a307d79917efc58df70/bench/hoist.ts) in Chromium,
10,000 rows of that shape, best of five invocations of best of seven: inside a mount, which is
where a list builds its rows, 28.5 ms through the eight `h` calls against 13.4 ms as one template
instance. Outside any mount, which is where a page builds the item it then hands to `mount` or
`hydrate`, the same two are 43.1 ms and 29.8 ms, because every node made there is marked as the
binding's own so `hydrate` can adopt the server's markup. For a clone that marking is a walk, and
it is about 16 ms per 10,000 instances. A row built during a mount that is not hydrating pays none
of it. The loop for re-running the script is
[`bench/README.md`](https://github.com/torrinworx/aweft/blob/9a5bb24770dc7555257f8a307d79917efc58df70/bench/README.md).

**Hoisting only happens where `h` is provably `@aweftjs/dom`'s in that file.** JSX still
compiles to any `h`; only this substitution is restricted, because it assumes `dom`'s `h`'s
semantics. Four things are left as plain `h` calls:

- an element with a spread in its properties, which may carry `children` at run time
- an element given `children` as a property
- an element whose tag is not a literal name, a component included
- every element in a file that binds the name `h` anywhere except the one import from
  `@aweftjs/dom`

In the first three the subtree around the element still hoists, with the call as one of its
varying parts. The fourth is not one element but the whole file: a file that binds `h` itself
hoists nothing anywhere in it, including the elements the shadow never reaches. That is
deliberate, because following a shadow properly needs real scope analysis, and the blunt rule can
only be wrong in the direction of hoisting less.

### The access rules

Every element the transform reads, in whichever notation wrote it, goes through eight rules
before anything is hoisted, and a fault is a `TransformError` at the element like any other
(design 265). Each is a case a screen reader or a keyboard cannot recover from, and each is
decidable from the source alone.

| reason | refused | write instead |
| --- | --- | --- |
| `image-needs-alt` | `<img src="a.png" />` | `alt="what it shows"`, or `alt=""` for decoration |
| `control-needs-label` | `<input />` on its own | an `id` with a `<label for>`, a `<label>` around it, or an `aria-label` |
| `click-needs-role` | `<div $onclick={go}>` | a `<button>`, or a `role` and a `tabindex` |
| `tabindex-positive` | `tabindex="1"` | `0` to join the tab order where it sits, `-1` to reach it from code |
| `link-needs-href` | `<a>docs</a>` | an `href`, or a `<button>` when it acts on the page |
| `button-needs-name` | `<button />` | text inside it, or an `aria-label` |
| `heading-needs-text` | `<h2 />` | the heading's text, or no heading |
| `frame-needs-title` | `<iframe src="/map" />` | a `title` saying what it holds |

An `input` whose literal `type` is `hidden`, `submit`, `button`, `reset` or `image` needs no
label. Natively interactive elements (`a`, `button`, `input`, `select`, `textarea`, `summary`,
`details`, `option`, `label`, `audio`, `video`) may take a click as they are.

The rules read what the source says and stop there. An attribute given as an expression is
present: `alt={caption}` passes whatever `caption` holds. A spread makes the element unknowable
and it passes. A component is not read itself, and the elements written inside it are. The Node
loader compiles `.tsx` only, so an `h` call in a `.ts` file meets the rules in a bundle and not
under `node --import @aweftjs/build/loader`. A `label` counts only when it is around the control in the same JSX, template or
`h` call; a label in another expression pairs through an `id`. What the source cannot settle, a
rendered page can: `audit` from
[`@aweftjs/testing/browser`](/docs/packages/testing)
runs axe over the page a test drives.

### Assert stripping

With `release: true`, a statement that is nothing but a call to a name imported by name from a
neighbouring `assert` module is removed, and the import goes with it when nothing else in the
file still names it. So does any other import the file named only inside those calls: a helper
an assert fed on has no use left. An import the file never names is left alone.

```ts
import { assert } from './assert.ts';   // removed with its last call
import assert from 'node:assert/strict'; // never touched
```

Three shapes stay: a default import (`node:assert` and friends), `assert` used as a value
rather than called, and a call whose value something reads, so `const ok = assert(x, 'm')`
keeps its initializer. To get your own asserts stripped, import them by name from a module
called `assert` next to the file.

### Icon names

A string literal shaped `set:name`, on the `name` prop of the `Icon` a file bound from
`@aweftjs/ui`, becomes an import of that one icon and the plugin and loader answer that import.
[`@aweftjs/icons`](/docs/packages/icons)'s
README says exactly which names move and which are left for run time.

### The text a page shows

`aweft({ text: true })`, `transform(source, { text: true })`, or `AWEFT_TEXT=1` for the loader.
Off by default. On, in a file whose `h` is `@aweftjs/ui`'s, every literal a person reads becomes
a `text()` call from `@aweftjs/ui`, which looks the string up in the render's catalog where the
page mounts ([`packages/ui/README.md`](/docs/packages/ui), The text a page shows):

```tsx
<input placeholder="Search" name="q" />
<p>Save changes</p>
```

compiles to

```tsx
import { text as _text } from '@aweftjs/ui';
<input placeholder={_text("Search")} name="q" />
<p>{_text("Save changes")}</p>
```

What moves: every literal text child of an element, in JSX, markup and a hand-written `h` call
alike, and every string literal on a text prop. The text props are `TEXT_PROPS` on this package:
`label`, `title`, `description`, `placeholder`, `alt`, `error`, `caption`, `aria-label`,
`aria-description`, `aria-placeholder`, `aria-valuetext` and `aria-roledescription`. A `class`,
an `href`, a `name`, a `type` or an `id` is a word for the machine and never moves.

What is left alone: text with no letter in it (punctuation, a number, a separator); an element
with a literal `translate="no"` and everything under it; a prop given as an expression; the props
of an element carrying a spread, whose children still move; a file compiled against `dom`'s `h`,
which comes out byte for byte as it went in; and a file under `node_modules`, because a package
ships compiled files a server render reads as they are. A string built at run time is never a
literal: write it as a message, `text('{n, plural, one {# item} other {# items}}', { n })`,
which is also the only way it translates. A sentence with an element inside it is one message
with a tag, `text('Read <link>the docs</link>', { link })`, not two literals.

**What the transform answers.** `transform` gains `text` on its result: every key the file's
tokens look up, each once, the wrapped literals and the `text()` calls the page wrote itself, a
literal `context` folded in as `source|context`. A page that compiles a stored module at run time
keeps that list beside the source, since the build below never saw it.

**What the plugin writes.** When the bundle closes, `text/source.json` under the bundler's root:
every key, sorted, with the files it came from relative to the root, and the keys every installed
`@aweftjs` package ships in its `text.json` folded in under that name. It then reads each
`text/<tag>.json` beside it and warns about the keys that catalog lacks and the entries it holds
that no file uses. It never fails a build for either: what to do about a missing translation is
yours. The loader writes nothing, because a process that renders pages is not a build.

**Both sides must agree.** A page rendered on a server with the option off and bundled with it
on has different trees on the two sides, one text node against one component per string, and
does not hydrate. `AWEFT_TEXT` is the loader's word for the plugin's `text`, as `AWEFT_DEFAULT_H`
is for `defaultH`, and the scaffold in [`recipes/full-stack`](/docs/recipes/full-stack) sets both. Set to anything but a yes
or a no it is refused when the loader starts.

**What it costs.** A literal that hoisted into the template is a hole filled by a component call,
about 0.7 µs per text in Chromium and a bracket pair in the static markup, measured in design 277.

## The release mangle

`mangle` is the configuration for renaming this stack's internal properties, in the pattern and
in the two shapes a minifier takes.

```ts
import { mangle } from '@aweftjs/build';

await minify(code, mangle.terser);   // or: build({ ...mangle.esbuild })
```

A property whose name ends in exactly one underscore is internal surface and may be renamed. A
property whose name **begins** with an underscore is runtime-private and must keep its name,
because a wildcard observer decides what it delivers by looking at the name. The two are never
the same rule. Nothing in the stack uses the trailing convention yet, so today this renames
nothing; it is here so that the rename, when it happens, is a rename and nothing else.

## What it never decides

Which bundler you use. Whether you write JSX, markup or `h` by hand. What a custom `h` does.
When source that arrives at run time is compiled, or by whom. Which language a page shows, and
whether a missing translation fails a build.

## Proven by

[`recipes/build/main.ts`](/docs/recipes/build/files/main.ts)
builds a real page through the transforms, runs it in all three modes, and checks that a release
build of the binding's own source has no asserts left in it.
[`recipes/translated-site`](/docs/recipes/translated-site)
builds a site with the text option on, reads the source catalog back, and hydrates the pages
in three languages. The package's own suite is the
equivalence suite: every fixture runs twice, once as written and once transformed, mounted,
rendered and hydrated, over a document whose nodes clone and one whose nodes do not.

## The design notes

A `design NNN` above is the note of that number in
[`docs/design/`](https://github.com/torrinworx/aweft/tree/9a5bb24770dc7555257f8a307d79917efc58df70/docs/design), which says what was
decided, why, what it costs, and what would reverse it.

## API

Every export of `@aweftjs/build`, its signature as the compiler resolves it, and its block comment.

### `@aweftjs/build`

#### `Mangle`

```ts
interface Mangle { readonly pattern: RegExp; readonly terser: { readonly mangle: { readonly properties: { readonly regex: RegExp; }; }; }; readonly esbuild: { readonly mangleProps: RegExp; }; }
```

No block comment on this export.

#### `Plugin`

```ts
interface Plugin { readonly name: string; readonly enforce: 'pre'; config(): { oxc: { jsx: 'preserve'; }; }; configResolved(config: { root?: string; }): void; transform(code: string, id: string): { code: string; map: string; } | null; closeBundle(): Promise<void>; resolveId(source: string, importer?: string): Promise<string | null>; load(id: string): Promise<string | null>; }
```

What a bundler asks of a plugin, and all this one answers.

#### `SourceMap`

```ts
interface SourceMap { readonly version: number; readonly sources: readonly string[]; readonly names: readonly string[]; readonly mappings: string; toString(): string; toUrl(): string; }
```

A source map in the shape every bundler and every browser reads.

#### `TEXT_PROPS`

```ts
TEXT_PROPS: ReadonlySet<string>
```

The props whose string literal is text a person reads: HTML's own text-carrying attributes,
and the four names `ui`'s components take a label, a description, an error and a caption
under. A `class`, an `href`, a `name` or a `type` is a word for the machine and is never here.

#### `TransformError`

```ts
TransformError: typeof TransformError
```

What `transform` throws when the source says something a compiled template cannot mean.

Every fault the runtime markup parser raises is raised here instead where the transform can
see it in the source: an unterminated tag, a closing tag with nothing open, a spread written
without `=`, a namespaced JSX tag or attribute, an attribute given no value. The build stops
rather than the page.

**Params**

- `message`: what is wrong, in the words the runtime parser uses
- `at`: the offset in the source it is at

**Returns** the error. `at` is an offset into the source `transform` was given, not a line and column, so a caller that wants a position works it out from the source it passed in.

**Example**

```ts
try { transform(source, { filename: 'page.ts' }); } catch (error) {
  if (error instanceof TransformError) report(source.slice(0, error.at).split('\n').length);
}
```

#### `TransformOptions`

```ts
interface TransformOptions { readonly filename?: string; readonly release?: boolean; readonly defaultH?: '@aweftjs/dom' | '@aweftjs/ui'; readonly text?: boolean; }
```

What a caller may say about the source it hands `transform`.

Both are optional and both default to off: with no `filename` the source is read as JSX and
the map names no source, and with no `release` the asserts stay in.

**Example**

```ts
transform(source, { filename: 'page.tsx', release: true });
```

#### `TransformResult`

```ts
interface TransformResult { readonly code: string; readonly map: SourceMap; readonly text: readonly string[]; }
```

What `transform` answers: the compiled source and the map from it back to the original.

`code` is the whole file. What the transform did not touch comes out byte for byte as it went
in, so a diff of the two shows only the elements that compiled.

**Example**

```ts
const { code, map } = transform(source, { filename: 'page.ts' });
write(code + `\n//# sourceMappingURL=` + map.toUrl());
```

#### `aweft`

```ts
aweft: (options?: Omit<TransformOptions, "filename">) => Plugin
```

The plugin for a bundler that takes rollup's shape, vite among them.

**Params**

- `options`: `release`, which removes assert calls; `defaultH`, the package a file with no `h` gets one from; `text`, which finds the text the page shows and writes `text/source.json` under the bundler's root when the bundle closes. The filename comes from the bundler.

**Returns** the plugin. It handles `.js`, `.jsx`, `.ts` and `.tsx` and answers null for anything else, which leaves the file to the rest of the pipeline. It also answers the icon imports the transform writes, and tells the bundler to leave JSX to it, so one plugin is still the whole registration. With `text` on it finds the text in the application's own files and never in one under `node_modules`, folds the keys every installed `@aweftjs` package ships in its `text.json` into the source catalog, and warns, when the bundle closes, about the keys each `text/<tag>.json` beside it lacks and the entries it holds that no file uses; it never fails a build for either.

**Throws** out of `load`, whatever `@aweftjs/icons/node` refuses with: `set-not-installed` for a set the application has not installed, naming the install command, and `icon-not-in-set` for a name the set does not have.

**Example**

```ts
export default { plugins: [aweft({ release: true, text: true })] };
```

#### `mangle`

```ts
mangle: Mangle
```

How a release build renames this stack's internal properties.

A property whose name ends in exactly one underscore is internal surface and may be renamed.
A property whose name begins with an underscore is runtime-private and must keep its name,
because a wildcard observer decides what it delivers by looking at the name.

**Example**

```ts
import { mangle } from '@aweftjs/build';
await minify(code, mangle.terser);
```

#### `transform`

```ts
transform: (source: string, options?: TransformOptions) => TransformResult
```

Compile markup, JSX and static subtrees, and take the asserts out of a release build.

The same function is the whole of the bundler plugin and is callable in a browser, so source
compiled at build time and source compiled at run time cannot disagree.

**Params**

- `source`: the file's text
- `options`: `filename`, whose extension picks the dialect and which names the map; `release`, which removes assert calls; `defaultH`, the package a file with no `h` gets one from; `text`, which finds the text the page shows

**Returns** the transformed code, its source map, and the text keys it found.

**Throws** a `TransformError` for a fault in the markup or the JSX, carrying `at`, the offset in the source. Its `reason` names the rule broken: `unterminated-tag`, `unclosed-element`, `mismatched-closing-tag`, `nothing-to-close`, `unterminated-closing-tag`, `tag-needs-name`, `bad-attribute-name`, `attribute-needs-value`, `unterminated-attribute`, `spread-needs-hole`, `unterminated-comment`, `invalid-escape`, `namespaced-tag`, `namespaced-attribute`, `empty-expression` or `unsupported-child`; or one of the access rules (design 265): `image-needs-alt`, `control-needs-label`, `click-needs-role`, `tabindex-positive`, `link-needs-href`, `button-needs-name`, `heading-needs-text` or `frame-needs-title`. Source the parser cannot read throws the parser's own error instead.

**Example**

```ts
const { code, map } = transform(source, { filename: 'app.tsx', release: true });
```

### `@aweftjs/build/loader`

#### `load`

```ts
load: (url: string, context: unknown, nextLoad: NextLoad) => Promise<LoadResult>
```

Node's `load` hook: compile a `.tsx` file, leave everything else alone.

**Params**

- `url`: the module's URL
- `context`: what Node hands the hook, passed on unchanged
- `nextLoad`: the rest of the chain

**Returns** for a `.tsx` file, its source with the JSX compiled, as `module-typescript` so Node strips the types afterwards. For a URL `resolve` above claimed, the generated icon module. For anything else, whatever the rest of the chain says. A file that binds no `h` of its own gets one from the package `AWEFT_DEFAULT_H` names, and from `@aweftjs/dom` when nothing names one.

**Throws** an `Error` naming the file and the line, with the fault as its `cause`. The fault is a `TransformError` for a rule a compiled template cannot meet, and the parser's own `SyntaxError` for source it cannot read. Neither names the file on its own, and a stack into a parser is not where the reader has to look. `AWEFT_DEFAULT_H` set to anything but the two package names is refused when this module loads, before any file is read and whether or not one ever is, and so is `AWEFT_TEXT` set to anything but a yes or a no. With `AWEFT_TEXT=1` the text pass runs on every file, as it does under `aweft({ text: true })`.

#### `resolve`

```ts
resolve: (specifier: string, context: unknown, nextResolve: NextResolve) => Promise<ResolveResult>
```

Node's `resolve` hook: claim an icon import, leave everything else to the chain.

**Params**

- `specifier`: the import as it was written
- `context`: what Node hands the hook; `parentURL` is the file that wrote the import
- `nextResolve`: the rest of the chain

**Returns** for `@aweftjs/icons/<set>`, `@aweftjs/icons/<set>/<name>` and `@aweftjs/icons/<set>/+standard`, a URL under this file's own scheme carrying the request and the directory to resolve the set from. For anything else, whatever the rest of the chain says, which is what the package's two real entries get, and what a specifier the generator does not recognise as a request gets.

**Example**

```ts
import check from '@aweftjs/icons/lucide/check';
```

## Refusals

Every error this package raises carries a `reason` to switch on and a `fix` that says what to do. These are its reasons, from `errors.txt`.

| reason | fix |
|---|---|
| `attribute-needs-value` | Give the attribute a value, or drop the = to make it true. |
| `bad-attribute-name` | Start the attribute with a name, or close the tag. |
| `button-needs-name` | Put text inside the button, or give it an aria-label. |
| `click-needs-role` | Make it a <button>, or give it a role and a tabindex so the keyboard reaches it. |
| `control-needs-label` | Give it an id and a <label for> naming it, wrap it in a <label>, or give it an aria-label. |
| `empty-expression` | Put an expression in the braces, or drop them to make the attribute true. |
| `frame-needs-title` | Give the frame a title that says what it holds. |
| `heading-needs-text` | Put the heading's text inside it, or drop the heading. |
| `image-needs-alt` | Give the image an alt that says what it shows, or alt="" for a decorative one. |
| `invalid-escape` | Use an escape the template accepts, or write the character itself. |
| `link-needs-href` | Give the link an href, or make it a <button> when it does something on the page. |
| `mismatched-closing-tag` | Name the element being closed, or write </> for the innermost one. |
| `namespaced-attribute` | Write the attribute name without a colon in it. |
| `namespaced-tag` | Write the tag as a plain name or a member expression. |
| `nothing-to-close` | Remove the closing tag, or open the element it closes. |
| `spread-needs-hole` | Put the object in a hole, written as =${object}. |
| `tabindex-positive` | Use 0 to join the tab order where the element sits, or -1 to reach it from code only. |
| `tag-needs-name` | Put a tag name after the <, or write the tag as a hole. |
| `unclosed-element` | Close the element, or write it as <tag /> if it has no children. |
| `unknown-default-h` | Set it to @aweftjs/dom or @aweftjs/ui, or leave it unset to compile a file with no h of its own against dom. |
| `unknown-text-setting` | Set it to 1 to find the text a page shows, or leave it unset. |
| `unsupported-child` | Write the child as text, an element, or an expression in braces. |
| `unterminated-attribute` | Close the value with the same quote it opened with. |
| `unterminated-closing-tag` | End the closing tag with >. |
| `unterminated-comment` | Close the comment with --> before the next hole. |
| `unterminated-tag` | Close the opening tag with > or />. |

## Recipes

The programs in the stack's gate that use this package, each a job someone would have.

- [`recipes/full-stack`](/docs/recipes/full-stack): A page and a server in one directory: the page reaching the server in development through the dev server's proxy, and a sign-in that sets the cookie on that one origin
- [`recipes/ui`](/docs/recipes/ui): A page with themes, contexts, control flow, a popup and a suspend, built by vite and driven in a real browser
- [`recipes/icons`](/docs/recipes/icons): Icons named three ways, and what each way puts in the bundle
- [`recipes/routed-site`](/docs/recipes/routed-site): A site with real URLs: nested pages, a page with a parameter, a page that arrives later, a dialog the back button dismisses, and a title per page
- [`recipes/ssg`](/docs/recipes/ssg): A routed site written out as files, served by anything, and taken over in place when the browser gets to it
- [`recipes/translated-site`](/docs/recipes/translated-site): A site written in one language and launched in three: the build finds every string and writes the catalog an agent fills, each language is a tree of pages with the plural rules of its own, a stored act is compiled where it runs, and the Ukrainian page hydrates in place
- [`recipes/static`](/docs/recipes/static): A generated site served by the stack's own server: one process is the whole deployment, and the page still comes alive where it stands
- [`recipes/room`](/docs/recipes/room): An act module stored on the server, run in a frame on the page: the board it shares reaches the server, its ask carries the page's identity, its own stage navigates on the page's URL under the host act, an error inside reaches the page's logs, and the frame paints but cannot fetch
- [`recipes/accessible-page`](/docs/recipes/accessible-page): A page everyone can use, driven by keyboard and audited in both modes, and each guardrail catching one page written wrong: the build refusing an element no one can read, the mount throwing on a nameless button and a page with no language, `audit` and `walk` reading what only the rendered page shows
- [`recipes/build`](/docs/recipes/build): the package's own recipe
