UNPKG

antd-tools

Version:
300 lines (248 loc) 9.16 kB
/* eslint-disable no-multi-assign */ const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); const glob = require('glob'); const argv = require('minimist')(process.argv.slice(2)); const babel = require('@babel/core'); const ProgressBar = require('progress'); const getBabelCommonConfig = require('../getBabelCommonConfig'); const { getProjectPath } = require('../utils/projectHelper'); const SKIP_REGEX = /deps-lint-skip:\s*([\w\d-_,\s]*)/; // ============================== Initial Config file ============================== let configFile; try { configFile = require(getProjectPath('.depslintrc.js')); } catch (configErr) { // Do nothing if config file failed } configFile = configFile || {}; // ================================= Process Logic ================================= function debug(...args) { if (!argv.debug) return; console.log(...args); } function getComponentRootPath(filePath) { const match = filePath.match(/^(.*\/components\/[^/]*)/); return match && match[1]; } const babelConfig = getBabelCommonConfig(); babelConfig.ast = true; function getModuleByAstNode(node) { if (!node) return null; const args = node.arguments; if (!args || !args.length) return null; if (node.callee && node.callee.name === 'require') { return args[0].value; } return getModuleByAstNode(args[0]); } function getModuleEntity(filePath, module, globConfig) { const isRelative = /^..\//.test(module); let shouldLint = false; const absolutePath = path.resolve(path.dirname(filePath), module); if (module && isRelative) { const checkedPath = glob.sync(`${absolutePath}?(.*)`, globConfig); if (checkedPath.length) { shouldLint = true; } } return { name: module, path: absolutePath, shouldLint, isRelative, }; } function getModules(filePath, globConfig, debugAST) { const moduleList = []; const code = fs.readFileSync(filePath, 'utf8'); const fileBabelConfig = { ...babelConfig, filename: filePath }; const { ast } = babel.transformSync(code, fileBabelConfig); const astBody = ast.program.body; const astComments = ast.comments; // Support skip module check const ignoreList = (astComments || []) .reduce((list, { value }) => { const match = value.match(SKIP_REGEX); if (match) { return [...list, ...match[1].split(',').map(str => str.trim())]; } return list; }, []) .filter(ignore => ignore); if (debugAST) { debug('AST:', JSON.stringify(ast, null, 2)); debug('IgnoreList:', ignoreList); } // Since import is always on the top in antd, // we can check variable declaration directly! if (Array.isArray(astBody)) { astBody.forEach(node => { let module; // Only handle declaration & directly statement if (node.type === 'ExpressionStatement') { module = getModuleByAstNode(node.expression); } else if (node.type === 'VariableDeclaration') { // Get declaration const declaration = (node.declarations || [])[0]; if (!declaration || !declaration.init || !declaration.init.arguments) return; const arg = declaration.init; module = getModuleByAstNode(arg) || ''; } // Skip if is just declare variable if (!module) return; moduleList.push(getModuleEntity(filePath, module, globConfig)); }); } // If `modulePattern` provided, we will also consider it if (configFile.modulePattern) { configFile.modulePattern.forEach(({ pattern, module }) => { if (pattern.test(code)) { moduleList.push({ ...getModuleEntity(filePath, module, globConfig), fromPattern: true, }); } }); } return { moduleList, ignoreList }; } // ================================ Export Processor =============================== module.exports = function(done) { let returnCode; try { const componentPathList = getProjectPath('components/**/*.ts*'); const globConfig = {}; // Set ignore files if (configFile.ignore) { globConfig.ignore = configFile.ignore; } else if (argv.ignore) { globConfig.ignore = argv.ignore.split(','); } if (argv.id) { console.log('Only file id:', argv.id); } if (argv.file) { console.log('Only file includes:', argv.file); } const tsFiles = glob .sync(componentPathList, globConfig) .filter(tsPath => !/components\/_/.test(tsPath) && !/~/.test(tsPath)); // User experience const useTerminal = process.stdout.isTTY && !process.env.CI; debug('Use terminal:', String(useTerminal)); let progressBar; if (useTerminal) { progressBar = !argv.debug && new ProgressBar('Processing [:bar] :percent (:current/:total)', { complete: '=', incomplete: ' ', width: 20, total: tsFiles.length, }); } // Statistic components used modules const componentModules = {}; // Statistic component file related id const componentIds = {}; tsFiles.forEach((filePath, index) => { if (progressBar) progressBar.tick(); // Skip if id not match if (argv.id && argv.id !== index) return; // Skip if file not match if (argv.file && !filePath.toUpperCase().includes(String(argv.file).toUpperCase())) { return; } const id = String(index).padStart(3, '0'); debug(chalk.cyan(`[${id}]`), chalk.blue('Transform:'), filePath); const { moduleList } = getModules(filePath, globConfig); const relativeDepsModuleList = moduleList.filter(module => { debug( chalk.blue(' > Module:'), module.shouldLint ? chalk.green('[KEEP]') : chalk.yellow('[SKIP]'), module.name, module.isRelative ? chalk.cyan('[RELATIVE]') : '', module.fromPattern ? chalk.cyan('[PATTERN]') : '' ); return module.shouldLint; }); // Process modules const componentRoot = getComponentRootPath(filePath); if (!componentRoot) { console.warn('File path not match:', filePath); return; } const componentModuleList = (componentModules[componentRoot] = componentModules[componentRoot] || []); const componentIdList = (componentIds[componentRoot] = componentIds[componentRoot] || []); relativeDepsModuleList.forEach(module => { if (componentModuleList.some(m => m.name === module.name)) return; componentModuleList.push(module); }); componentIdList.push(id); }); // Loop component check Object.keys(componentModules).forEach(componentPath => { const componentModuleList = componentModules[componentPath]; const moduleRootStyleList = componentModuleList.map(module => path.resolve(getComponentRootPath(module.path), 'style') ); const componentRootStylePath = path.resolve(componentPath, 'style/index.tsx'); if (!fs.existsSync(componentRootStylePath)) { debug('Style file path not exist:', componentRootStylePath); return; } const { moduleList: rootStyleModuleList, ignoreList: rootStyleModuleIgnoreList } = getModules( componentRootStylePath, {} ); // Check missed files const missStyleModuleList = moduleRootStyleList.filter( styleModulePath => rootStyleModuleList.every(module => !module.path.includes(styleModulePath)) && rootStyleModuleIgnoreList.every(ignoreWord => !styleModulePath.includes(ignoreWord)) ); // Check useless files const uselessStyleModuleList = rootStyleModuleList // filter self styles .filter(({ name }) => name.slice(0, 2) !== './' && name !== '../../style/index.less') // filter ignore list .filter(({ path: stylePath }) => rootStyleModuleIgnoreList.every(ignoreWord => !stylePath.includes(ignoreWord)) ) // filter used styles .filter(({ path: stylePath }) => { return !moduleRootStyleList.includes(stylePath); }); // Log error message if (missStyleModuleList.length || uselessStyleModuleList.length) { returnCode = 1; console.log(chalk.red('Dependency style file(s) not included in:', componentRootStylePath)); missStyleModuleList.forEach(styleModulePath => { console.log(chalk.yellow(' - [Missing]'), styleModulePath); }); uselessStyleModuleList.forEach(({ path: uselessPath }) => { console.log(chalk.yellow(' - [Useless]'), uselessPath); }); console.log(`(debug id: ${componentIds[componentPath].join(', ')})`); } else { console.log(chalk.green('Check pass:'), componentPath); } }); } catch (err) { returnCode = 1; console.log(err); } if (!returnCode) { console.log(chalk.green('✅ Congratulations! All style dependencies included!')); } else { console.log( chalk.cyan( 'Style dependencies check failed. You can use `--debug` with `--id=xxx` or `--file=xxx` to check detail.' ) ); } done(returnCode); };