--- 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//` 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 `` 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//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 {@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 `` in that recipe is the path from the app to the repository root, `../..` for `apps/`, 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 `` 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