You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/consuming.md

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.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); 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. 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:

  • 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.

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.

Powered by TurnKey Linux.