test: 补强架构收敛导入门禁

This commit is contained in:
sunlei 2026-06-15 10:09:15 +08:00
parent e66d3fd824
commit c3b0476dcc

View File

@ -1,15 +1,27 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path'; import {
dirname,
join,
normalize,
relative,
resolve,
sep,
} from 'node:path';
const repoRoot = join(__dirname, '..', '..'); const repoRoot = join(__dirname, '..', '..');
const srcRoot = join(repoRoot, 'src'); const srcRoot = join(repoRoot, 'src');
const legacyRoots = ['admin', 'blog', 'minio', 'wordpress', 'qqbot']; const legacyRootNames = ['admin', 'blog', 'minio', 'wordpress', 'qqbot'];
const legacyRootPaths = legacyRootNames.map((root) => join(srcRoot, root));
const moduleRoots = ['admin', 'asset', 'blog', 'wordpress', 'qqbot']; const moduleRoots = ['admin', 'asset', 'blog', 'wordpress', 'qqbot'];
const qqbotRoots = ['core', 'napcat', 'plugin-platform', 'plugins']; const qqbotRoots = ['core', 'napcat', 'plugin-platform', 'plugins'];
function isDirectory(path: string): boolean {
return existsSync(path) && statSync(path).isDirectory();
}
function listFiles(dir: string): string[] { function listFiles(dir: string): string[] {
if (!existsSync(dir)) return []; if (!isDirectory(dir)) return [];
return readdirSync(dir).flatMap((entry) => { return readdirSync(dir).flatMap((entry) => {
const absolute = join(dir, entry); const absolute = join(dir, entry);
const stat = statSync(absolute); const stat = statSync(absolute);
@ -18,19 +30,51 @@ function listFiles(dir: string): string[] {
}); });
} }
function readTextFiles(dir: string): Array<{ file: string; text: string }> { function readTextFiles(
dir: string,
): Array<{ absolute: string; file: string; text: string }> {
return listFiles(dir) return listFiles(dir)
.filter((file) => /\.(ts|tsx|vue|js|mjs|cjs)$/.test(file)) .filter((file) => /\.(ts|tsx|vue|js|mjs|cjs)$/.test(file))
.map((file) => ({ .map((file) => ({
absolute: file,
file: relative(repoRoot, file).replace(/\\/g, '/'), file: relative(repoRoot, file).replace(/\\/g, '/'),
text: readFileSync(file, 'utf8'), text: readFileSync(file, 'utf8'),
})); }));
} }
function extractImportSpecifiers(text: string): string[] {
const importPattern =
/(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;
return [...text.matchAll(importPattern)]
.map((match) => match[1] || match[2])
.filter((specifier): specifier is string => Boolean(specifier));
}
function resolvesInsideLegacyRoot(fromFile: string, specifier: string): boolean {
let resolved: string | null = null;
if (specifier === '@') resolved = srcRoot;
if (specifier.startsWith('@/')) {
resolved = join(srcRoot, specifier.slice(2));
}
if (specifier.startsWith('.')) {
resolved = resolve(dirname(fromFile), specifier);
}
if (!resolved) return false;
const normalized = normalize(resolved);
return legacyRootPaths.some((legacyRoot) => {
const normalizedLegacy = normalize(legacyRoot);
return (
normalized === normalizedLegacy ||
normalized.startsWith(`${normalizedLegacy}${sep}`)
);
});
}
describe('architecture convergence', () => { describe('architecture convergence', () => {
it('does not keep API legacy source roots', () => { it('does not keep API legacy source roots', () => {
const existing = legacyRoots.filter((root) => const existing = legacyRootNames.filter((root) =>
existsSync(join(srcRoot, root)), isDirectory(join(srcRoot, root)),
); );
expect(existing).toEqual([]); expect(existing).toEqual([]);
@ -38,7 +82,7 @@ describe('architecture convergence', () => {
it('keeps all business modules under src/modules', () => { it('keeps all business modules under src/modules', () => {
const missing = moduleRoots.filter( const missing = moduleRoots.filter(
(root) => !existsSync(join(srcRoot, 'modules', root)), (root) => !isDirectory(join(srcRoot, 'modules', root)),
); );
expect(missing).toEqual([]); expect(missing).toEqual([]);
@ -46,29 +90,30 @@ describe('architecture convergence', () => {
it('keeps QQBot subdomains under src/modules/qqbot', () => { it('keeps QQBot subdomains under src/modules/qqbot', () => {
const missing = qqbotRoots.filter( const missing = qqbotRoots.filter(
(root) => !existsSync(join(srcRoot, 'modules', 'qqbot', root)), (root) => !isDirectory(join(srcRoot, 'modules', 'qqbot', root)),
); );
expect(missing).toEqual([]); expect(missing).toEqual([]);
}); });
it('does not import old roots from src/modules', () => { it('does not import old roots from src/modules', () => {
const forbidden = /@\/(?:admin|blog|minio|wordpress|qqbot)\//;
const offenders = readTextFiles(join(srcRoot, 'modules')) const offenders = readTextFiles(join(srcRoot, 'modules'))
.filter(({ text }) => forbidden.test(text)) .flatMap(({ absolute, file, text }) =>
.map(({ file }) => file); extractImportSpecifiers(text)
.filter((specifier) => resolvesInsideLegacyRoot(absolute, specifier))
.map((specifier) => `${file} -> ${specifier}`),
);
expect(offenders).toEqual([]); expect(offenders).toEqual([]);
}); });
it('does not import old roots from app module', () => { it('does not import old roots from app module', () => {
const appModule = readFileSync(join(srcRoot, 'app.module.ts'), 'utf8'); const appModulePath = join(srcRoot, 'app.module.ts');
const appModule = readFileSync(appModulePath, 'utf8');
const offenders = extractImportSpecifiers(appModule).filter((specifier) =>
resolvesInsideLegacyRoot(appModulePath, specifier),
);
expect(appModule).not.toMatch( expect(offenders).toEqual([]);
/from ['"]\.\/(?:admin|blog|minio|wordpress|qqbot)/,
);
expect(appModule).not.toMatch(
/from ['"]@\/(?:admin|blog|minio|wordpress|qqbot)\//,
);
}); });
}); });