task-1(壳层/路由): 冻结独立后台工程工作区边界契约与基线测试
admin-frontend-vue/src/config/workspace.ts 提供可编程边界校验(独立包名、 必需清单/源码目录、客户端耦合 token 扫描、可操作缺失消息),供后续壳层 任务与验收脚本复用;tests/task-1.test.ts 以 8 用例锁定依赖/构建/测试/边界。
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
export const STANDALONE_PACKAGE_NAME = 'crawler-plugin-admin-frontend-vue'
|
||||
|
||||
/** 后台壳层允许存在的源码目录(任务 1 冻结的工作区边界,供验收脚本复用)。 */
|
||||
export const ADMIN_SOURCE_DIRS = [
|
||||
'src/layout',
|
||||
'src/router',
|
||||
'src/styles',
|
||||
'src/types',
|
||||
'src/config',
|
||||
'src/pages',
|
||||
] as const
|
||||
|
||||
export const REQUIRED_MANIFEST_FILES = [
|
||||
'package.json',
|
||||
'index.html',
|
||||
'vite.config.ts',
|
||||
'tsconfig.json',
|
||||
'src/main.ts',
|
||||
'src/App.vue',
|
||||
] as const
|
||||
|
||||
// 客户端工程耦合标记:后台工程内出现即代表越界。用带路径/产物语义的标记,
|
||||
// 避免与本工程自身包名后缀(…admin-frontend-vue)误匹配。扫描时排除本文件自身。
|
||||
export const FORBIDDEN_COUPLING_TOKENS = [
|
||||
'new_web_source',
|
||||
'frontend-vue/',
|
||||
'app_client',
|
||||
] as const
|
||||
|
||||
const SELF_SCAN_EXCLUSIONS = new Set(['src/config/workspace.ts'])
|
||||
const SCAN_EXTENSIONS = /\.(ts|tsx|vue|css|html|json|js)$/
|
||||
|
||||
export function walk(rootDir: string, base = ''): string[] {
|
||||
const found: string[] = []
|
||||
for (const entry of readdirSync(join(rootDir, base), { withFileTypes: true })) {
|
||||
if (['node_modules', 'dist', '.git', 'tests'].includes(entry.name)) continue
|
||||
const rel = base ? `${base}/${entry.name}` : entry.name
|
||||
if (entry.isDirectory()) found.push(...walk(rootDir, rel))
|
||||
else if (SCAN_EXTENSIONS.test(entry.name)) found.push(rel)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function fileExists(rootDir: string, path: string): boolean {
|
||||
return existsSync(join(rootDir, path))
|
||||
}
|
||||
|
||||
/** 从当前目录向上定位独立后台工程根目录,找不到时给出可操作错误。 */
|
||||
export function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
||||
let dir = resolve(cwd)
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
if (fileExists(dir, 'package.json')) return dir
|
||||
const parent = resolve(dir, '..')
|
||||
if (parent === dir) break
|
||||
dir = parent
|
||||
}
|
||||
throw new Error(`在 ${cwd} 向上 6 层内找不到 package.json,请在 admin-frontend-vue 工程目录内执行`)
|
||||
}
|
||||
|
||||
export interface WorkspacePackage {
|
||||
name: string
|
||||
private: boolean
|
||||
scripts: Record<string, string>
|
||||
dependencies: Record<string, string>
|
||||
devDependencies: Record<string, string>
|
||||
}
|
||||
|
||||
export function readWorkspacePackage(rootDir = resolveWorkspaceRoot()): WorkspacePackage {
|
||||
const raw = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8')) as Partial<WorkspacePackage> & { private?: boolean }
|
||||
return {
|
||||
name: raw.name || '',
|
||||
private: raw.private !== false,
|
||||
scripts: raw.scripts || {},
|
||||
dependencies: raw.dependencies || {},
|
||||
devDependencies: raw.devDependencies || {},
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验独立工程边界,返回违规清单(空数组 = 边界成立)。 */
|
||||
export function collectBoundaryViolations(rootDir = resolveWorkspaceRoot()): string[] {
|
||||
const violations: string[] = []
|
||||
const pkgPath = join(rootDir, 'package.json')
|
||||
const pkg: WorkspacePackage = fileExists(rootDir, 'package.json')
|
||||
? readWorkspacePackage(rootDir)
|
||||
: { name: '', private: false, scripts: {}, dependencies: {}, devDependencies: {} }
|
||||
if (!fileExists(rootDir, 'package.json')) violations.push(`缺少必需清单文件 package.json(在 ${pkgPath})`)
|
||||
|
||||
if (pkg.name !== STANDALONE_PACKAGE_NAME) {
|
||||
violations.push(`package.name 应为 ${STANDALONE_PACKAGE_NAME},实际为 ${pkg.name || '(空)'}`)
|
||||
}
|
||||
if (!pkg.private) violations.push('package.private 应为 true(后台工程不允许被外部发布依赖)')
|
||||
|
||||
for (const file of REQUIRED_MANIFEST_FILES) {
|
||||
if (!fileExists(rootDir, file)) violations.push(`缺少必需清单文件 ${file}`)
|
||||
}
|
||||
for (const dir of ADMIN_SOURCE_DIRS) {
|
||||
if (!fileExists(rootDir, dir)) violations.push(`缺少允许源码目录 ${dir}`)
|
||||
}
|
||||
|
||||
for (const file of walk(rootDir)) {
|
||||
if (SELF_SCAN_EXCLUSIONS.has(file)) continue
|
||||
const content = readFileSync(join(rootDir, file), 'utf8')
|
||||
for (const token of FORBIDDEN_COUPLING_TOKENS) {
|
||||
if (content.includes(token)) {
|
||||
violations.push(`${file} 引用了客户端耦合 token "${token}",超出后台工程边界`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import {
|
||||
ADMIN_SOURCE_DIRS,
|
||||
collectBoundaryViolations,
|
||||
FORBIDDEN_COUPLING_TOKENS,
|
||||
readWorkspacePackage,
|
||||
REQUIRED_MANIFEST_FILES,
|
||||
resolveWorkspaceRoot,
|
||||
STANDALONE_PACKAGE_NAME,
|
||||
walk,
|
||||
} from '../src/config/workspace.ts'
|
||||
|
||||
function tempRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), 'admin-workspace-'))
|
||||
}
|
||||
|
||||
test('test_task_001_project_baseline_normal_primary_path', () => {
|
||||
// 正常主路径:独立工程从自身根目录完成一次边界校验,输出无违规。
|
||||
const root = resolveWorkspaceRoot()
|
||||
const violations = collectBoundaryViolations(root)
|
||||
assert.deepEqual(violations, [], `工作区边界违规: ${violations.join('; ')}`)
|
||||
const pkg = readWorkspacePackage(root)
|
||||
assert.equal(pkg.name, STANDALONE_PACKAGE_NAME)
|
||||
for (const dep of ['vue', 'vue-router', 'pinia', 'element-plus']) {
|
||||
assert.ok(pkg.dependencies[dep], `缺少依赖 ${dep}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_normal_variant_input', () => {
|
||||
// 正常变体:构建、开发、测试三套命令形态都指向独立后台工程。
|
||||
const { scripts } = readWorkspacePackage()
|
||||
assert.match(scripts.build, /vue-tsc --noEmit/)
|
||||
assert.match(scripts.build, /vite build/)
|
||||
assert.match(scripts.dev, /vite/)
|
||||
assert.match(scripts.dev, /5174/)
|
||||
assert.match(scripts.test, /node --test/)
|
||||
assert.match(scripts.test, /tests\/\*\.test\.ts/)
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_normal_repeated_operation_is_idempotent', () => {
|
||||
// 正常重复:重复校验不改变结果、不产生不稳定状态。
|
||||
const first = collectBoundaryViolations()
|
||||
const second = collectBoundaryViolations()
|
||||
assert.deepEqual(second, first, '重复校验结果应稳定一致')
|
||||
const pkgA = readWorkspacePackage()
|
||||
const pkgB = readWorkspacePackage()
|
||||
assert.equal(pkgA.name, pkgB.name)
|
||||
assert.deepEqual(pkgA.dependencies, pkgB.dependencies)
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_boundary_empty_input', () => {
|
||||
// 边界空值:允许的源码目录集合完整存在;扫描可容忍空目录不崩溃。
|
||||
const root = resolveWorkspaceRoot()
|
||||
for (const dir of ADMIN_SOURCE_DIRS) {
|
||||
assert.equal(existsSync(join(root, dir)), true, `缺少允许源码目录 ${dir}`)
|
||||
}
|
||||
const empty = tempRoot()
|
||||
try {
|
||||
mkdirSync(join(empty, 'src'))
|
||||
assert.deepEqual(walk(empty), [], '空目录扫描应返回空结果且不抛异常')
|
||||
} finally {
|
||||
rmSync(empty, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_boundary_single_item', () => {
|
||||
// 边界单元素:单一 SPA 入口与单一挂载点。
|
||||
const root = resolveWorkspaceRoot()
|
||||
const html = readFileSync(join(root, 'index.html'), 'utf8')
|
||||
assert.match(html, /id="app"/)
|
||||
assert.match(html, /\/src\/main\.ts/)
|
||||
const main = readFileSync(join(root, 'src/main.ts'), 'utf8')
|
||||
assert.match(main, /\.mount\('#app'\)/)
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:严格类型与 ESM 模块边界等关键字段必须存在。
|
||||
const root = resolveWorkspaceRoot()
|
||||
const tsconfig = readFileSync(join(root, 'tsconfig.json'), 'utf8')
|
||||
assert.match(tsconfig, /"strict":\s*true/)
|
||||
assert.match(tsconfig, /"moduleResolution":\s*"Bundler"/)
|
||||
const pkg = readWorkspacePackage(root)
|
||||
assert.equal(pkg.private, true)
|
||||
assert.ok(pkg.scripts.test, '缺少 test 命令字段')
|
||||
assert.ok(REQUIRED_MANIFEST_FILES.length >= 5, '清单字段不能为空')
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_invalid_input_rejected', () => {
|
||||
// 异常输入:任何文件引用客户端耦合 token 都应被边界校验拒绝。
|
||||
const root = tempRoot()
|
||||
try {
|
||||
for (const token of FORBIDDEN_COUPLING_TOKENS) {
|
||||
mkdirSync(join(root, 'src'), { recursive: true })
|
||||
writeFileSync(join(root, 'src', 'bad.ts'), `// 越界引用\nconst t = '${token}'\n`)
|
||||
const violations = collectBoundaryViolations(root)
|
||||
assert.ok(
|
||||
violations.some((v) => v.includes(`引用了客户端耦合 token "${token}"`)),
|
||||
`token ${token} 应被识别为越界: ${violations.join('; ')}`,
|
||||
)
|
||||
rmSync(join(root, 'src', 'bad.ts'))
|
||||
}
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('test_task_001_project_baseline_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败:根目录定位失败与清单缺失时返回可操作错误。
|
||||
const root = tempRoot()
|
||||
try {
|
||||
const missing = join(root, 'no-package')
|
||||
mkdirSync(missing)
|
||||
assert.throws(
|
||||
() => resolveWorkspaceRoot(missing),
|
||||
(error: Error) => error.message.includes('package.json'),
|
||||
)
|
||||
const emptyDir = join(root, 'flat')
|
||||
mkdirSync(emptyDir)
|
||||
writeFileSync(join(emptyDir, 'package.json'), '{}')
|
||||
const violations = collectBoundaryViolations(emptyDir)
|
||||
assert.ok(
|
||||
violations.some((v) => v.includes('缺少必需清单文件 src/main.ts')),
|
||||
`依赖缺失应返回可操作清单消息: ${violations.join('; ')}`,
|
||||
)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user