|
|
---
|
|
|
title: Consuming the framework from an app
|
|
|
type: guide
|
|
|
audience: human + agent
|
|
|
authority: E4 guide — the contract an app in this repository follows to consume UIX as source
|
|
|
status: current
|
|
|
---
|
|
|
|
|
|
# Consuming the framework from an app
|
|
|
|
|
|
UIX is consumed **as source, from a workspace in this repository**. There is no
|
|
|
npm package, no `exports` map and no `bin`: an app lives next to the framework,
|
|
|
imports it through the same alias table every tool in the repository reads, and
|
|
|
compiles it with its own build. This page is that contract, section by section.
|
|
|
Where a subject already has a reference page, this page links it instead of
|
|
|
repeating it.
|
|
|
|
|
|
## 1. Where the app lives
|
|
|
|
|
|
An app is a workspace under `apps/<name>/` with its own `package.json`, sharing
|
|
|
the repository's single `node_modules` and lockfile. One install, one copy of
|
|
|
`svelte`, `vite` and `@sveltejs/kit` for the framework and every app — two
|
|
|
copies of Svelte would mean two runtimes of runes, and the framework's
|
|
|
`.svelte.ts` modules would not share state with the app's components.
|
|
|
|
|
|
Vite finds the workspace root from the root `package.json`'s `workspaces` field,
|
|
|
which is what lets the app's dev server read files under the repository's
|
|
|
`src/`. An app taken outside the workspace must add the repository root to its
|
|
|
own `server.fs.allow`.
|
|
|
|
|
|
The contract is written for, and verified on, a fully prerendered static site:
|
|
|
`@sveltejs/adapter-static` with no `fallback`, and `export const prerender = true` in
|
|
|
`src/routes/+layout.ts`. The pre-hydration boot (§6) and the CSP `<meta>` are then written into
|
|
|
every HTML file at build time, and a page that fails to render fails the build.
|
|
|
|
|
|
## 2. Aliases
|
|
|
|
|
|
The framework imports itself through `$…` and `@/` specifiers. The table lives
|
|
|
in ONE module at the repository root, [`uix.aliases.js`](../uix.aliases.js), and
|
|
|
the app reads it exactly as the framework's own configs do:
|
|
|
|
|
|
```js
|
|
|
// apps/<name>/svelte.config.js
|
|
|
import { dirname, resolve } from 'node:path';
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
import { resolveUixAliases } from '../../uix.aliases.js';
|
|
|
|
|
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
|
|
|
|
export default {
|
|
|
kit: {
|
|
|
alias: resolveUixAliases(REPO_ROOT)
|
|
|
}
|
|
|
};
|
|
|
```
|
|
|
|
|
|
SvelteKit writes the aliases into the app's generated tsconfig and hands them to
|
|
|
Vite, so the app does not repeat them in `resolve.alias`. Order is load-bearing
|
|
|
(a longer specifier before any key that is its prefix); never re-sort or copy the
|
|
|
table — `src/uix/aliases.test.ts` guards it where it is written.
|
|
|
|
|
|
`@` is the framework's `src/`, not the app's. The app keeps its own `$lib`.
|
|
|
|
|
|
The app's `tsconfig.json` extends the one Kit generates, and carries one option the framework's
|
|
|
sources need:
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"extends": "./.svelte-kit/tsconfig.json",
|
|
|
"compilerOptions": {
|
|
|
"rewriteRelativeImportExtensions": true,
|
|
|
"allowJs": true,
|
|
|
"checkJs": true,
|
|
|
"strict": true,
|
|
|
"moduleResolution": "bundler"
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
The framework imports some of its modules with their `.ts` extension (`scripts/generate-boot.ts`,
|
|
|
`src/libs/errs/`), and `svelte-check` follows the app's `vite.config.ts` into them. Without
|
|
|
`rewriteRelativeImportExtensions`, the check of `apps/base` reports 578 errors, every one
|
|
|
`An import path can only end with a '.ts' extension`; with it, none.
|
|
|
|
|
|
## 3. Runes
|
|
|
|
|
|
The framework is written in Svelte 5 runes mode, and the repository enforces it
|
|
|
by config rather than per file. The app's `svelte.config.js` carries the same
|
|
|
three lines, or framework components that happen to use no rune compile in
|
|
|
legacy mode:
|
|
|
|
|
|
```js
|
|
|
vitePlugin: {
|
|
|
dynamicCompileOptions: ({ filename }) =>
|
|
|
filename.includes('node_modules') ? undefined : { runes: true };
|
|
|
}
|
|
|
```
|
|
|
|
|
|
## 4. The composition root
|
|
|
|
|
|
An app boots UIX in **standalone** mode: `createActiveUix` owns the services,
|
|
|
`setActiveUix` publishes it to the tree, `Soma.create()` opens the headless
|
|
|
layer and `ActiveEidos.create({ applyDom: true })` the visual one. Both are
|
|
|
disposed in `onDestroy`. The boot sequence, the minimum contracts and the prefs
|
|
|
schema are described in [`architecture/active-uix.md`](./architecture/active-uix.md);
|
|
|
the per-app prefs schema is the same module the pre-hydration boot compiles
|
|
|
(§6). The root layout of `apps/base`, with the app's own imports reduced to a comment:
|
|
|
|
|
|
```svelte
|
|
|
<!-- apps/<name>/src/routes/+layout.svelte -->
|
|
|
<script lang="ts">
|
|
|
import '$uix/eidos/index.css';
|
|
|
import { onDestroy } from 'svelte';
|
|
|
import { createActiveUix, setActiveUix } from '$active-uix';
|
|
|
import { Soma } from '$soma/core/soma.svelte';
|
|
|
import { ActiveEidos } from '$uix/eidos';
|
|
|
// The app's own modules: its reset (§5), `eidosConfig` and `bootPrefsSchema` (§6).
|
|
|
|
|
|
let { children } = $props();
|
|
|
|
|
|
const uix = createActiveUix({
|
|
|
langs: { schema: {}, defaultLocale: 'es' },
|
|
|
prefs: { schema: bootPrefsSchema('es') }
|
|
|
});
|
|
|
setActiveUix(uix);
|
|
|
Soma.create();
|
|
|
|
|
|
const eidos = ActiveEidos.create({ applyDom: true, config: eidosConfig });
|
|
|
|
|
|
onDestroy(() => {
|
|
|
eidos.dispose();
|
|
|
uix.dispose();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
{@render children()}
|
|
|
```
|
|
|
|
|
|
The reset is §5; `eidosConfig` and `bootPrefsSchema` are §6. `langs.schema` is the
|
|
|
app's catalogue for `langs.t(path)` — the `strings` of the theming guide's snippet — and `{}`
|
|
|
when the app has none; the framework registers its components' own words under `components.*`
|
|
|
by itself. `uix.langs` is not typed with the app's catalogue, so a misspelt `t()` path
|
|
|
compiles, and a production build renders the raw path without a word. `apps/base` keeps its
|
|
|
words in records instead (`src/lib/strings.ts`, `satisfies` one string per language) and reads
|
|
|
them with `langs.ts(record)`, which follows the language preference the same way. The languages
|
|
|
are one tuple in `src/prefs-schema.ts` that the schema reads too, so a missing language, a
|
|
|
language added to the schema or a misspelt name fails `npm run check`.
|
|
|
|
|
|
`attachActiveUix` — an external `ActiveApp` that owns its services — exists and
|
|
|
is tested, but no app consumes it and the pre-hydration boot does not cover it.
|
|
|
Build on standalone.
|
|
|
|
|
|
## 5. CSS and static assets
|
|
|
|
|
|
- **CSS.** Import `$uix/eidos/index.css` once, in the root layout. It carries the
|
|
|
static foundation, which a prerendered or server-rendered page needs in its
|
|
|
HTML so the first paint is styled before hydration.
|
|
|
- **The document reset is the app's.** The foundation ships none, and its
|
|
|
recipes assume three rules: `box-sizing: border-box` on every box, no browser
|
|
|
margin on `body`, and the theme's body style on `body`, by token
|
|
|
(`--style-body-font-family` and its siblings — the foundation declares them
|
|
|
and applies them to no element). `apps/base/src/lib/reset.css` is those
|
|
|
rules, imported after `index.css`. Without them, measured in Chromium,
|
|
|
Firefox and WebKit at 375 × 667: the document is 683 px tall under an
|
|
|
`AppShell` that fills `100dvh`, an open `Dialog` is 409 px wide
|
|
|
(`content-box` plus its padding), and `body` computes each browser's default
|
|
|
family, which the shell's skip link inherits. Whether the foundation should
|
|
|
own the reset instead is an open framework question
|
|
|
([F18](./process/PLAN-blocks-quality.md)); until it does, this is a step of
|
|
|
the contract.
|
|
|
- **Fonts and sounds are served by URL, not imported.** The type scale points at
|
|
|
`/fonts/{family}/…` and the default sound pack at `/sounds/ui/…`, both absolute
|
|
|
paths on the page's own origin. Their files live in the repository's
|
|
|
`static/fonts` and `static/sounds`; the app copies them into its own `static/`
|
|
|
before `dev` and `build` (`predev` / `prebuild`), and git-ignores the copy. A
|
|
|
symlink is not an option on Windows without privileges, and a committed copy
|
|
|
duplicates binaries that drift. `apps/base/scripts/sync-assets.mjs` is the
|
|
|
copy; with `--check` it changes nothing and exits 1 when the copy drifted.
|
|
|
|
|
|
## 6. The pre-hydration boot
|
|
|
|
|
|
The recipe — placeholder under `%sveltekit.head%`, a `transformPageChunk` with a
|
|
|
function replacement, the CSP hash in `kit.csp` — is
|
|
|
[`theming/guide.md` § How to kill the dark-mode flash](./theming/guide.md). An
|
|
|
app with its own prefs schema compiles its own boot, and must: the section
|
|
|
[Your schema, your boot](./theming/guide.md) explains why the framework's
|
|
|
default artifact resolves a different direction and language than a
|
|
|
multi-language app's runtime. Inside this workspace every `<uix>` in that recipe
|
|
|
is the path from the app to the repository root, `../..` for `apps/<name>`, and
|
|
|
`tsx` and `esbuild` go in the app's `devDependencies` at the root's versions.
|
|
|
|
|
|
Three things the recipe does not say, measured in `apps/base`:
|
|
|
|
|
|
- **`themeIds` has no instance to ask.** `hooks.server.ts` runs on the server,
|
|
|
where no `ActiveEidos` exists, so the recipe's `eidos.listThemes()` has nothing
|
|
|
to call. Keep the config in ONE module the root and the hook both import
|
|
|
(`apps/base/src/lib/eidos-config.ts`): `createThemeBaseEidosConfig()` from
|
|
|
`$uix/eidos/lib/themes/base` builds it, the root passes it to
|
|
|
`ActiveEidos.create({ applyDom: true, config })`, and the hook passes
|
|
|
`listEidosThemes(config)` from `$uix/eidos/lib/config` — deep imports, no
|
|
|
runes. The list the boot receives is then the list eidos registers, by
|
|
|
construction.
|
|
|
- **Nothing in the build checks the placeholder's position.** With `%uix.boot%`
|
|
|
moved above `%sveltekit.head%`, the build is green, the boot tag lands before
|
|
|
the CSP `<meta>` and it runs under no policy — measured with the framework's
|
|
|
hash instead of the app's in `kit.csp`: nine attributes stamped, zero
|
|
|
violations. The app's smoke checks the order in `build/index.html`. Prettier
|
|
|
joins the two placeholders on one line (`%sveltekit.head% %uix.boot%`); the
|
|
|
order is what matters.
|
|
|
- **The staleness guard and Kit read the current directory.** `uixBootCheck`
|
|
|
resolves `schema` and `out` against it, and Kit loads `svelte.config.js` from
|
|
|
it; `npm run <script> -w apps/<name>` sets it to the app's directory.
|
|
|
|
|
|
The direction contract the boot follows — `<html dir>` is a projection, the
|
|
|
template's `dir` is the only one read back — is
|
|
|
[`canon/direction-contract.md` §6](./canon/direction-contract.md).
|
|
|
|
|
|
## 7. What stays with the framework
|
|
|
|
|
|
Everything under `src/` is the framework's. An app that needs a component, a
|
|
|
token or an API the framework lacks flags the gap so it is built as a reusable
|
|
|
part of the framework, never reinvented inside the app
|
|
|
([`guides/component-guide.md`](./guides/component-guide.md) §4).
|
|
|
|
|
|
## 8. Verification
|
|
|
|
|
|
[`apps/base`](../apps/base/README.md) is this contract followed to the letter, and
|
|
|
its check is part of the gate:
|
|
|
|
|
|
```bash
|
|
|
npm run apps:check # check + build + smoke of apps/base (member of `npm run gate`)
|
|
|
npm run boot -w apps/base # recompiles src/generated/boot.js; twice in a row is byte-identical
|
|
|
npm run assets:sync -w apps/base -- --check # the copied fonts and sounds have not drifted
|
|
|
```
|
|
|
|
|
|
The smoke serves `build/` from a real HTTP server and prints one line of
|
|
|
figures: the boot tag after the CSP `<meta>`; then, for three visitors —
|
|
|
`es-ES`, `ar-EG`, and `es-ES` asking for a dark scheme and reduced motion —
|
|
|
the nine boot attributes on `<html>` with the app's modules aborted, carrying
|
|
|
the values that visitor asks for (`dir="rtl" lang="ar"`;
|
|
|
`data-mode="dark" data-motion="reduce"`), and a full load with zero page errors,
|
|
|
`console.error` and CSP violations and the same nine attributes after
|
|
|
hydration. It does not count missing translations: langs reports them in dev
|
|
|
only, so a production build has nothing to show. A missing context never
|
|
|
reaches it: it fails the prerender. The commands and the passes are described in
|
|
|
[`apps/base/README.md`](../apps/base/README.md).
|