import { test } from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' /** task-213:前端共享层导出白名单。页面不得直连 shared/api/http 或拼 URL;只经 API 层/桥。 */ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'src') function walk(dir: string): string[] { const out: string[] = [] for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name) if (entry.isDirectory()) out.push(...walk(full)) else if (entry.name.endsWith('.ts') || entry.name.endsWith('.vue')) out.push(full) } return out } const relative = (f: string) => path.relative(ROOT, f).replaceAll('\\', '/') /** 命中直连 http 模块 import 的文件(http.ts 内部及 shared 内部允许)。 */ function httpDirectImports(files: string[]): string[] { const hits: string[] = [] for (const f of files) { const text = fs.readFileSync(f, 'utf8') const imp = /from\s+['"][^'"]*(?:shared\/)?api\/http(?:\.ts)?['"]/.test(text) if (imp && !relative(f).startsWith('shared/')) hits.push(relative(f)) } return hits } /** 直接 import axios(应只在 shared/api/http.ts)。 */ function axiosDirectImports(files: string[]): string[] { const hits: string[] = [] for (const f of files) { const text = fs.readFileSync(f, 'utf8') if (/from\s+['"]axios['"]/.test(text) && relative(f) !== 'shared/api/http.ts') { hits.push(relative(f)) } } return hits } const allFiles = walk(ROOT) test('页面不直连 shared/api/http', () => { assert.deepEqual(httpDirectImports(allFiles), []) }) test('页面不直接 import axios(收敛到 http.ts)', () => { assert.deepEqual(axiosDirectImports(allFiles), []) }) test('违规 import 可被检出(helper 判真)', () => { const tmp = path.join(ROOT, 'pages/__tmp_violation__.ts') fs.writeFileSync(tmp, "import { http } from '../../shared/api/http.ts'\n") try { const hits = httpDirectImports(walk(ROOT)) assert.ok(hits.some((h) => h.includes('pages/__tmp_violation__')), '应检出 shared 外违规 import') } finally { fs.rmSync(tmp) } }) test('shared 内部 import http 被放行(允许项)', () => { const tmp = path.join(ROOT, 'shared/api/__tmp_internal__.ts') fs.writeFileSync(tmp, "import { http } from './http.ts'\n") try { const hits = httpDirectImports([tmp]) assert.deepEqual(hits, [], 'shared 内部允许直连 http') } finally { fs.rmSync(tmp) } }) test('页面经 java-modules/shared api 层调用(存在且被引用)', () => { const texts = allFiles.filter((f) => relative(f).startsWith('pages/') || /-main\.ts$/.test(relative(f))) .map((f) => fs.readFileSync(f, 'utf8')) const usingApi = texts.filter((t) => /shared\/api\/(java-modules|types\/modules|progress-light|permission|upload|user)/.test(t)) assert.ok(usingApi.length >= 1, '应至少一个页面入口使用共享 API 层') }) test('bridge 适配层 import http 被放行', () => { const bridges = allFiles.filter((f) => relative(f).startsWith('shared/bridges/')) for (const f of bridges) { const text = fs.readFileSync(f, 'utf8') if (/api\/http/.test(text)) return // 存在 bridge 使用 http,属允许 } // 无 bridge 使用 http 也接受(不强求) assert.ok(true) }) test('页面文件不得拼接 /newApi URL 字面量', () => { const pages = allFiles.filter((f) => relative(f).startsWith('pages/') || /-main\.ts$/.test(relative(f))) for (const f of pages) { const text = fs.readFileSync(f, 'utf8') assert.ok(!text.includes("'/newApi") && !text.includes('"/newApi'), `页面不应拼 URL: ${relative(f)}`) } }) test('白名单扫描可重复、结果稳定', () => { assert.deepEqual(httpDirectImports(allFiles), httpDirectImports(allFiles)) assert.deepEqual(axiosDirectImports(allFiles), axiosDirectImports(allFiles)) })