|
|
|
|
@ -71,6 +71,46 @@ function grepSources(root: string, pattern: RegExp): string[] {
|
|
|
|
|
return listSourceFiles(root).filter((file) => pattern.test(readFileSync(file, 'utf8')));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stripComments(source: string): string {
|
|
|
|
|
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectKnownMorfoDataAttrs(): Set<string> {
|
|
|
|
|
const attrs = new Set<string>();
|
|
|
|
|
for (const file of listSourceFiles(join(HERE, 'morfo', 'components'))) {
|
|
|
|
|
const source = stripComments(readFileSync(file, 'utf8'));
|
|
|
|
|
const component = /kebab:\s*'([^']+)'/.exec(source)?.[1];
|
|
|
|
|
if (!component) continue;
|
|
|
|
|
|
|
|
|
|
const partRe = /kebab:\s*'([^']+)'/g;
|
|
|
|
|
let partMatch: RegExpExecArray | null;
|
|
|
|
|
while ((partMatch = partRe.exec(source))) {
|
|
|
|
|
const part = partMatch[1];
|
|
|
|
|
attrs.add(part === 'provider' ? `data-${component}` : `data-${component}-${part}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dataRe = /attr:\s*'([^']+)'/g;
|
|
|
|
|
let dataMatch: RegExpExecArray | null;
|
|
|
|
|
while ((dataMatch = dataRe.exec(source))) {
|
|
|
|
|
if (dataMatch[1].startsWith('data-')) attrs.add(dataMatch[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return attrs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectDataAttrs(source: string): Set<string> {
|
|
|
|
|
const clean = stripComments(source);
|
|
|
|
|
const attrs = new Set<string>();
|
|
|
|
|
const stringRe = /['"`](data-[a-zA-Z0-9_-]+)['"`]/g;
|
|
|
|
|
const inlineRe = /\b(data-[a-zA-Z0-9_-]+)(?=(?:=|\s|>|\/|}|\]))/g;
|
|
|
|
|
let match: RegExpExecArray | null;
|
|
|
|
|
|
|
|
|
|
while ((match = stringRe.exec(clean))) attrs.add(match[1]);
|
|
|
|
|
while ((match = inlineRe.exec(clean))) attrs.add(match[1]);
|
|
|
|
|
|
|
|
|
|
return attrs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('UIX layer contracts', () => {
|
|
|
|
|
it('pins ActiveUix public service names and rejects legacy aliases', () => {
|
|
|
|
|
const uix = createActiveUix({ langs: minimalLang });
|
|
|
|
|
@ -205,6 +245,27 @@ describe('UIX layer contracts', () => {
|
|
|
|
|
expect(violations).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('guards Soma from direct mutable DOM writes', () => {
|
|
|
|
|
const violations = grepSources(
|
|
|
|
|
join(HERE, 'soma'),
|
|
|
|
|
/\.setAttribute\(|\.removeAttribute\(|\.toggleAttribute\(|\.classList\.|\.style\.setProperty\(|\.style\.[a-zA-Z]+\s*=/
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(violations).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('guards hardcoded Soma component data attrs with morfo contracts', () => {
|
|
|
|
|
const knownAttrs = collectKnownMorfoDataAttrs();
|
|
|
|
|
const violations = listSourceFiles(join(HERE, 'soma', 'components')).flatMap((file) => {
|
|
|
|
|
const attrs = collectDataAttrs(readFileSync(file, 'utf8'));
|
|
|
|
|
return [...attrs]
|
|
|
|
|
.filter((attr) => !knownAttrs.has(attr))
|
|
|
|
|
.map((attr) => `${file.replace(REPO_ROOT, '')}: ${attr}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(violations).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);
|
|
|
|
|
|