feat(sdk): P2.6 @insignia/iios-message-web (React hooks) + allowlist boundary
MessageProvider/useThread/useMessages over the kernel-client socket (live append, typing, dedupe). Reconnect test (re-opens active thread). Rewrote import-boundary as an allowlist so SDK<->service sibling imports are forbidden both ways. 28 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Import-boundary law (Bottom-Up §06 dependency law; Critics §12).
|
||||
*
|
||||
* Layers, low → high: contracts < testkit < service. A LOWER layer must never
|
||||
* import a HIGHER one. This guards the "no specialization leaks into the kernel"
|
||||
* rule from day one. Runnable as `pnpm boundary` (CI) and imported by a test.
|
||||
* Each package declares exactly which other @insignia/iios-* packages it MAY
|
||||
* import. Anything else is a violation. This is an allowlist, not a ladder —
|
||||
* it correctly forbids sibling imports (e.g. the SDK must never import the
|
||||
* backend service, and vice-versa). Runnable as `pnpm boundary` (CI) + a test.
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -11,12 +12,15 @@ import { dirname, join, resolve } from 'node:path';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/** Lower rank = lower layer. A package may only import packages of <= its rank. */
|
||||
const RANK = {
|
||||
'@insignia/iios-contracts': 0,
|
||||
'@insignia/iios-testkit': 1,
|
||||
'@insignia/iios-service': 2,
|
||||
/** package -> the @insignia/iios-* packages it is allowed to import. */
|
||||
const ALLOWED = {
|
||||
'@insignia/iios-contracts': [],
|
||||
'@insignia/iios-testkit': ['@insignia/iios-contracts'],
|
||||
'@insignia/iios-kernel-client': ['@insignia/iios-contracts'],
|
||||
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit'],
|
||||
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||
};
|
||||
const KNOWN = Object.keys(ALLOWED);
|
||||
|
||||
function listTsFiles(dir) {
|
||||
if (!existsSync(dir)) return [];
|
||||
@@ -25,13 +29,13 @@ function listTsFiles(dir) {
|
||||
if (entry === 'node_modules' || entry === 'dist') continue;
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) out.push(...listTsFiles(full));
|
||||
else if (full.endsWith('.ts')) out.push(full);
|
||||
else if (full.endsWith('.ts') || full.endsWith('.tsx')) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function packageOf(importPath) {
|
||||
return Object.keys(RANK).find((k) => importPath === k || importPath.startsWith(k + '/'));
|
||||
return KNOWN.find((k) => importPath === k || importPath.startsWith(k + '/'));
|
||||
}
|
||||
|
||||
export function findBoundaryViolations() {
|
||||
@@ -43,11 +47,9 @@ export function findBoundaryViolations() {
|
||||
const pkgJsonPath = join(pkgsDir, pkg, 'package.json');
|
||||
if (!existsSync(pkgJsonPath)) continue;
|
||||
const name = JSON.parse(readFileSync(pkgJsonPath, 'utf8')).name;
|
||||
const ownRank = RANK[name];
|
||||
if (ownRank === undefined) continue;
|
||||
const allowed = ALLOWED[name];
|
||||
if (!allowed) continue; // not a governed package
|
||||
|
||||
// Catch every module-specifier form: `from '…'`, side-effect `import '…'`,
|
||||
// dynamic `import('…')`, and `require('…')`.
|
||||
const patterns = [
|
||||
/from\s*['"](@insignia\/iios-[^'"]+)['"]/g,
|
||||
/import\s*['"](@insignia\/iios-[^'"]+)['"]/g,
|
||||
@@ -59,9 +61,9 @@ export function findBoundaryViolations() {
|
||||
for (const re of patterns) {
|
||||
let m;
|
||||
while ((m = re.exec(content))) {
|
||||
const targetPkg = packageOf(m[1]);
|
||||
if (targetPkg && RANK[targetPkg] > ownRank) {
|
||||
violations.push({ file: file.replace(ROOT + '/', ''), from: name, imports: targetPkg });
|
||||
const target = packageOf(m[1]);
|
||||
if (target && target !== name && !allowed.includes(target)) {
|
||||
violations.push({ file: file.replace(ROOT + '/', ''), from: name, imports: target });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,9 +75,9 @@ export function findBoundaryViolations() {
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const violations = findBoundaryViolations();
|
||||
if (violations.length > 0) {
|
||||
console.error('✗ import-boundary violations (a lower layer imports a higher one):');
|
||||
console.error('✗ import-boundary violations (a package imports a forbidden dependency):');
|
||||
for (const v of violations) console.error(` ${v.file}: ${v.from} → ${v.imports}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ import-boundary: OK (no lower→higher imports)');
|
||||
console.log('✓ import-boundary: OK');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user