11 KiB
| title | type | audience | authority | status |
|---|---|---|---|---|
| Consuming the framework from an app | guide | human + agent | E4 guide — the contract an app in this repository follows to consume UIX as source | 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, and
the app reads it exactly as the framework's own configs do:
// 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:
{
"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:
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;
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:
<!-- 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.cssonce, 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-boxon every box, no browser margin onbody, and the theme's body style onbody, by token (--style-body-font-familyand its siblings — the foundation declares them and applies them to no element).apps/base/src/lib/reset.cssis those rules, imported afterindex.css. Without them, measured in Chromium, Firefox and WebKit at 375 × 667: the document is 683 px tall under anAppShellthat fills100dvh, an openDialogis 409 px wide (content-boxplus its padding), andbodycomputes 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); 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'sstatic/fontsandstatic/sounds; the app copies them into its ownstatic/beforedevandbuild(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.mjsis the copy; with--checkit 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. An
app with its own prefs schema compiles its own boot, and must: the section
Your schema, your boot 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:
themeIdshas no instance to ask.hooks.server.tsruns on the server, where noActiveEidosexists, so the recipe'seidos.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/basebuilds it, the root passes it toActiveEidos.create({ applyDom: true, config }), and the hook passeslistEidosThemes(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 inkit.csp: nine attributes stamped, zero violations. The app's smoke checks the order inbuild/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.
uixBootCheckresolvesschemaandoutagainst it, and Kit loadssvelte.config.jsfrom 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.
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 §4).
8. Verification
apps/base is this contract followed to the letter, and
its check is part of the gate:
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.