Files
a-share-analysis/tools/boundaries/verify-project-boundaries.test.mjs
T

143 lines
4.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const checkerPath = path.resolve('tools/boundaries/verify-project-boundaries.mjs');
async function createFixture() {
const repositoryRoot = await mkdtemp(path.join(tmpdir(), 'a-share-boundary-'));
await mkdir(path.join(repositoryRoot, 'web/src/api/generated'), { recursive: true });
await mkdir(path.join(repositoryRoot, 'server/libs'), { recursive: true });
await writeFile(
path.join(repositoryRoot, 'web/src/api/generated/schema.ts'),
'export interface paths {}\n',
);
return repositoryRoot;
}
async function runChecker(repositoryRoot) {
try {
await execFileAsync(process.execPath, [checkerPath, '--root', repositoryRoot]);
return { exitCode: 0, stderr: '' };
} catch (error) {
return {
exitCode: error.code,
stderr: String(error.stderr),
};
}
}
test('拒绝 Web 通过静态、动态或导出语句越界依赖 Server', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/illegal.ts'),
[
"import '../../server/libs/static';",
"export { value } from '../../server/libs/exported';",
"void import('../../server/libs/dynamic');",
].join('\n'),
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /Web 禁止依赖 Server/);
assert.match(result.stderr, /illegal\.ts/);
});
test('拒绝生成契约泄漏 RocketMQ 内部消息结构', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/api/generated/schema.ts'),
'export interface MessageEnvelope { messageId: string }\n',
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /生成契约包含 Server 内部消息/);
});
test('拒绝通过 tsconfig 路径别名依赖 Server', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/tsconfig.json'),
JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@server/*': ['../server/*'] },
},
}),
);
await writeFile(
path.join(repositoryRoot, 'web/src/aliased.ts'),
"import type { Internal } from '@server/libs/internal';\nexport type Leaked = Internal;\n",
);
await writeFile(
path.join(repositoryRoot, 'server/libs/internal.ts'),
'export interface Internal { secret: string }\n',
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /Web 禁止依赖 Server/);
});
test('拒绝 Vue 单文件组件脚本越界依赖 Server', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/IllegalView.vue'),
`<script setup lang="ts">
import type { Internal } from '../../server/libs/internal';
const value: Internal | undefined = undefined;
</script>
<template><main>{{ value }}</main></template>
`,
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /Web 禁止依赖 Server/);
assert.match(result.stderr, /IllegalView\.vue/);
});
for (const [name, scriptTag] of [
['双引号', '<script lang="ts" src="../../server/libs/internal.ts">'],
['单引号', "<script src='../../server/libs/internal.ts' lang='ts'>"],
['无引号且属性换序', '<script lang=ts src=../../server/libs/internal.ts>'],
]) {
test(`拒绝 Vue 外部 script src 通过${name}绕过源码边界`, async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/ExternalScript.vue'),
`${scriptTag}</script>\n<template><main /></template>\n`,
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /禁止使用外部 script src/);
assert.match(result.stderr, /ExternalScript\.vue/);
});
}
test('接受只消费公开 OpenAPI 类型的 Web 工程', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/legal.ts'),
"import type { paths } from './api/generated/schema';\nexport type ApiPaths = paths;\n",
);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 0);
assert.equal(result.stderr, '');
});