@lorcan-store/vue-auto-router
Version:
A Vite plugin for auto-generating Vue router configuration with smart naming conventions and custom templates
448 lines (445 loc) • 17.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouteGenerator = void 0;
const glob_1 = require("glob");
const path_1 = __importDefault(require("path"));
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const fs_2 = require("fs");
/**
* 路由生成器类
*/
class RouteGenerator {
constructor(options) {
this.routeTemplateFunction = null;
// 消息模板
this.messages = {
EN: {
dirCreated: '✨ Directories created/verified successfully',
dirCreateError: 'Failed to create directories:',
noFiles: '⚠️ No .vue files found in the specified directory',
noFilesInDir: (dir) => `⚠️ No .vue files found in directory: ${dir}`,
noIndex: (dir) => `⚠️ No index.vue found in directory: ${dir}\n` +
`A default route with empty path will not be generated.`,
fileExists: (fileName, dirName) => `⚠️ Route file already exists: ${fileName}\n` +
`To update the route configuration:\n` +
`1. Add "${dirName}" to forceOverwrite option, or\n` +
`2. Manually delete the file and restart the server.`,
overwriting: (fileName) => `🔄 Overwriting route file: ${fileName} (forceOverwrite enabled)`,
generated: (path) => `✨ Generated route file: ${path}`,
writeError: '❌ Failed to write route file:',
templateNotFound: (path) => `⚠️ Route template file not found: ${path}`,
templateNotFunction: '⚠️ Route template must export a function',
templateLoaded: '✨ Route template loaded successfully',
templateLoadError: '❌ Failed to load route template:',
templateError: '❌ Failed to generate content using template:',
fallingBack: '↪️ Falling back to default template'
},
CN: {
dirCreated: '✨ 目录创建/验证成功',
dirCreateError: '创建目录失败:',
noFiles: '⚠️ 在指定目录中未找到 .vue 文件',
noFilesInDir: (dir) => `⚠️ 在目录 ${dir} 中未找到 .vue 文件`,
noIndex: (dir) => `⚠️ 在目录 ${dir} 中未找到 index.vue\n` +
`将不会生成默认的空路径路由。`,
fileExists: (fileName, dirName) => `⚠️ 路由文件已存在:${fileName}\n` +
`要更新路由配置,请:\n` +
`1. 将 "${dirName}" 添加到 forceOverwrite 选项中,或\n` +
`2. 手动删除文件并重启服务器。`,
overwriting: (fileName) => `🔄 正在覆盖路由文件:${fileName}(已启用强制覆盖)`,
generated: (path) => `✨ 已生成路由文件:${path}`,
writeError: '❌ 写入路由文件失败:',
templateNotFound: (path) => `⚠️ 未找到路由模板文件:${path}`,
templateNotFunction: '⚠️ 路由模板必须导出一个函数',
templateLoaded: '✨ 路由模板加载成功',
templateLoadError: '❌ 加载路由模板失败:',
templateError: '❌ 使用模板生成内容失败:',
fallingBack: '↪️ 回退到默认模板'
}
};
this.options = {
scanDir: this.normalizePath(options.scanDir || 'src/pages'),
outputDir: this.normalizePath(options.outputDir || 'src/router/'),
exclude: options.exclude || [],
layoutPath: options.layoutPath || '@/pages/layout/index.vue',
forceOverwrite: options.forceOverwrite || [],
routeTemplate: options.routeTemplate,
language: options.language || 'EN' // 添加语言配置
};
this.ensureDirectoriesExist();
this.loadRouteTemplate();
}
// 获取当前语言的消息
get msg() {
return this.messages[this.options.language];
}
/**
* 标准化路径
* @param path 路径
* @returns 标准化后的路径
*/
normalizePath(path) {
if (!path)
return '';
// 将反斜杠转换为正斜杠
const normalizedPath = path.replace(/\\/g, '/');
// 移除末尾斜杠(除了 outputDir)
if (normalizedPath === this.options?.outputDir) {
return normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`;
}
return normalizedPath.endsWith('/') ? normalizedPath.slice(0, -1) : normalizedPath;
}
/**
* 确保必要的目录存在
*/
async ensureDirectoriesExist() {
try {
// 创建扫描目录
const scanDirPath = this.options.scanDir.startsWith('@')
? path_1.default.join(process.cwd(), 'src', this.options.scanDir.slice(2))
: path_1.default.join(process.cwd(), this.options.scanDir);
// 创建输出目录
const outputDirPath = this.options.outputDir.startsWith('@')
? path_1.default.join(process.cwd(), 'src', this.options.outputDir.slice(2))
: path_1.default.join(process.cwd(), this.options.outputDir);
await (0, promises_1.mkdir)(scanDirPath, { recursive: true });
await (0, promises_1.mkdir)(outputDirPath, { recursive: true });
console.log(this.msg.dirCreated);
}
catch (error) {
console.error(this.msg.dirCreateError, error);
}
}
/**
* 生成路由配置
*/
async generate() {
try {
const files = await this.scanFiles();
if (!files.length) {
console.warn(this.msg.noFiles);
return;
}
// 按目录分组处理文件
const fileGroups = this.groupFilesByDirectory(files);
// 为每个目录生成单独的路由文件
for (const [dir, groupFiles] of Object.entries(fileGroups)) {
// 检查目录是否有文件
if (!groupFiles.length) {
console.warn(this.msg.noFilesInDir(dir));
continue;
}
// 检查是否有 index.vue 文件
const hasIndex = groupFiles.some(file => path_1.default.basename(file, '.vue') === 'index');
if (!hasIndex) {
console.warn(this.msg.noIndex(dir));
}
const routes = [{
path: this.getRoutePath(dir),
component: 'Layout',
name: this.getRouteBaseName(dir),
children: groupFiles.map(file => ({
path: this.getSubRoutePath(file),
component: this.getComponentName(file),
name: this.getRouteName(dir, file)
}))
}];
const imports = this.generateImports(groupFiles);
// 检查是否在强制覆盖列表中
const isForceOverwrite = this.options.forceOverwrite.includes(dir);
await this.writeRouteFile(routes, imports, dir, isForceOverwrite);
}
}
catch (error) {
console.error('❌ Failed to generate routes:', error);
throw error;
}
}
/**
* 扫描文件
* @returns 匹配的文件路径数组
*/
async scanFiles() {
try {
// 构建排除模式
const excludePatterns = [
'**/node_modules/**',
...this.options.exclude.map(pattern => pattern.includes('/') ? pattern : `${this.options.scanDir}${pattern}/**`)
];
// 首先获取所有一级目录
const scanDirPath = this.options.scanDir.startsWith('@')
? path_1.default.join(process.cwd(), 'src', this.options.scanDir.slice(2))
: path_1.default.join(process.cwd(), this.options.scanDir);
const dirents = await fs_1.promises.readdir(scanDirPath, { withFileTypes: true });
const directories = dirents
.filter(dirent => dirent.isDirectory())
.map(dirent => path_1.default.join(scanDirPath, dirent.name));
// 然后获取每个目录下的 .vue 文件
const allFiles = [];
for (const dir of directories) {
const dirName = path_1.default.basename(dir);
// 跳过被排除的目录
if (this.options.exclude.includes(dirName)) {
continue;
}
// 只扫描一级 .vue 文件
const files = await (0, glob_1.glob)(`${dir}/*.vue`, {
absolute: true,
nodir: true,
ignore: excludePatterns
});
allFiles.push(...files);
}
return allFiles;
}
catch (error) {
console.error('Failed to scan files:', error);
return [];
}
}
/**
* 生成导入声明
* @param files 文件路径数组
* @returns 导入声明数组
*/
generateImports(files) {
return files.map(file => ({
name: this.getComponentName(file),
path: this.getImportPath(file)
}));
}
/**
* 生成路由配置
* @param files 文件路径数组
* @returns 路由节点数组
*/
generateRoutes(files) {
const fileGroups = this.groupFilesByDirectory(files);
return Object.entries(fileGroups).map(([dir, files]) => ({
path: this.getRoutePath(dir),
component: 'Layout',
name: this.getRouteBaseName(dir),
children: files.map(file => ({
path: this.getSubRoutePath(file),
component: this.getComponentName(file),
name: this.getRouteName(dir, file)
}))
}));
}
/**
* 写入路由文件
* @param routes 路由配置
* @param imports 导入声明
* @param dirName 目录名
* @param forceOverwrite 是否强制覆盖
*/
async writeRouteFile(routes, imports, dirName, forceOverwrite = false) {
const content = this.generateRouteFileContent(routes, imports);
try {
await (0, promises_1.mkdir)(this.options.outputDir, { recursive: true });
const fileName = `${dirName}.ts`;
const outputPath = path_1.default.join(this.options.outputDir, fileName);
// 检查文件是否已存在
if (await this.fileExists(outputPath)) {
if (!forceOverwrite) {
console.warn(this.msg.fileExists(fileName, dirName));
return;
}
console.log(this.msg.overwriting(fileName));
}
await fs_1.promises.writeFile(outputPath, content, 'utf-8');
console.log(this.msg.generated(outputPath));
}
catch (error) {
console.error(this.msg.writeError, error);
throw error;
}
}
/**
* 检查文件否存在
* @param filePath 文件路径
* @returns 是否存在
*/
async fileExists(filePath) {
try {
await fs_1.promises.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* 获取组件名称
* @param file 文件路径
* @returns 组件名称
*/
getComponentName(file) {
const basename = path_1.default.basename(file, '.vue');
// 处理文件名中的特殊字符(横杠、下划线等)
return this.toPascalCase(basename);
}
/**
* 获取导入路径
* @param file 文件路径
* @returns 导入路径
*/
getImportPath(file) {
const normalizedPath = file.replace(/\\/g, '/');
const match = normalizedPath.match(/src\/(.*)/);
if (!match)
return file;
return `@/${match[1]}`;
}
/**
* 获取路由路径
* @param dir 目录名
* @returns 路由路径
*/
getRoutePath(dir) {
if (!dir)
return '/';
const dirName = dir.split('/').pop();
if (!dirName)
return '/';
// 保持原有的分隔符(横杠或下划线)
return '/' + dirName;
}
/**
* 获取子路由路径
* @param file 文件路径
* @returns 子路由路径
*/
getSubRoutePath(file) {
const basename = path_1.default.basename(file, '.vue');
// 保持原有的分隔符(横杠或下划线)
return basename === 'index' ? '' : basename;
}
/**
* 获取路由基础名称
* @param dir 目录名
* @returns 路由基础名称
*/
getRouteBaseName(dir) {
// 处理目录名中的特殊字符
return this.toPascalCase(dir);
}
/**
* 获取路由名称
* @param dir 目录名
* @param file 文件路径
* @returns 路由名称
*/
getRouteName(dir, file) {
const dirName = dir.split('/').pop() || '';
const fileName = path_1.default.basename(file, '.vue');
const baseName = this.getRouteBaseName(dirName);
if (fileName === 'index') {
return baseName;
}
// 处理文件名中的特殊字符
const componentName = this.toPascalCase(fileName);
return baseName + componentName;
}
/**
* 按目录分组文件
* @param files 文件路径数组
* @returns 分组后的文件映射
*/
groupFilesByDirectory(files) {
return files.reduce((acc, file) => {
const normalizedPath = file.replace(/\\/g, '/');
const dir = path_1.default.basename(path_1.default.dirname(normalizedPath));
if (!acc[dir])
acc[dir] = [];
acc[dir].push(file);
return acc;
}, {});
}
/**
* 加载路由模板函数
*/
async loadRouteTemplate() {
if (!this.options.routeTemplate)
return;
try {
const templatePath = path_1.default.resolve(process.cwd(), this.options.routeTemplate);
if (!(0, fs_2.existsSync)(templatePath)) {
console.warn(this.msg.templateNotFound(templatePath));
return;
}
const template = require(templatePath);
if (typeof template !== 'function') {
console.warn(this.msg.templateNotFunction);
return;
}
this.routeTemplateFunction = template;
console.log(this.msg.templateLoaded);
}
catch (error) {
console.error(this.msg.templateLoadError, error);
}
}
/**
* 生成路由文件内容
*/
generateRouteFileContent(routes, imports) {
// 如果有自定义模板函数,使用它
if (this.routeTemplateFunction) {
try {
// 获取目录名和处理后的文件路径
const files = imports.map(imp => imp.path);
const dirName = path_1.default.basename(path_1.default.dirname(files[0]));
// 为模板函数提供更多上下文信息
const context = {
dirName,
files,
imports,
routes,
options: {
layoutPath: this.options.layoutPath
}
};
// 调用模板函数
const content = this.routeTemplateFunction(dirName, files, context);
if (typeof content !== 'string') {
throw new Error('Template function must return a string');
}
return content;
}
catch (error) {
console.error(this.msg.templateError, error);
console.log(this.msg.fallingBack);
}
}
// 使用默认模板
const routeStr = JSON.stringify(routes, null, 2)
.replace(/"component": "([^"]+)"/g, 'component: $1')
.replace(/"children": /g, 'children: ')
.replace(/"path": /g, 'path: ')
.replace(/"name": /g, 'name: ');
return `
import type { RouteRecordRaw } from 'vue-router'
import Layout from '${this.options.layoutPath}'
${imports.map(imp => `const ${imp.name} = () => import('${imp.path}')`).join('\n')}
const Router: Array<RouteRecordRaw> = ${routeStr}
export default Router
`;
}
/**
* 转换为帕斯卡命名法(PascalCase)
* @param str 输入字符串
* @returns 转换后的字符串
*/
toPascalCase(str) {
return str
.split(/[-_]/)
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join('');
}
}
exports.RouteGenerator = RouteGenerator;
//# sourceMappingURL=generator.js.map