Harden Eidos runtime config validation

active-uix
dev 5 months ago
parent bfd3b4020e
commit 8e17e64d13

@ -18,9 +18,16 @@ Actualizacion Eidos ecosystem 2026-05-16:
- Guardia anadida: el contrato CSS de Eidos debe estar alineado con los
renderers generados; cada token `static` debe aparecer en `renderStaticCss()`
y cada token `theme` en `renderThemeCss('base-light')`.
- Overrides runtime reforzados: `ActiveEidos` valida y clona `cssVariables`
iniciales aunque `applyDom:false`, y `setCssVariables(...)` es
transaccional. Un mapa invalido no reemplaza el bloque de variables vigente.
- Validacion de config reforzada: documentos persistidos con `options`
malformadas fallan como `EidosConfigValidationError` con issues de contrato,
no como `TypeError` de runtime.
- Validado:
- `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 26 tests OK.
- `npx vitest run src/uix/eidos` -> 7 archivos, 65 tests OK.
- `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 27 tests OK.
- `npx vitest run src/uix/eidos/active-eidos.test.ts` -> 19 tests OK.
- `npx vitest run src/uix/eidos` -> 7 archivos, 67 tests OK.
- `npm run check` -> 0 errores, 0 warnings.
Actualizacion permutation runner 2026-05-16:

@ -437,7 +437,9 @@ activeEidos.setCssVariables({
`ActiveEidos` lo escribe en `${styleId}-variables` como un `<style>`
gestionado. Por defecto valida los nombres contra `getCssContract()`; si una
app necesita variables locales fuera del contrato puede usar
`{ strict: false }`.
`{ strict: false }`. La actualizacion es transaccional: primero renderiza y
valida el siguiente bloque, y solo reemplaza el estado runtime si el mapa es
usable. Un token desconocido no deja el style block anterior a medias.
No crear `ActiveEidos` desactiva la capa visual runtime de Eidos. Los
componentes Soma siguen funcionando headless porque el comportamiento pertenece

@ -316,6 +316,28 @@ describe('ActiveEidos config', () => {
)
})
it('reports malformed persisted config options as validation issues', () => {
const malformed = {
kind: EIDOS_CONFIG_DOCUMENT_KIND,
version: EIDOS_CONFIG_DOCUMENT_VERSION,
options: {}
}
try {
createEidosFromDocument(malformed)
throw new Error('expected malformed config to fail')
} catch (error) {
expect(error).toBeInstanceOf(EidosConfigValidationError)
expect((error as EidosConfigValidationError).issues.map((issue) => issue.path)).toEqual(
expect.arrayContaining(['primitives', 'semantics.color.roles'])
)
}
expect(() => parseEidosConfigFromJson(JSON.stringify(malformed))).toThrow(
EidosConfigValidationError
)
})
it('ships typography families separately from typography styles', () => {
const typography = createThemeBaseEidos().snapshot().primitives.typography!

@ -165,7 +165,7 @@ export class ActiveEidos {
this.#themeSource = options.themeSource ?? 'auto';
this.#themeResolver = options.themeResolver ?? defaultActiveEidosThemeResolver;
this.#applyDom = options.applyDom !== false;
this.#cssVariables = options.cssVariables;
this.#cssVariables = options.cssVariables ? { ...options.cssVariables } : undefined;
this.#cssVariablesOptions = {
selector: options.cssVariablesSelector,
strict: options.cssVariablesStrict
@ -175,6 +175,8 @@ export class ActiveEidos {
throw new ActiveEidosConfigError('dom service is required when applyDom is enabled');
}
if (this.#cssVariables) this.renderCssVariables(this.#cssVariables, this.#cssVariablesOptions);
if (this.#applyDom) {
this.apply();
this.#unsubscribe = this.#preferences.onPreferenceChange(() => this.apply());
@ -340,11 +342,14 @@ export class ActiveEidos {
variables: EidosCssVariableMap,
renderOptions: ActiveEidosCssVariablesOptions = {}
): void {
this.#cssVariables = { ...variables };
this.#cssVariablesOptions = {
const nextVariables = { ...variables };
const nextOptions = {
...this.#cssVariablesOptions,
...renderOptions
};
this.renderCssVariables(nextVariables, nextOptions);
this.#cssVariables = nextVariables;
this.#cssVariablesOptions = nextOptions;
this.apply();
}

@ -10,6 +10,7 @@ import {
createActiveEidos,
type ActiveEidosPreferenceSource
} from './active-eidos.svelte';
import { EidosCssVariableError } from './errors';
function preferencesHarness(
initial: {
@ -227,6 +228,14 @@ describe('ActiveEidos', () => {
'--size-md-control-height: 42px;'
);
const previousVariablesCss = document.getElementById('uix-eidos-variables')?.textContent;
expect(() => eidos.setCssVariables({ '--unknown-token': '1px' })).toThrow(
EidosCssVariableError
);
expect(document.getElementById('uix-eidos-variables')?.textContent).toBe(
previousVariablesCss
);
eidos.clearCssVariables();
expect(document.getElementById('uix-eidos-variables')).toBeNull();
@ -262,6 +271,20 @@ describe('ActiveEidos', () => {
eidos.dispose();
});
it('validates initial runtime CSS variables even without DOM application', () => {
const { preferences } = preferencesHarness();
expect(() =>
createActiveEidos({
preferences,
applyDom: false,
cssVariables: {
'--unknown-token': '1px'
}
})
).toThrow(EidosCssVariableError);
});
it('requires an injected DOM service when applyDom is enabled', () => {
const { preferences } = preferencesHarness();

@ -81,6 +81,13 @@ export function serializeEidosConfig(options: EidosConfig): string {
export function validateEidosConfig(options: EidosConfig): EidosValidationReport {
const issues: EidosValidationIssue[] = []
if (!validateEidosConfigShape(options, issues)) {
return {
ok: false,
issues
}
}
validateColorScales(options, issues)
validateColorRoles(options, issues)
validateTypography(options, issues)
@ -117,6 +124,61 @@ export function assertValidEidosConfig(options: EidosConfig): void {
throw new EidosConfigValidationError(report.issues)
}
function validateEidosConfigShape(
options: EidosConfig,
issues: EidosValidationIssue[]
): options is EidosConfig {
const input = options as unknown
if (!isPlainRecord(input)) {
issues.push({
path: '',
message: 'Eidos config must be a plain object'
})
return false
}
let ok = true
if (!isPlainRecord(input.primitives)) {
issues.push({
path: 'primitives',
message: 'Eidos config must define a primitives object'
})
ok = false
}
const semantics = input.semantics
const color = isPlainRecord(semantics) ? semantics.color : undefined
const roles = isPlainRecord(color) ? color.roles : undefined
if (!isPlainRecord(roles)) {
issues.push({
path: 'semantics.color.roles',
message: 'Eidos config must define semantic color roles'
})
ok = false
}
if (input.themes !== undefined && !isPlainRecord(input.themes)) {
issues.push({
path: 'themes',
message: 'Eidos config themes must be a plain object'
})
ok = false
}
if (input.recipes !== undefined && !isPlainRecord(input.recipes)) {
issues.push({
path: 'recipes',
message: 'Eidos config recipes must be a plain object'
})
ok = false
}
return ok
}
export function listEidosColorScales(options: EidosConfig): readonly string[] {
return Object.keys(options.primitives.color?.scales ?? {})
}

Loading…
Cancel
Save

Powered by TurnKey Linux.