# chunks — assets a control loads on demand

> A `chunks { }` block declares assets a control ships but does not need at mount — a maths renderer, a diagram engine, a stylesheet only one feature uses. The shim asks for one by name when it needs it, so a heavy optional feature costs nothing on the pages that never use it.

<!-- id: ui-control-chunks · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-chunks/ -->

## Summary        {#summary}
A control is one bundle, and every page that mounts it downloads all of it. That is the right shape for a control's own
code, and the wrong shape for a feature most documents never touch: a maths renderer is a quarter of a megabyte, a
diagram engine several times that, and bundling either means charging every reader for a thing one page in fifty uses.

A **`chunks { }`** block is the second address. It names the assets a control ships separately, and the shim asks for
one **by name** at the moment it needs it:

```ts syntax
if (documentHasMath) {
  const { render } = await import(host.chunkUrl("Math"));
  render(el, source);
}
```

A chunk is stored, content-addressed, cached and garbage-collected exactly like the bundle beside it. What is different
is only that the platform does not load it — **the control decides when, and whether at all**.

A chunk can be a single file or a whole **package** directory. The package shape exists for libraries that load their
own parts at run time — a worker, a `.wasm` sibling, a renderer per diagram type — which no bundler can flatten into
one module. See **One file, or a package** below.

## Signature      {#signature}
```osy syntax
control <Name> {
  chunks {
    <ChunkName>;
    <ChunkName>;
  }
}
```

Registered per name — as one file, or as a directory:

```bash
osy control add controls/editor.js \
  --chunk Math=vendor/temml.js \
  --chunk MathCss=vendor/temml.css \
  --chunk-package Diagrams=dist/diagrams/index.js
```

## Description    {#description}

### Declaring a chunk and pointing it at a file   {#declaring}

The declaration names the chunks; the registration says which file each one is.

```osy title="an editor whose maths support is optional" test app=ui-control-chunks
control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string ownerType; }
  chunks {
    /// A MathML renderer, loaded the first time a document contains `$…$`.
    Math;
    /// Its stylesheet.
    MathCss;
  }
}

[Page("/doc")] [AllowAnonymous]
component DocPage() {
  render {
    MarkdownEditor(ownerType: "Document");
  }
}
```

The two halves are checked against each other. A `--chunk` name the control does not declare is refused, naming the
ones it does; a declared chunk with no file registered is a **compile error** once anything renders the control, and a
warning while nothing does — so the bundle can be registered first and its chunks next, without a broken state in
between.

Nothing about a chunk reaches the app. A `chunks { }` block changes no call site, no prop, and no page: it is a
statement about how the control ships itself.

### Getting a chunk's URL in the shim — `host.chunkUrl`   {#asking}

`host.chunkUrl(name)` returns a same-origin URL. The generated typings key it to the names you declared, so a typo does
not compile:

```ts syntax
export interface MarkdownEditorHost {
  chunkUrl(name: "Math" | "MathCss"): string;
}
```

**It returns a URL, not a module**, and that is deliberate. One member then covers both kinds of asset — a module you
`import()`, and a stylesheet you put in a `<link>`:

```ts syntax
// a module
const temml = await import(host.chunkUrl("Math"));

// a stylesheet
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = host.chunkUrl("MathCss");
document.head.appendChild(link);
```

A member that returned a *module* could not express the stylesheet at all, and one that decided *when* to load would be
answering a question only the control knows the answer to.

The URL is **content-addressed and immutable**, so caching it forever is safe and re-asking is free — `chunkUrl` does no
work beyond a lookup. Loading twice is the shim's own concern: keep the promise.

### One file, or a package    {#one-file-or-package}

A chunk name resolves to one of two things, and the registration decides which. The declaration is the same either
way — a chunk name is a name, and `host.chunkUrl()` returns something you can load.

**A single file** is served at its own content address, with **no directory around it**. So a relative specifier
inside one has nothing to resolve against:

```ts syntax
import { helper } from "./helper.js";   // ✗ refused in a single-file chunk — there is no "." to be relative to
```

`osy control add` refuses that at registration and names the specifier it found. Bundle the chunk to one file
(`esbuild --bundle --format=esm`), or register it as a package instead.

