255 lines
7.5 KiB
TypeScript
255 lines
7.5 KiB
TypeScript
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;
|
||
}
|
||
|
||
async function genIndexFile(config: Config) {
|
||
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");
|
||
|
||
// 写入之前,确保输出目录存在,如果不存在就递归创建
|
||
if (!fs.existsSync(config.outputDir)) {
|
||
fs.mkdirSync(config.outputDir, { recursive: true });
|
||
}
|
||
|
||
// 将内容写入到最终的 index.ts 文件中
|
||
const outputFilePath = path.join(config.outputDir, config.outputFile);
|
||
fs.writeFileSync(outputFilePath, fileContent, "utf-8");
|
||
|
||
console.log(`✨ 成功生成入口文件: ${outputFilePath}`);
|
||
}
|
||
|
||
// ================= 脚本执行入口 =================
|
||
try {
|
||
console.log("🚀 开始扫描并生成入口文件...");
|
||
await genIndexFile(cssConfig);
|
||
await genIndexFile(tsConfig);
|
||
console.log("✅ 脚本执行完毕!");
|
||
} catch (error) {
|
||
console.error("❌ 脚本执行失败:", error);
|
||
process.exit(1); // 如果报错,让进程以非 0 状态码退出
|
||
}
|