import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const scanDist = process.argv.includes('--dist'); const forbiddenPaths = [ 'src/pages/dashboard', 'src/pages/detail', 'src/pages/form', 'src/pages/list', 'src/pages/user', 'src/components/common-table', 'src/components/product-card', 'src/components/trend', 'src/api/finance.ts', 'src/api/list.ts', 'src/api/detail.ts', 'src/api/permission.ts', 'src/api/model/listModel.ts', 'src/api/model/detailModel.ts', 'src/api/model/permissionModel.ts', 'src/utils/route/index.ts', 'src/assets/assets-empty.svg', 'src/assets/assets-logo-full.svg', 'src/assets/assets-t-logo.svg', 'src/assets/assets-tencent-logo.png', 'src/assets/assets-product-1.svg', 'src/assets/assets-product-2.svg', 'src/assets/assets-product-3.svg', 'src/assets/assets-product-4.svg', 'src/assets/assets-setting-auto.svg', 'src/assets/assets-setting-dark.svg', 'src/assets/assets-setting-light.svg', ]; const sourceFragments = [ "import.meta.glob('../../pages/**/*.vue')", '/dashboard/base', '/detail/base', '/list/base', 'demo-select-base', 'assets-tencent-logo', 'assets-product-', 'TDesign Vue Next Starter', 'tdesign.gtimg.com/starter/brand-logo', ]; const distFragments = [ 'dashboardBase', 'dashboardDetail', 'detailBase', 'formBase', 'listBase', 'demo-select-base', 'assets-tencent-logo', 'TDesign Vue Next Starter', 'tdesign.gtimg.com/starter/brand-logo', ]; async function exists(relativePath) { try { await stat(path.join(root, relativePath)); return true; } catch (error) { if (error?.code === 'ENOENT') return false; throw error; } } async function filesUnder(relativePath) { const absolutePath = path.join(root, relativePath); if (!(await exists(relativePath))) return []; const entries = await readdir(absolutePath, { withFileTypes: true }); const files = []; for (const entry of entries) { const child = path.join(relativePath, entry.name); if (entry.isDirectory()) files.push(...(await filesUnder(child))); else if (entry.isFile()) files.push(child); } return files; } async function findFragments(relativeFiles, fragments) { const violations = []; for (const relativeFile of relativeFiles) { const content = (await readFile(path.join(root, relativeFile))).toString('utf8'); for (const fragment of fragments) { if (content.includes(fragment)) violations.push(`${relativeFile}: ${fragment}`); } } return violations; } const violations = []; for (const relativePath of forbiddenPaths) { if (await exists(relativePath)) violations.push(`${relativePath}: forbidden production path`); } const mockFiles = await filesUnder('mock'); for (const file of mockFiles) violations.push(`${file}: Mock fixture is forbidden in production frontend`); const sourceFiles = (await filesUnder('src')).filter((file) => /\.(?:css|html|js|json|jsx|less|ts|tsx|vue)$/.test(file), ); violations.push(...(await findFragments(sourceFiles, sourceFragments))); let distFiles = []; if (scanDist) { if (!(await exists('dist'))) { violations.push('dist: production build output is missing'); } else { distFiles = await filesUnder('dist'); for (const file of distFiles) { if (/assets-(?:empty|logo-full|product-|setting-|t-logo|tencent-logo)/.test(path.basename(file))) { violations.push(`${file}: forbidden Starter asset`); } } violations.push(...(await findFragments(distFiles, distFragments))); } } if (violations.length > 0) { console.error(`Production hygiene check failed with ${violations.length} violation(s):`); for (const violation of violations) console.error(`- ${violation}`); process.exit(1); } console.log( `Production hygiene passed: ${sourceFiles.length} source files${scanDist ? ` and ${distFiles.length} dist files` : ''} scanned.`, );