/** * FIT-001: no core package imports a concrete extension. * * Architecture guard (CI-enforced, see .gitea/workflows/ci.yml): * - a "concrete extension" is any workspace package under extensions/ * - code under packages/ (the core packages) must never import one, neither * by package name nor by a relative path that resolves into extensions/ * * The rule is discovered from the filesystem, so adding an extension package * under extensions/ automatically extends the guard; no manifest edits needed. */ import { describe, expect, it } from 'vitest'; import { readdirSync, readFileSync, existsSync } from 'node:fs'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); const packagesRoot = join(repoRoot, 'packages'); const extensionsRoot = join(repoRoot, 'extensions'); interface ExtensionInfo { /** Absolute path of the extension package directory. */ dir: string; /** Package name from its manifest (e.g. "@personal-blog/example-extension"). */ name: string; } /** True when `candidate` is `parent` itself or lies inside it. */ function isWithin(parent: string, candidate: string): boolean { const rel = relative(parent, candidate); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); } function findExtensionPackages(): ExtensionInfo[] { if (!existsSync(extensionsRoot)) { return []; } return readdirSync(extensionsRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry): ExtensionInfo | null => { const manifestPath = join(extensionsRoot, entry.name, 'package.json'); if (!existsSync(manifestPath)) { return null; } const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: string; }; return manifest.name ? { dir: join(extensionsRoot, entry.name), name: manifest.name } : null; }) .filter((info): info is ExtensionInfo => info !== null); } /** Matches static imports, side-effect imports, dynamic imports and require. */ const IMPORT_SPECIFIER_RE = /(?:from\s*|import\s*\(\s*|require\s*\(\s*|import\s+)(['"])([^'"]+)\1/g; function extractImportSpecifiers(source: string): string[] { return [...source.matchAll(IMPORT_SPECIFIER_RE)] .map((match) => match[2] ?? '') .filter((specifier) => specifier.length > 0); } /** True when `specifier` targets a concrete extension from `importerFile`. */ function isExtensionImport( specifier: string, importerFile: string, extensions: ExtensionInfo[], ): boolean { for (const ext of extensions) { if (specifier === ext.name || specifier.startsWith(`${ext.name}/`)) { return true; } } if (specifier.startsWith('.')) { const resolved = resolve(dirname(importerFile), specifier); if (isWithin(extensionsRoot, resolved)) { return true; } } return false; } function listSourceFiles(dir: string): string[] { if (!existsSync(dir)) { return []; } const files: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isDirectory()) { files.push(...listSourceFiles(path)); } else if (entry.isFile() && /\.(?:[cm]?ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) { files.push(path); } } return files; } describe('FIT-001: no core package imports a concrete extension', () => { const extensions = findExtensionPackages(); it('finds at least one concrete extension package to check against', () => { // Guards against a vacuous pass when extensions/ is empty. expect(extensions.length).toBeGreaterThan(0); }); it('packages/* sources import no concrete extension', () => { const violations: string[] = []; for (const file of listSourceFiles(packagesRoot)) { const source = readFileSync(file, 'utf8'); for (const specifier of extractImportSpecifiers(source)) { if (isExtensionImport(specifier, file, extensions)) { violations.push( `${relative(repoRoot, file)}: import "${specifier}" targets concrete extension "${extensions .filter((ext) => isExtensionImport(specifier, file, [ext])) .map((ext) => ext.name) .join(', ')}"`, ); } } } expect(violations).toEqual([]); }); it('detector rejects extension imports and accepts benign imports (sanity)', () => { const ext = extensions[0]; expect(ext, 'expected at least one extension package').toBeDefined(); if (!ext) { return; } const importer = join(packagesRoot, 'core', 'src', 'index.ts'); // By package name (bare and subpath). expect(isExtensionImport(ext.name, importer, extensions)).toBe(true); expect(isExtensionImport(`${ext.name}/server`, importer, extensions)).toBe(true); // By relative path resolving into extensions/. expect( isExtensionImport(relative(dirname(importer), join(ext.dir, 'src', 'index.js')), importer, extensions), ).toBe(true); // Benign imports must pass. expect(isExtensionImport('node:test', importer, extensions)).toBe(false); expect(isExtensionImport('vitest', importer, extensions)).toBe(false); expect(isExtensionImport('@personal-blog/core', importer, extensions)).toBe(false); expect(isExtensionImport('./site.js', importer, extensions)).toBe(false); }); });