refactor(build): un solo mapa de alias — uix.aliases.js lo importan vite.config.ts, svelte.config.js, el compilador del boot y docs-check; muere la copia a mano (F2 del cierre)
La tabla de alias (38 entradas) estaba escrita dos veces, en vite.config.ts y a mano en svelte.config.js, y dos scripts la sacaban de vuelta de la config de Vite con una regex (generate-boot.ts y el invariante 8 de docs-check.ts). Una app del workspace habría sido el cuarto sitio. Ahora vive UNA vez en uix.aliases.js (raíz, .js con JSDoc porque Node carga svelte.config.js sin bundler): UIX_ALIASES con rutas relativas y resolveUixAliases(root) con las absolutas, en el mismo orden. Las dos tablas eran idénticas en claves, valores y orden (comprobado antes de sustituirlas). El orden es contrato: Vite y esbuild casan un alias de cadena por prefijo y SvelteKit se los pasa en orden de inserción (leído en @sveltejs/kit/src/exports/vite/utils.js), así que $svrs/auth/testing y $svrs/auth van antes que $svrs. src/uix/aliases.test.ts lo guarda junto a lo demás: cada destino existe (anti-vacío ≥ 38), resolveUixAliases conserva orden y rutas, ninguna config vuelve a llevar una copia, y el tsconfig generado por SvelteKit tiene paths para cada entrada. Visto fallar: con $svrs delante de $svrs/auth, 1 rojo (restaurado byte a byte). svelte.config.js gana kit.typescript.config para que svelte-check incluya el módulo. readViteAliases muere sin shim; pack.test.ts importa el módulo. CLAUDE.md y AGENTS.md dicen dónde vive la tabla (la sección de AGENTS.md listaba alias que ya no existen). Verificación: aliases.test + pack.test 6/6 · generate:boot byte-idéntico (esbuild recibe el mismo mapa) · docs:check 0/0 · check:gate OK (src/ a cero; 89 en web/, dentro del ledger) · suite entera 464 ficheros / 5 442 tests, exit 0 · npm run build de la raíz exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>alpha-0.1-background
parent
29e581c721
commit
9254bcc15e
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* The import map exists ONCE (`uix.aliases.js`) and it resolves.
|
||||
*
|
||||
* Until 2026-09-16 the table was written twice — in `vite.config.ts` and, by
|
||||
* hand, in `svelte.config.js` — and two scripts parsed it back out of the Vite
|
||||
* config with a regex. Every consumer now imports the module, so the guard has
|
||||
* four jobs: the table points at things that exist, nobody writes a second
|
||||
* copy, the order the resolvers depend on holds, and SvelteKit's generated
|
||||
* tsconfig carries every entry.
|
||||
*/
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveUixAliases, UIX_ALIASES } from '../../uix.aliases.js';
|
||||
|
||||
const REPO = resolve('.');
|
||||
const entries = Object.entries(UIX_ALIASES);
|
||||
|
||||
describe('uix.aliases.js — the one import map', () => {
|
||||
it('has the whole table, and every entry points at something on disk', () => {
|
||||
// Anti-empty: a table that lost its entries would make every other assertion vacuous.
|
||||
expect(entries.length).toBeGreaterThanOrEqual(38);
|
||||
const missing = entries.filter(([, path]) => !existsSync(resolve(REPO, path)));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves every entry against the root it is given, in table order', () => {
|
||||
const resolved = resolveUixAliases(REPO);
|
||||
expect(Object.keys(resolved)).toEqual(entries.map(([specifier]) => specifier));
|
||||
for (const [specifier, path] of entries) {
|
||||
expect(resolved[specifier]).toBe(resolve(REPO, path));
|
||||
}
|
||||
});
|
||||
|
||||
it('lists a longer specifier before any key that is its prefix', () => {
|
||||
// Vite and esbuild take the FIRST string alias that matches `key` or
|
||||
// `key/…`, so `$svrs` listed before `$svrs/auth` would swallow it.
|
||||
const keys = entries.map(([specifier]) => specifier);
|
||||
const shadowed: string[] = [];
|
||||
keys.forEach((key, index) => {
|
||||
for (const longer of keys.slice(index + 1)) {
|
||||
if (longer.startsWith(`${key}/`)) shadowed.push(`${key} before ${longer}`);
|
||||
}
|
||||
});
|
||||
expect(shadowed).toEqual([]);
|
||||
});
|
||||
|
||||
it('is not copied back into either config', () => {
|
||||
for (const config of ['vite.config.ts', 'svelte.config.js']) {
|
||||
const source = readFileSync(resolve(REPO, config), 'utf8');
|
||||
expect(source, config).toContain('resolveUixAliases(__dirname)');
|
||||
expect(source, config).not.toMatch(/resolve\(__dirname,\s*['"]src\//);
|
||||
}
|
||||
});
|
||||
|
||||
it("reaches SvelteKit's generated tsconfig, entry by entry", () => {
|
||||
const generated = resolve(REPO, '.svelte-kit/tsconfig.json');
|
||||
expect(existsSync(generated), 'run `svelte-kit sync` (npm install runs it)').toBe(true);
|
||||
const { compilerOptions } = JSON.parse(readFileSync(generated, 'utf8')) as {
|
||||
compilerOptions: { paths: Record<string, string[]> };
|
||||
};
|
||||
const absent = entries
|
||||
.map(([specifier]) => specifier)
|
||||
.filter((specifier) => compilerOptions.paths[specifier] === undefined);
|
||||
expect(absent).toEqual([]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* The framework's import map — ONE table for every tool that resolves a `$…`
|
||||
* or `@/` specifier: `vite.config.ts` (dev, build, both vitest projects),
|
||||
* `svelte.config.js` (Kit writes it into `.svelte-kit/tsconfig.json` and hands
|
||||
* it to Vite), `scripts/generate-boot.ts` (esbuild), `scripts/docs-check.ts`
|
||||
* (invariant 8), and any app in this workspace that consumes the framework as
|
||||
* source (`docs/consuming.md`).
|
||||
*
|
||||
* It is `.js` with JSDoc, not `.ts`: Node loads `svelte.config.js` with no
|
||||
* bundler, and that file imports this one.
|
||||
*
|
||||
* ORDER IS LOAD-BEARING. Vite and esbuild match a string alias as a prefix
|
||||
* (`$svrs` also matches `$svrs/auth/testing`) and SvelteKit hands them over in
|
||||
* insertion order, so a longer specifier must come before any key that is its
|
||||
* prefix. `src/uix/aliases.test.ts` fails if it does not.
|
||||
*/
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
/**
|
||||
* Specifier → path relative to the repository root.
|
||||
*
|
||||
* @type {Readonly<Record<string, string>>}
|
||||
*/
|
||||
export const UIX_ALIASES = Object.freeze({
|
||||
// ── Runtime artifacts (src/arts) ──────────────────────────────────────
|
||||
'$active-app': 'src/arts/active-app',
|
||||
$adom: 'src/arts/adom',
|
||||
$agent: 'src/arts/agent',
|
||||
$auth: 'src/arts/auth',
|
||||
$bus: 'src/arts/bus',
|
||||
$cache: 'src/arts/cache',
|
||||
$clipboard: 'src/arts/clipboard',
|
||||
$color: 'src/arts/color',
|
||||
$connection: 'src/arts/connection',
|
||||
$ethereal: 'src/arts/ethereal',
|
||||
$format: 'src/arts/format',
|
||||
$http: 'src/arts/http',
|
||||
$langs: 'src/arts/langs',
|
||||
$logger: 'src/arts/logger',
|
||||
$motion: 'src/arts/motion',
|
||||
$orca: 'src/arts/orca',
|
||||
$perf: 'src/arts/perf',
|
||||
$perm: 'src/arts/perm',
|
||||
$prefs: 'src/arts/prefs',
|
||||
$scene: 'src/arts/scene',
|
||||
$session: 'src/arts/session',
|
||||
$sium: 'src/arts/sium',
|
||||
$sound: 'src/arts/sound',
|
||||
$storage: 'src/arts/storage',
|
||||
$timer: 'src/arts/timer',
|
||||
|
||||
// ── Pure helpers (src/libs) ───────────────────────────────────────────
|
||||
$libs: 'src/libs',
|
||||
$locale: 'src/libs/locale',
|
||||
$reactive: 'src/libs/reactive',
|
||||
|
||||
// ── Encapsulated opt-in packs (src/packs) ─────────────────────────────
|
||||
$packs: 'src/packs',
|
||||
|
||||
// ── Server-authoritative engines (src/svrs) ───────────────────────────
|
||||
// The GATE of each engine is a native SvelteKit server-only module
|
||||
// (`*.server.ts`), so the alias points at the file and no consumer
|
||||
// import changes. Longest specifier first: these are prefix matches.
|
||||
'$svrs/auth/testing': 'src/svrs/auth/testing.server.ts',
|
||||
'$svrs/auth': 'src/svrs/auth/index.server.ts',
|
||||
$svrs: 'src/svrs',
|
||||
|
||||
// ── UIX layers (src/uix) ──────────────────────────────────────────────
|
||||
$uix: 'src/uix',
|
||||
'$active-uix': 'src/uix/active-uix',
|
||||
$soma: 'src/uix/soma',
|
||||
|
||||
// ── Blocks tier (src/uix/blocks) — page-function compositions ─────────
|
||||
$blocks: 'src/uix/blocks',
|
||||
|
||||
// ── Demo controls (src/lib/_demo) — the frozen docs site's widgets ────
|
||||
$demo: 'src/lib/_demo',
|
||||
|
||||
// ── @/ catch-all ──────────────────────────────────────────────────────
|
||||
'@': 'src'
|
||||
});
|
||||
|
||||
/**
|
||||
* The same table with absolute paths, in table order.
|
||||
*
|
||||
* @param {string} root Absolute path of the repository root.
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function resolveUixAliases(root) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(UIX_ALIASES).map(([specifier, path]) => [specifier, resolve(root, path)])
|
||||
);
|
||||
}
|
||||
Loading…
Reference in new issue