This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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.`,
|
||||
);
|
||||
Reference in New Issue
Block a user