import * as fs from "fs"; import * as path from "path"; /** * 默认测试 / story / example / fixture / type 文件匹配规则 * 仅用于“识别”,不直接决定是否排除 */ const DEFAULT_TEST_FILE_PATTERNS: RegExp[] = [ // ========================= // Test / Spec // ========================= /\.(test|spec)\.(ts|tsx|js|jsx)$/, /\.(test|spec)\.(ts|tsx|js|jsx)\?.*$/, // query param safe // ========================= // E2E / Playwright / Cypress // ========================= /\.(cy|playwright|e2e)\.(ts|tsx|js|jsx)$/, // ========================= // Storybook // ========================= /\.(story|stories)\.(ts|tsx|js|jsx|mdx)$/, // ========================= // Snapshot / Mock / Fixture // ========================= /\.snap$/, /\.mock\.(ts|tsx|js|jsx)$/, /\.fixture\.(ts|tsx|js|jsx)$/, // ========================= // Type declarations // ========================= /\.d\.ts$/, // ========================= // Examples / Demos / Docs // ========================= /\.(example|demo|docs)\.(ts|tsx|js|jsx)$/, ]; /** * 测试 / 辅助目录(目录级排除) */ const DEFAULT_TEST_DIR_PATTERNS: RegExp[] = [ /[/\\](__tests__|tests|test|spec|__mocks__|__fixtures__|__snapshots__)[\/\\]/, /[/\\](cypress|playwright|e2e|__e2e__|__playwright__)[\/\\]/, /[/\\](story|stories|\.storybook)[\/\\]/, /[/\\](types|type|typings|interfaces)[\/\\]/, /[/\\](examples|example|demo|demos|docs)[\/\\]/, ]; type Config = { outputDir: string; outputFile: string; scanDirs: string[]; preamble?: string[]; importPrefix?: string; entryFilePatterns?: RegExp[]; barrelFirstMode?: boolean; includeFilePatterns?: RegExp[]; excludeFilePatterns?: RegExp[]; excludeDirPatterns?: RegExp[]; }; const tsConfig: Config = { outputDir: "src", outputFile: "index.ts", scanDirs: ["src"], importPrefix: "export * from", barrelFirstMode: true, preamble: ["import './index.css';"], entryFilePatterns: [/^index\.(js|ts|jsx|tsx)$/], includeFilePatterns: [/\.(js|ts|jsx|tsx)$/], excludeFilePatterns: DEFAULT_TEST_FILE_PATTERNS, excludeDirPatterns: DEFAULT_TEST_DIR_PATTERNS, }; const cssConfig: Config = { outputDir: "src", outputFile: "index.css", scanDirs: ["src"], importPrefix: "@import", barrelFirstMode: false, entryFilePatterns: [/^index\.(css)$/], includeFilePatterns: [/\.(css)$/], excludeFilePatterns: DEFAULT_TEST_FILE_PATTERNS, excludeDirPatterns: DEFAULT_TEST_DIR_PATTERNS, }; function isEntryFile(fileName: string, config: Config): boolean { const regExps = config.entryFilePatterns; if (!regExps || regExps.length === 0) return false; return regExps.some((regExp) => regExp.test(fileName)); } function isIncludeFile(fileName: string, config: Config): boolean { const regExps = config.includeFilePatterns; if (!regExps || regExps.length === 0) return false; return regExps.some((regExp) => regExp.test(fileName)); } function isExcludeFile(fileName: string, config: Config): boolean { const regExps = config.excludeFilePatterns; if (!regExps || regExps.length === 0) return false; return regExps.some((regExp) => regExp.test(fileName)); } function isExcludeDir(filePath: string, config: Config): boolean { const regExps = config.excludeDirPatterns; if (!regExps || regExps.length === 0) return false; return regExps.some((regExp) => regExp.test(filePath)); } function isValidDir(filePath: string, config: Config): boolean { if (isExcludeDir(filePath, config)) { return false; } return true; } function isValidFile(fileName: string, config: Config): boolean { if (isIncludeFile(fileName, config) && !isExcludeFile(fileName, config)) { return true; } return false; } const buildExportStatement = (filePath: string, config: Config) => { // 文件夹(不是文件)与目标文件的相对路径 let exportFilePath = path.relative(config.outputDir, filePath); // 去除扩展名,并且转化成正斜杠 const exportFilePathWithoutExt = path .join( path.dirname(exportFilePath), path.basename(exportFilePath, path.extname(exportFilePath)), ) .replace(/\\/g, "/"); // 加上 ./ 前缀 return `${config.importPrefix} './${exportFilePathWithoutExt}';`; }; function isOutputEntry(filePath: string, config: Config) { const outputFilePath = path.resolve(config.outputDir, config.outputFile); const targetFilePath = path.resolve(filePath); return outputFilePath === targetFilePath; } function generateExports(dirPath: string, config: Config): string[] { const fileNames = fs.readdirSync(dirPath); const exports: string[] = []; // ========================= // 逻辑分支:Barrel First Mode // ========================= if (config.barrelFirstMode) { // 查找当前目录有没有 index.ts const entryFileName = fileNames.find((fileName) => { const filePath = path.join(dirPath, fileName); return fs.statSync(filePath).isFile() && isEntryFile(fileName, config); }); // 如果有 if (entryFileName) { const entryFilePath = path.join(dirPath, entryFileName); // 如果是 outputFile 自身,则跳过 if (isOutputEntry(entryFilePath, config)) { console.log(`⏭️ 跳过自身入口文件: ${entryFilePath}`); } else { exports.push(buildExportStatement(entryFilePath, config)); } } } // ========================= // 逻辑分支:index.ts 不存在,则继续正常遍历与递归 // ========================= fileNames.forEach((fileName) => { const filePath = path.join(dirPath, fileName); const stat = fs.statSync(filePath); // 情况1:是文件,且通过了校验 if ( stat.isFile() && isValidFile(fileName, config) && !isEntryFile(fileName, config) ) { exports.push(buildExportStatement(filePath, config)); } // 情况2:是文件夹,且通过了校验,递归扫描子文件夹 else if (stat.isDirectory() && isValidDir(filePath, config)) { const subExports = generateExports(filePath, config); exports.push(...subExports); } }); return exports; } function genIndexFile(config: Config) { // 确保输出目录存在,如果不存在就递归创建 if (!fs.existsSync(config.outputDir)) { fs.mkdirSync(config.outputDir, { recursive: true }); } const allExports: string[] = []; // 遍历所有需要扫描的根目录 config.scanDirs.forEach((scanDir) => { // 只有当目录真实存在时才进行扫描 if (fs.existsSync(scanDir)) { const exports = generateExports(scanDir, config); allExports.push(...exports); } else { console.warn(`⚠️ 警告:扫描目录不存在,已跳过 -> ${scanDir}`); } }); // 拼接最终的文件内容:前言 + 导出语句(使用 Set 自动去重) const fileContent = [ ...(config.preamble ?? []), ...Array.from(new Set(allExports)), ].join("\n"); // 将内容写入到最终的 index.ts 文件中 const outputFilePath = path.join(config.outputDir, config.outputFile); fs.writeFileSync(outputFilePath, fileContent, "utf-8"); console.log(`✨ 成功生成入口文件: ${outputFilePath}`); } // ================= 脚本执行入口 ================= try { console.log("🚀 开始扫描并生成入口文件..."); genIndexFile(tsConfig); genIndexFile(cssConfig); console.log("✅ 脚本执行完毕!"); } catch (error) { console.error("❌ 脚本执行失败:", error); process.exit(1); // 如果报错,让进程以非 0 状态码退出 }