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/src/uix/eidos/component-api-contract.test.ts

139 lines
4.5 KiB

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const COMPONENTS_DIR = 'src/uix/eidos/components'
const ROOT_FILE_EXCEPTIONS = new Set(['svg'])
const COMPONENT_SOURCE_FILE = /\.(css|svelte|ts)$/
interface NamespaceContract {
component: string
properties: readonly string[]
assignments: readonly string[]
}
function stripComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
}
function readComponentDirs(): readonly string[] {
return readdirSync(COMPONENTS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort()
}
function readComponentSourceFiles(dir = COMPONENTS_DIR): readonly { path: string; source: string }[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) return readComponentSourceFiles(path)
if (!entry.isFile() || !COMPONENT_SOURCE_FILE.test(entry.name)) return []
return [{ path, source: stripComments(readFileSync(path, 'utf8')) }]
})
}
function readIndexSource(component: string): string {
return stripComments(readFileSync(join(COMPONENTS_DIR, component, 'index.ts'), 'utf8'))
}
function readNamespaceContract(source: string): NamespaceContract | undefined {
const match = /type\s+([A-Z][A-Za-z0-9]+)Namespace\s*=\s*typeof\s+\w+Root\s*&\s*\{([\s\S]*?)\n\}/.exec(
source
)
if (!match) return undefined
const component = match[1]
const body = match[2]
const properties = [...body.matchAll(/^\s*([A-Z][A-Za-z0-9]+)\s*:/gm)].map((m) => m[1])
const assignments = [
...source.matchAll(new RegExp(`\\b${component}\\.([A-Z][A-Za-z0-9]+)\\s*=`, 'g'))
].map((m) => m[1])
return { component, properties, assignments }
}
describe('Eidos component API contract', () => {
it('keeps component barrels on the disciplined option C surface', () => {
const violations: string[] = []
for (const component of readComponentDirs()) {
const source = readIndexSource(component)
if (/\bObject\.assign\s*\(/.test(source)) {
violations.push(`${component}: Object.assign`)
}
if (/export\s+(?:type\s+)?\{[^}]*\bProvider\b[^}]*\}/s.test(source)) {
violations.push(`${component}: public Provider export`)
}
if (/from\s+['"]\.\/.*provider\.svelte['"]/.test(source)) {
violations.push(`${component}: provider component import`)
}
}
expect(violations).toEqual([])
})
it('keeps removed flat API markers out of component source', () => {
const violations = readComponentSourceFiles()
.filter(({ source }) => /\bdata-[a-z0-9-]+-flat\b/.test(source))
.map(({ path }) => path)
expect(violations).toEqual([])
})
it('keeps every public Eidos component rooted at {component}.svelte', () => {
const missingRoots = readComponentDirs()
.filter((component) => !ROOT_FILE_EXCEPTIONS.has(component))
.filter((component) => !existsSync(join(COMPONENTS_DIR, component, `${component}.svelte`)))
expect(missingRoots).toEqual([])
})
it('keeps namespace type members and explicit assignments in sync', () => {
const violations: string[] = []
for (const component of readComponentDirs()) {
const contract = readNamespaceContract(readIndexSource(component))
if (!contract) continue
const declared = new Set(contract.properties)
const assigned = new Set(contract.assignments)
for (const property of declared) {
if (!assigned.has(property)) {
violations.push(`${component}: ${contract.component}.${property} declared but not assigned`)
}
}
for (const property of assigned) {
if (!declared.has(property)) {
violations.push(`${component}: ${contract.component}.${property} assigned but not declared`)
}
}
}
expect(violations).toEqual([])
})
it('keeps component wrappers free of legacy lifecycle and scoped styles', () => {
const violations: string[] = []
for (const component of readComponentDirs()) {
const dir = join(COMPONENTS_DIR, component)
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.svelte')) continue
const path = join(dir, entry.name)
const source = readFileSync(path, 'utf8')
if (/\bonMount\b/.test(source)) violations.push(`${component}/${entry.name}: onMount`)
if (/<style(?:\s|>)/.test(source)) violations.push(`${component}/${entry.name}: <style>`)
if (/\bdata-soma-/.test(source)) {
violations.push(`${component}/${entry.name}: data-soma-*`)
}
}
}
expect(violations).toEqual([])
})
})

Powered by TurnKey Linux.