This commit is contained in:
2026-06-08 04:19:23 +08:00
parent 8351498071
commit f2d8ad0de2
152 changed files with 2362 additions and 2227 deletions

View File

@@ -1,147 +1,254 @@
import fs from "fs";
import path from "path";
// 1. 引入 tinyglobby
import { globSync } from "tinyglobby";
import * as fs from "fs";
import * as path from "path";
interface Config {
/**
* 默认测试 / 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;
outputFilenameWithExt: string;
outputFile: string;
scanDirs: string[];
importPrefix: string;
predefineStatements: string[];
includeExtensions: string[];
excludeDirs: string[];
excludeFileExtensions: string[];
excludePatterns: RegExp[];
}
const cssConfig: Config = {
outputDir: "src",
outputFilenameWithExt: "index.css",
scanDirs: ["src"],
importPrefix: "@import",
predefineStatements: [],
includeExtensions: [".css"],
excludeDirs: ["__tests__", "tests", "story", "stories", "types"],
excludeFileExtensions: [],
excludePatterns: [/^index\.(css)$/, /\.(test|spec)\./, /\.(story|stories)\./],
preamble?: string[];
importPrefix?: string;
entryFilePatterns?: RegExp[];
barrelFirstMode?: boolean;
includeFilePatterns?: RegExp[];
excludeFilePatterns?: RegExp[];
excludeDirPatterns?: RegExp[];
};
const tsConfig: Config = {
outputDir: "src",
outputFilenameWithExt: "index.ts",
outputFile: "index.ts",
scanDirs: ["src"],
importPrefix: "export * from",
predefineStatements: ["import './index.css'"],
includeExtensions: [".ts", "tsx", "js", "jsx"],
excludeDirs: ["__tests__", "tests", "story", "stories", "types"],
excludeFileExtensions: [".d.ts"],
excludePatterns: [
/^index\.(ts|tsx|js|jsx)$/,
/\.(test|spec)\./,
/\.(story|stories)\./,
],
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 normalizePath = (p: string) => p.replace(/\\/g, "/");
const isExcludeDir = (filePath: string, excludeDirs: string[]) => {
const normalized = normalizePath(filePath);
return excludeDirs.some((dir) => normalized.includes(`/${dir}/`));
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,
};
const isExcludeFileExtensions = (
filePath: string,
excludeFileExtensions: string[],
) => excludeFileExtensions.some((ext) => filePath.endsWith(ext));
function isEntryFile(fileName: string, config: Config): boolean {
const regExps = config.entryFilePatterns;
const isExcludePattern = (fileName: string, excludePatterns: RegExp[]) =>
excludePatterns.some((pattern) => pattern.test(fileName));
if (!regExps || regExps.length === 0) return false;
// ----------------------------------------
function isValidFile(filePath: string, config: Config): boolean {
const fileName = filePath.split(/[\\/]/).pop()!;
if (isExcludeDir(filePath, config.excludeDirs)) return false;
if (isExcludeFileExtensions(filePath, config.excludeFileExtensions))
return false;
if (isExcludePattern(fileName, config.excludePatterns)) return false;
const ext = path.extname(filePath);
return config.includeExtensions.includes(ext);
return regExps.some((regExp) => regExp.test(fileName));
}
// -----------------------------------------
function generateIndexFile(config: Config) {
const currentPath = process.cwd();
const outputPath = path.resolve(currentPath, config.outputDir);
let exportStatements: string[] = [];
// ------ scanDirs forEach start ------------------------
config.scanDirs.forEach((dir) => {
// 2. 路径模式保持不变tinyglobby 能够正确处理
const scanPattern = path.resolve(currentPath, dir, "**", "*.*");
function isIncludeFile(fileName: string, config: Config): boolean {
const regExps = config.includeFilePatterns;
const allFilePath = globSync(scanPattern, {
absolute: true,
// 3. 移除了 windowsPathsNoEscapetinyglobby 默认处理路径更智能
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);
const validFiles = allFilePath.filter((filePath) => {
return isValidFile(filePath, config);
});
if (validFiles.length === 0) {
console.log(
`⚠️ 未找到符合条件的文件,跳过生成 ${config.outputFilenameWithExt}`,
);
return;
}
validFiles.sort();
validFiles.forEach((file) => {
const relativePath = path.relative(outputPath, file);
const importPath = `./${relativePath.replace(/\\/g, "/")}`;
exportStatements.push(`${config.importPrefix} '${importPath}';`);
});
});
// --------- scanDirs forEach end ----------------
const indexFileContent = `
${config.predefineStatements.join("\n")}
${exportStatements.join("\n")}
`.trim();
const indexFilePath = path.resolve(
currentPath,
config.outputDir,
config.outputFilenameWithExt,
);
// ✅ 内容比对,避免重复写入
if (fs.existsSync(indexFilePath)) {
const old = fs.readFileSync(indexFilePath, "utf8");
if (old === indexFileContent) {
console.log(
`${config.outputFilenameWithExt} 内容无变化,无需重新生成`,
);
return;
// 如果是 outputFile 自身,则跳过
if (isOutputEntry(entryFilePath, config)) {
console.log(`⏭️ 跳过自身入口文件: ${entryFilePath}`);
} else {
exports.push(buildExportStatement(entryFilePath, config));
}
}
}
fs.writeFileSync(indexFilePath, indexFileContent, "utf8");
console.log(`✅ 成功生成 ${config.outputFilenameWithExt}: ${indexFilePath}`);
// =========================
// 逻辑分支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(`🚀 [gen-index] 开始扫描`);
generateIndexFile(cssConfig);
generateIndexFile(tsConfig);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`❌ [gen-index] 执行失败: ${msg}`);
process.exit(1);
console.log("🚀 开始扫描并生成入口文件...");
genIndexFile(tsConfig);
genIndexFile(cssConfig);
console.log("✅ 脚本执行完毕!");
} catch (error) {
console.error("❌ 脚本执行失败:", error);
process.exit(1); // 如果报错,让进程以非 0 状态码退出
}