From a093963b048462f967eba03df4aa2210192963db Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 04:46:41 +0200 Subject: [PATCH] Guard component lang namespaces --- src/uix/contracts.test.ts | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/uix/contracts.test.ts b/src/uix/contracts.test.ts index 407239fdd..c9348e958 100644 --- a/src/uix/contracts.test.ts +++ b/src/uix/contracts.test.ts @@ -29,6 +29,7 @@ import { EngineSemantic, defineEngineSemantic } from './sema'; import { SemaConfigError } from './sema/errors'; import { createSomaRuntime } from './soma/runtime.svelte'; import { UIX_LAYER_CONTRACTS } from './contracts'; +import { componentLangs } from './langs'; const minimalLang = { schema: {} satisfies LangNode, defaultLocale: 'es' as const }; const prefsProjectionSchema = standardPrefsDimensions({ @@ -111,6 +112,36 @@ function collectDataAttrs(source: string): Set { return attrs; } +function collectComponentLangPathViolations(source: string, file: string): string[] { + const clean = stripComments(source); + const violations: string[] = []; + const refRe = /#\?components\.([^|'"`\s]+)/g; + let match: RegExpExecArray | null; + + while ((match = refRe.exec(clean))) { + const path = match[1]; + const segments = path.split('.'); + if (segments.some((segment) => /[A-Z]/.test(segment))) { + violations.push(`${file.replace(REPO_ROOT, '')}: components.${path}`); + } + } + + return violations; +} + +function collectLangCatalogKeyViolations( + node: unknown, + prefix = 'components' +): string[] { + if (!node || typeof node !== 'object') return []; + + return Object.entries(node as Record).flatMap(([key, value]) => { + const path = `${prefix}.${key}`; + const ownViolation = /[A-Z]/.test(key) ? [path] : []; + return [...ownViolation, ...collectLangCatalogKeyViolations(value, path)]; + }); +} + describe('UIX layer contracts', () => { it('pins ActiveUix public service names and rejects legacy aliases', () => { const uix = createActiveUix({ langs: minimalLang }); @@ -266,6 +297,21 @@ describe('UIX layer contracts', () => { expect(violations).toEqual([]); }); + it('guards component translation namespaces as kebab-case', () => { + const sourceRoots = [ + join(HERE, 'soma', 'components'), + join(HERE, 'morfo', 'components') + ]; + const pathViolations = sourceRoots.flatMap((root) => + listSourceFiles(root).flatMap((file) => + collectComponentLangPathViolations(readFileSync(file, 'utf8'), file) + ) + ); + + expect(pathViolations).toEqual([]); + expect(collectLangCatalogKeyViolations(componentLangs)).toEqual([]); + }); + it('events/sema uses events as service name and requires dom or projector for visual projection', () => { const factory = defineEngineSemantic({ visual: false }); expect(factory.name).toBe(UIX_LAYER_CONTRACTS.events.publicServiceName);