The same applies to a stylesheet's `url()` in a single-file chunk. Its CSS cannot reference a font by relative path;
register the face with [web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/) and point the `url()` at the address that returns.

**A package** is a whole directory, served under a real base — so relative imports resolve exactly as they do on a
static host:

```bash
osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js
```

You give the **entry file**; the package is the directory containing it, taken recursively. `host.chunkUrl("Diagrams")`
returns that entry's URL, and everything the entry reaches from there — `./chunks/x.js`, a `.wasm` sibling, a worker
started with `new Worker(new URL("./worker.js", import.meta.url))` — resolves against the directory it genuinely sits
in.

Reach for a package when a library **loads its own parts at run time**. Many do, and flattening one into a single file
is both wasteful and sometimes impossible: a diagram engine that lazy-loads a renderer per diagram type becomes one
module carrying every renderer, and a library that starts a worker or fetches a `.wasm` cannot be bundled at all.

⚠️ **Register your own build output, never a package's distribution directory.** A published package typically ships
several build flavours and a full type-definition tree — one common diagram library's is 83 MB across 1167 files.
Point a bundler at the library and register what it writes:

```bash
esbuild node_modules/some-lib/dist/index.mjs --bundle --format=esm --minify --splitting --outdir=dist/diagrams
osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js
```

A path inside `node_modules` is refused by name, before anything is read.

A package's identity is its **whole file set**, so re-registering an unchanged directory writes nothing, and a file
added or deleted since it was registered fails the next compile rather than shipping a set nobody registered.

### What may be served    {#content-types}

A chunk's content type comes from its extension, and the set is closed:

| Extension | Served as |
|---|---|
| `.js`, `.mjs` | `text/javascript` |
| `.css` | `text/css` |
| `.json` | `application/json` |
| `.wasm` | `application/wasm` |
| `.woff2`, `.woff`, `.ttf` | the matching font type |

Anything else is refused. `.html` and `.svg` are refused deliberately: both would give the app a same-origin address
that executes script, and no control needs one.

Anything else is refused, in a package member exactly as in a single file — and it is refused **by name** rather than
skipped, so a build output containing (say) a source map tells you which file to exclude instead of registering
cleanly and 404ing later on the one file nobody thought about.

The per-asset size limit is the bundle's — **5 MB**, applied per file — and every asset a control ships counts toward
the app's total.

### A chunk is not a prop, a style, or a slot    {#not-the-others}

Four ways a control takes something from outside, and they answer different questions:

| | What it is | Decided by |
|---|---|---|
| **prop** | a value the control renders with | the app, per call site |
| **style** | a look knob the app may override | the app, or its theme |
| **slot** | content the app writes inside the control | the app |
| **chunk** | code or an asset the control ships itself | the **control** |

A chunk is the only one of the four the app knows nothing about. If an app should be able to decide something, that is
a prop; if it should be able to supply something, that is a slot. A chunk is the control's own weight, moved off the
critical path.

Unlike `styles { }`, a chunk is legal at **every** participation rung — including `opaque`. A chunk is neither look nor
app data; it is the control's own code, and an opaque island is exactly the kind of control that ships a heavy renderer.

## Examples       {#examples}

A diagram control that loads its engine once, on the first diagram it meets:

```osy title="a chunk nothing pays for until a diagram appears" test app=ui-control-chunks-diagram
control DiagramView {
  contractVersion "1.1"
  participation opaque
  props { string source; }
  chunks { Engine; }
}
```

```ts syntax
let engine: Promise<{ render(el: HTMLElement, src: string): void }> | undefined;

export function mount(el, props, host) {
  const draw = async (src: string) => {
    // Loaded ONCE and remembered — a second diagram on the page costs nothing.
    engine ??= import(host.chunkUrl("Engine"));
    (await engine).render(el, src);
  };
  if (props.source) void draw(props.source);
  return {
    update(next) { if (next.source) void draw(next.source); },
    destroy() { el.replaceChildren(); },
  };
}
```

The page that renders `DiagramView` with no source downloads the control and nothing else.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block chunks are declared in
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control publishes
- [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) — a control's own look knobs
- [web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/) — how an app ships a typeface, including one a chunk's stylesheet names
