Files
PersonalBlog/tests/architecture.test.ts
T
bot-implementer 1d649648da
CI / Workspace bootstrap checks (pull_request) Successful in 4m41s
feat: bootstrap pnpm workspace (E00-S01)
pnpm 11.23.0 workspace with a committed frozen lockfile, Node engine
restricted to the 24.x line, TypeScript 6.0.3 pinned exactly, strict base
tsconfig, apps/packages/extensions kept as separate workspaces, root
build/test/typecheck commands, and the FIT-001 architecture test rejecting
core-to-extension imports. Minimal CI runs only the root scripts (S05
stages stay out of scope). Closes #58.
2026-08-27 23:00:54 +00:00

146 lines
5.4 KiB
TypeScript

/**
* 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);
});
});