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/scripts/uix-boot-check.ts

184 lines
7.3 KiB

feat(active-uix)!: el boot lo compila el build del SITIO con su propio esquema, y lo que UIX escribe en <html dir> lleva marca de propiedad (fila 2 del boot, F1 del cierre) F1 del plan de cierre del framework. Cierra el último trabajo del eje boot. EL DEFECTO. El boot por defecto compilaba un catálogo de preferencias de UN solo idioma (createDefaultUixPrefsSchema), y la raíz sembraba su entorno leyendo el <html dir> que el boot acababa de escribir: un usuario árabe de una app multiidioma no veía un parpadeo, se quedaba en LTR TODA la sesión, porque el runtime tomaba la salida del boot como si la hubiera declarado la página. EL COMPILADOR POR SITIO. Una sola fusión, composeUixPrefsSchema, la consumen createActiveUix y el boot: no puede derivar. El generador acepta --schema y --out (el especificador boot/boot-schema.ts apunta al esquema del sitio o a boot/default-schema.ts); los guards (sin runas, techo de tamaño, ASCII, sin </script, valores string por atributo) son errores del COMPILADOR con mensajes para el consumidor, porque ahora el artefacto lo produce el sitio. Un esquema que importa el barrel $prefs se rechaza nombrándolo. scripts/uix-boot-check.ts es el guard de rancidez: plugin de Vite que tumba el build con un artefacto viejo (probado con vite build real) y función para CI. Delta cero por esquema: por defecto, multiidioma con árabe, ejes redefinidos y esquema parcial. LA MARCA DE PROPIEDAD, diseño firmado por el autor. El canon de dirección dice que <html dir> es una proyección y nunca una fuente, con una sola excepción: el dir del AUTOR en la plantilla o el servidor. El boot rompió la premisa de esa excepción. Todo lo que UIX escribe en <html dir> lleva ahora PREFS_DIR_PROJECTED_ATTR (el boot con valor «boot», cada proyección del runtime con un token de instancia); la semilla solo adopta un dir SIN marca. Un dir escrito por script no es una fuente: en ejecución la dirección se afirma con prefs.setIntent('direction') o options.prefs.environment, que ganan a la semilla. Se descartaron, midiendo, la marca con valor (cierra el script y congela la dirección al navegar entre layouts) y la inferencia por valor. dispose solo retira lo que todavía es SUYO: SvelteKit crea la raíz del layout nuevo ANTES de destruir la vieja (medido), y un retiro a ciegas dejaba el <html> de la raíz nueva sin dir, lang ni data-motion/sound/haptic (medido hoy: los nueve atributos a null). Mismo principio que el unstamp de sema, que comprueba data-event-id. esbuild se declara como devDependency EXACTA 0.27.4: los bytes del boot y su hash dependen del minificador, y una subida dentro de un rango rompería la sincronía. Boot 15 198 B, sha256-hsqdGYcrRu3oEc0Q3G/A67ApQT3q9c/vT9zMDgxROg8= (el hash se mueve: firmado por el autor). VERIFICACIÓN. Constructor Opus en cuatro rondas y adversarial Opus en dos pasadas independientes, con sus reproducciones repetidas tras cada cierre: en Chromium, entrar en árabe y pasar a inglés y a español sigue al idioma, y al revés también; el dir de plantilla gana antes y después de hidratar y en una raíz recreada; una raíz recreada sobre una viva sigue al idioma con y sin boot; tras create b → destroy a, <html> conserva lo que proyectó b. Diez defectos declarados por el adversarial, cerrados (D1–D10): marca de propiedad, esquema parcial que estampaba "undefined", vigilante de dev mudo, plugin sin test, receta de CI que no cargaba en jsdom, cifras y prosa, y un vite build real que se cae cuando el boot no compila. Mutaciones en rojo, restauradas byte a byte: la proyección no marca · dispose sin guard de dueño (también repetida por el coordinador: 3 rojos) · la semilla compara valor en vez de presencia · marcar el dir del autor. Suite entera 463 ficheros / 5 437 tests, exit 0 · check 0 errores en src/ y en scripts/ (89 en web/, ledger intacto) · docs:check 0/0 · generate:boot dos veces byte-idéntico. LO QUE NO CIERRA (al ledger de cierre): (1) PREEXISTENTE — en la misma navegación entre layouts, el ActiveEidos.dispose de la raíz vieja sigue retirando data-theme/mode/density/scaling de la raíz nueva; exige cambiar el dispose de eidos, otra capa. (2) La propiedad de los atributos proyectados cuelga de la marca de dir: si la plantilla declara la dirección, dispose deja lang y data-motion puestos cuando la raíz se desmonta sin sustituta (benigno). (3) Sin boot, un script que escribe dir antes de la primera raíz es indistinguible de la plantilla y se adopta. (4) El vigilante de dev no reacciona a ficheros nuevos ni a inputs fuera del root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
/**
* uix-boot-check — the guard that breaks the build when the compiled boot
* on disk no longer describes the source it came from.
*
* The whole boot axis fails SILENTLY by construction. The tag is wrapped
* in a mute `try`/`catch` (it runs in `<head>`, before anything can
* report), a stale artifact stamps values that are merely OLD rather than
* invalid, and a browser that blocks the script leaves no trace in the
* page. So the only guard worth having is one that stops the build: a
* site that edits its preference schema and forgets to recompile must
* learn it from `vite build`, not from a user in Cairo reading an LTR
* page. (`createActiveUix`'s dev audit is the other half — it names the
* axis that moved, in the browser, after the fact.)
*
* It lives in `scripts/` and not under `src/uix` on purpose: it imports
* esbuild and Vite's plugin type, which are the HOST's build tools. The
* framework does not ship its consumer's bundler.
*
* Two shapes, one check:
* - `uixBootCheck()` — a Vite plugin. `buildStart` recompiles with the
* site's own configuration and fails the build on any difference. In
* dev it WARNS instead (a dev server that refuses to start over a
* stale cosmetic artifact helps nobody) and registers every input the
* compile read, so editing a dimension re-runs the check there and
* then instead of waiting for a deploy. That doctrine covers a compile
* that FAILS outright too — a half-typed schema module is the most
* ordinary state a watched file is ever in, and it must cost a line in
* the log, not the session.
* - `checkUixBootArtifact()` — the same check as a promise, for a site
* with no Vite (or a CI job that prefers a test over a build).
*/
import { readFileSync } from 'node:fs';
import { relative, resolve } from 'node:path';
import type { Plugin } from 'vite';
import { BOOT_OUTPUT, compileBoot, renderBootModule } from './generate-boot.ts';
export interface UixBootCheckOptions {
/**
* The module exporting `bootPrefsSchema` the artifact was compiled
* with — the same path passed to `generate-boot --schema`. Omitted,
* the framework's default schema.
*/
readonly schema?: string;
/** The artifact on disk. Defaults to the framework's own. */
readonly out?: string;
}
export interface UixBootCheckResult {
readonly ok: boolean;
/** Why it is stale, in the words a site can act on. Absent when `ok`. */
readonly reason?: string;
/** Every source file the compile read — what dev watches. */
readonly inputs: readonly string[];
}
/**
* Recompile and compare. The comparison is over the artifact's TEXT, not
* over the hash alone: the hash lives in the same file, and a check that
* only compared hashes would pass on a file where someone edited the
* script and left the hash — the exact pair that makes a browser block
* the tag.
*/
export async function checkUixBootArtifact(
options: UixBootCheckOptions = {},
// Filled as the compile reads, and still filled when it throws — see
// `compileBoot`.
read = new Set<string>()
): Promise<UixBootCheckResult> {
const out = options.out === undefined ? BOOT_OUTPUT : resolve(options.out);
const schema = options.schema === undefined ? undefined : resolve(options.schema);
const { script, inputs } = await compileBoot({ schema }, read);
const expected = renderBootModule(script);
let actual: string;
try {
// LF: the artifact is written with LF and a checkout with
// `core.autocrlf` must not be read as a stale one.
actual = readFileSync(out, 'utf8').replace(/\r\n/g, '\n');
} catch {
return { ok: false, reason: `no compiled boot at ${out} — run the boot compiler`, inputs };
}
if (actual === expected) return { ok: true, inputs };
return {
ok: false,
reason: `the compiled boot at ${out} is stale — it no longer matches ${
schema === undefined ? 'the framework schema' : relative(process.cwd(), schema)
} and the modules it imports. Recompile it.`,
inputs
};
}
/**
* A compile that never produced a result — almost always a syntax error
* the site is halfway through typing into its schema module. It is a
* different event from a stale artifact and gets its own sentence,
* because the artifact on disk may well be fine.
*/
function compileFailure(error: unknown): string {
return `could not compile the boot: ${error instanceof Error ? error.message : String(error)}`;
}
export function uixBootCheck(options: UixBootCheckOptions = {}): Plugin {
let serve = false;
// The files the last compile READ. A file that is NOT one of them cannot
// make the artifact stale, and a file that becomes one can only do it
// through an edit to a file that already is — so membership is enough to
// decide whether a save is worth a recompile.
//
// That holds for a compile that FAILED too, as long as the set is the one
// it read before it broke: the file that broke it is in there, and so is
// every file that imports it. Measured, twice: a set kept only from
// SUCCESSFUL compiles was empty on a dev server that started on a
// half-typed schema, and never held a module the schema imported broken
// for the first time — either way the fix was discarded as "not an input"
// and the guard stayed silent. Checking every save instead while broken
// was measured too: ten duplicate warnings from a SvelteKit start-up,
// which writes its own files.
let inputs = new Set<string>();
return {
name: 'uix-boot-check',
configResolved(config) {
serve = config.command === 'serve';
},
async buildStart() {
const read = new Set<string>();
let result: UixBootCheckResult | undefined;
let failure: unknown;
try {
result = await checkUixBootArtifact(options, read);
} catch (error) {
// In dev this is a warning like any other: a guard over a
// cosmetic artifact must never be the reason a dev server
// refuses to start, and the site is about to see the same
// syntax error from Vite itself. On a build there is nothing
// to recover to.
if (!serve) throw error;
failure = error;
}
inputs = read;
// In dev the input files are registered so a later edit re-runs
// the check; on a build there is no "later", so a difference is
// the end of the build.
if (serve) for (const input of read) this.addWatchFile(input);
if (result === undefined) {
this.warn(compileFailure(failure));
return;
}
if (result.ok) return;
if (serve) this.warn(result.reason!);
else this.error(result.reason!);
},
configureServer(server) {
server.watcher.on('change', (file) => {
if (!inputs.has(resolve(file))) return;
const read = new Set<string>();
void checkUixBootArtifact(options, read)
.then((result) => {
inputs = new Set(result.inputs);
if (result.ok) return;
server.config.logger.warn(`[uix-boot-check] ${result.reason}`);
})
// Without this the rejection is unhandled and Node 24 ENDS
// THE PROCESS: one typo in a schema module and the session
// is over. (The compile itself no longer throws from
// outside a promise — see `inputCollector` in
// `generate-boot.ts` — but a guard that watches a file the
// user is editing has to survive every state that file
// passes through.)
.catch((error: unknown) => {
inputs = read;
server.config.logger.warn(`[uix-boot-check] ${compileFailure(error)}`);
});
});
}
};
}

Powered by TurnKey Linux.