mm
This commit is contained in:
@@ -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. 移除了 windowsPathsNoEscape,tinyglobby 默认处理路径更智能
|
||||
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 状态码退出
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user