UNPKG

unplugin-convention-routes

Version:
547 lines (532 loc) 18.8 kB
//#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let unplugin = require("unplugin"); let node_process = require("node:process"); node_process = __toESM(node_process, 1); let _antfu_utils = require("@antfu/utils"); //#region src/core/options.ts /** * 各框架默认支持的文件扩展名 */ const DEFAULT_EXTENSIONS = { vue: [ "vue", "ts", "js" ], react: [ "tsx", "jsx", "ts", "js" ] }; /** * 解析用户配置选项 * 将用户提供的配置转换为内部使用的完整配置对象 * @param userOptions - 用户配置选项 * @returns 解析后的完整配置对象 */ function resolveOptions(userOptions) { const { resolver, dirs = "src/pages", extensions = DEFAULT_EXTENSIONS[resolver], exclude = [ "node_modules", ".git", "**/__*__/**", "**/components/**", "**/components.*" ], importPath = "relative", caseSensitive = false, routeNameSeparator = "-", extendRoute, onRoutesGenerated, onClientGenerated } = userOptions; return { root: (0, _antfu_utils.slash)(node_process.default.cwd()), resolver, dirs: (0, _antfu_utils.toArray)(typeof dirs === "string" ? { dir: dirs, baseRoute: "" } : dirs).map((dir) => { if (typeof dir === "string") return { dir: (0, _antfu_utils.slash)(dir), baseRoute: "" }; return { ...dir, dir: (0, _antfu_utils.slash)(dir.dir) }; }), extensions, exclude, importPath, caseSensitive, routeNameSeparator, extendRoute, onRoutesGenerated, onClientGenerated }; } //#endregion //#region src/core/path.ts /** * 生成 Vite 的 glob 匹配模式 * @param dir - 页面目录配置 * @param extensions - 文件扩展名列表 * @returns glob 匹配模式字符串 */ function getGlobPattern(dir, extensions) { const ext = extensions.length > 1 ? `{${extensions.join(",")}}` : extensions[0]; return `${dir.dir.startsWith("/") ? dir.dir : `/${dir.dir}`}/**/*.${ext}`; } /** * 生成 Rspack/Webpack 的 context 匹配模式 * 注意:Rspack 虚拟模块中的相对路径会有问题,所以使用绝对路径 * @param dir - 页面目录配置 * @param extensions - 文件扩展名列表 * @param root - 项目根目录 * @returns context 配置对象 */ function getWebpackContextPattern(dir, extensions, root) { const extPattern = extensions.join("|"); let dirPath = dir.dir; if (!dirPath.startsWith("/")) dirPath = `/${dirPath}`; return { path: root + dirPath, recursive: true, regExp: `\\.(${extPattern})$` }; } //#endregion //#region src/resolvers/utils.ts /** * Resolver 公共工具函数模块 * 提供 glob 模式转换、正则表达式转义等功能 */ /** * 正则表达式特殊字符匹配模式 * 移到模块作用域以避免每次调用重新编译 */ const ESCAPE_REGEXP = /[.*+?^${}()|[\]\\/:]/g; /** * glob 模式转换用的正则表达式 * 移到模块作用域以避免每次调用重新编译 */ const GLOB_DOUBLE_STAR_RE = /\*\*/g; const GLOB_SINGLE_STAR_RE = /\*/g; const GLOB_QUESTION_RE = /\?/g; /** * 正则元字符匹配模式(不包含 / 和 :,因为它们在 new RegExp() 中不是特殊字符) * 用于 glob 模式转换时转义字面字符 * 移到模块作用域以避免每次调用重新编译 */ const REGEX_META_CHARS_RE = /[.+^${}()|[\]\\]/g; /** * 占位符替换用的正则表达式 * 移到模块作用域以避免每次调用重新编译 */ const PLACEHOLDER_DOUBLE_STAR_RE = /<<<DOUBLE_STAR>>>/g; const PLACEHOLDER_SINGLE_STAR_RE = /<<<SINGLE_STAR>>>/g; const PLACEHOLDER_QUESTION_RE = /<<<QUESTION>>>/g; /** * 反斜杠匹配正则表达式 * 用于转义正则表达式字符串中的反斜杠 */ const BACKSLASH_RE = /\\/g; /** * 扩展名点前缀匹配正则表达式 * 移到模块作用域以避免每次调用重新编译 */ const EXTENSION_DOT_PREFIX_RE = /^\./; /** * 转义正则表达式中的特殊字符 * 包括 Windows 路径中的特殊字符(如 / 和 :) * @param str - 待转义的字符串 * @returns 转义后的字符串 */ function escapeRegExp$1(str) { return str.replace(ESCAPE_REGEXP, "\\$&"); } /** * 将 glob 模式转换为正则表达式字符串 * 支持 **、*、? 等基本 glob 语法 * * 转换顺序很重要: * 1. 先用占位符替换 glob 通配符,避免后续转义影响 * 2. 转义所有正则元字符(. + ^ $ { } ( ) | [ ] \),让它们作为字面量 * 3. 将占位符替换为正则表达式 * * 注意:/ 和 : 不需要转义,因为在 new RegExp() 中它们不是特殊字符 * * @param pattern - glob 模式 * @returns 正则表达式字符串 */ function globToRegExp(pattern) { return pattern.replace(GLOB_DOUBLE_STAR_RE, "<<<DOUBLE_STAR>>>").replace(GLOB_SINGLE_STAR_RE, "<<<SINGLE_STAR>>>").replace(GLOB_QUESTION_RE, "<<<QUESTION>>>").replace(REGEX_META_CHARS_RE, "\\$&").replace(PLACEHOLDER_DOUBLE_STAR_RE, ".*").replace(PLACEHOLDER_SINGLE_STAR_RE, "[^/]*").replace(PLACEHOLDER_QUESTION_RE, "[^/]"); } /** * 转义正则表达式字符串中的反斜杠 * 用于生成 new RegExp() 的字符串参数 * 因为在字符串字面量中,\ 需要双重转义才能表示正则中的 \ * @param str - 正则表达式字符串 * @returns 转义后的字符串 */ function escapeRegexString(str) { return str.replace(BACKSLASH_RE, "\\\\"); } /** * 创建排除模式的正则表达式数组代码 * 在循环外部调用,只执行一次,避免每次迭代重复创建正则表达式 * @param exclude - 排除模式数组 * @param varName - 正则数组变量名 * @returns 创建正则数组的代码字符串,如果 exclude 为空则返回空字符串 */ function createExcludePatterns(exclude, varName = "excludePatterns") { if (!exclude || exclude.length === 0) return ""; return `const ${varName} = [${exclude.map((p) => escapeRegexString(globToRegExp(p))).map((p) => `new RegExp('${p}')`).join(", ")}]`; } /** * 生成排除模式的检查代码 * 在循环内部调用,用于检查当前路径是否匹配排除模式 * @param pathVarName - 路径变量名,Vite 用 'path',Rspack 用 'key' * @param patternsVarName - 正则数组变量名,需要与 createExcludePatterns 中的一致 * @returns 检查代码字符串 */ function generateExcludeCheck(pathVarName = "path", patternsVarName = "excludePatterns") { return `if (${patternsVarName}.some(pattern => pattern.test(${pathVarName}))) return`; } /** * 生成路由路径转换代码 * 用于将文件路径转换为路由路径 * @param pathVar - 路径变量名(如 'path' 或 'key') * @param escapedDir - 转义后的目录路径 * @param basePath - 基础路径 * @param extensions - 文件扩展名列表 * @param catchAllFormat - catch-all 格式,Vue 用 '(.*)*',React 用 '/*' * @param caseSensitive - 是否区分大小写 * @returns 路径转换代码字符串 */ function generateRoutePathTransformCode(pathVar, escapedDir, basePath, extensions, catchAllFormat, caseSensitive) { return `let routePath = ${pathVar} .replace(/${escapedDir}/, '') .replace(/^\\.\\//, '') .replace(/^\\//, '') .replace(/\\.(${extensions.map((e) => e.replace(EXTENSION_DOT_PREFIX_RE, "")).join("|")})$/, '') .replace(/\\/index$/, '') .replace(/^index$/, '') .replace(/\\[(\\.\\.\\.)?(.+?)\\]/g, (_, isCatchAll, name) => isCatchAll ? \`:\${name}${catchAllFormat}\` : \`:\${name}\` ) .replace(/\\$(.+)/g, ':$1') .replace(/\\$$$/, '${catchAllFormat}') ${caseSensitive ? "" : ".toLowerCase()"} routePath = '${basePath}' + (routePath ? '/' + routePath : '') || '/'`; } /** * 生成路由名称转换代码 * 用于将文件路径转换为路由名称 * @param pathVar - 路径变量名(如 'path' 或 'key') * @param escapedDir - 转义后的目录路径 * @param extensions - 文件扩展名列表 * @param routeNameSeparator - 路由名称分隔符 * @returns 路由名称转换代码字符串 */ function generateRouteNameTransformCode(pathVar, escapedDir, extensions, routeNameSeparator) { return `const name = ${pathVar} .replace(/${escapedDir}/, '') .replace(/^\\.\\//, '') .replace(/^\\//, '') .replace(/\\.(${extensions.map((e) => e.replace(EXTENSION_DOT_PREFIX_RE, "")).join("|")})$/, '') .replace(/\\/index$/, '') .replace(/^index$/, '') .replace(/\\//g, '${routeNameSeparator}') .replace(/\\[(\\.\\.\\.)?(.+?)\\]/g, '$2') .replace(/\\$(.+)/g, '$1') .replace(/^${routeNameSeparator}/, '') || 'index'`; } //#endregion //#region src/resolvers/react.ts /** * 生成 React + Vite 的路由代码 * 使用 import.meta.glob 自动扫描页面文件 * @param options - 解析后的配置选项 * @returns 生成的路由代码字符串 */ function generateReactViteCode(options) { const contextBlocks = []; const excludePatternsCode = createExcludePatterns(options.exclude); const excludeCheckCode = excludePatternsCode ? generateExcludeCheck("path") : ""; for (const dir of options.dirs) { const globPattern = getGlobPattern(dir, options.extensions); const contextVar = `__pages_${contextBlocks.length}__`; const basePath = dir.baseRoute ? `/${dir.baseRoute}` : ""; const routePathCode = generateRoutePathTransformCode("path", escapeRegExp$1(dir.dir), basePath, options.extensions, "/*", options.caseSensitive); contextBlocks.push(` const ${contextVar} = import.meta.glob('${globPattern}') Object.entries(${contextVar}).forEach(([path, moduleFn]) => { const pathSegments = path.split('/') const shouldIgnore = pathSegments.some(seg => /^__.*__$/.test(seg)) if (shouldIgnore) return ${excludeCheckCode ? ` ${excludeCheckCode}\n` : ""} ${routePathCode} routes.push({ path: routePath, element: React.createElement(React.lazy(moduleFn)) }) })`); } return `import React from 'react' const routes = [] ${excludePatternsCode ? `${excludePatternsCode}\n` : ""}${contextBlocks.join("\n")} export default routes `; } /** * 生成 React + Rspack 的路由代码 * 使用 import.meta.webpackContext 自动扫描页面文件 * @param options - 解析后的配置选项 * @returns 生成的路由代码字符串 */ function generateReactRspackCode(options) { const contextBlocks = []; const excludePatternsCode = createExcludePatterns(options.exclude); const excludeCheckCode = excludePatternsCode ? generateExcludeCheck("key") : ""; for (const dir of options.dirs) { const pattern = getWebpackContextPattern(dir, options.extensions, options.root); const contextVar = `__pages_${contextBlocks.length}__`; const basePath = dir.baseRoute ? `/${dir.baseRoute}` : ""; const routePathCode = generateRoutePathTransformCode("key", escapeRegExp$1(pattern.path), basePath, options.extensions, "/*", options.caseSensitive); contextBlocks.push(` const ${contextVar} = import.meta.webpackContext('${pattern.path}', { recursive: ${pattern.recursive}, regExp: /${pattern.regExp}$/ }) ${contextVar}.keys().forEach((key) => { const pathSegments = key.split('/') const shouldIgnore = pathSegments.some(seg => /^__.*__$/.test(seg)) if (shouldIgnore) return ${excludeCheckCode ? ` ${excludeCheckCode}\n` : ""} ${routePathCode} const loadModule = async () => { const module = await ${contextVar}(key) return { default: module.default || module } } routes.push({ path: routePath, element: React.createElement(React.lazy(loadModule)) }) })`); } return `import React from 'react' const routes = [] ${excludePatternsCode ? `${excludePatternsCode}\n` : ""}${contextBlocks.join("\n")} export default routes `; } /** * 根据构建工具生成对应的 React 路由代码 * @param options - 解析后的配置选项 * @param buildTool - 构建工具类型 * @returns 生成的路由代码字符串 */ function generateReactRoutes(options, buildTool) { return buildTool === "vite" ? generateReactViteCode(options) : generateReactRspackCode(options); } //#endregion //#region src/resolvers/vue.ts /** * 生成 Vue + Vite 的路由代码 * 使用 import.meta.glob 自动扫描页面文件 * @param options - 解析后的配置选项 * @returns 生成的路由代码字符串 */ function generateVueViteCode(options) { const contextBlocks = []; const excludePatternsCode = createExcludePatterns(options.exclude); const excludeCheckCode = excludePatternsCode ? generateExcludeCheck("path") : ""; for (const dir of options.dirs) { const globPattern = getGlobPattern(dir, options.extensions); const contextVar = `__pages_${contextBlocks.length}__`; const basePath = dir.baseRoute ? `/${dir.baseRoute}` : ""; const escapedDir = escapeRegExp$1(dir.dir); const routePathCode = generateRoutePathTransformCode("path", escapedDir, basePath, options.extensions, "(.*)*", options.caseSensitive); const routeNameCode = generateRouteNameTransformCode("path", escapedDir, options.extensions, options.routeNameSeparator); contextBlocks.push(` const ${contextVar} = import.meta.glob('${globPattern}') Object.entries(${contextVar}).forEach(([path, module]) => { const pathSegments = path.split('/') const shouldIgnore = pathSegments.some(seg => /^__.*__$/.test(seg)) if (shouldIgnore) return ${excludeCheckCode ? ` ${excludeCheckCode}\n` : ""} ${routePathCode} ${routeNameCode} routes.push({ path: routePath, name, component: module }) })`); } return `const routes = [] ${excludePatternsCode ? `${excludePatternsCode}\n` : ""}${contextBlocks.join("\n")} export default routes `; } /** * 生成 Vue + Rspack 的路由代码 * 使用 import.meta.webpackContext 自动扫描页面文件 * @param options - 解析后的配置选项 * @returns 生成的路由代码字符串 */ function generateVueRspackCode(options) { const contextBlocks = []; const excludePatternsCode = createExcludePatterns(options.exclude); const excludeCheckCode = excludePatternsCode ? generateExcludeCheck("key") : ""; for (const dir of options.dirs) { const pattern = getWebpackContextPattern(dir, options.extensions, options.root); const contextVar = `__pages_${contextBlocks.length}__`; const basePath = dir.baseRoute ? `/${dir.baseRoute}` : ""; const escapedDir = escapeRegExp$1(pattern.path); const routePathCode = generateRoutePathTransformCode("key", escapedDir, basePath, options.extensions, "(.*)*", options.caseSensitive); const routeNameCode = generateRouteNameTransformCode("key", escapedDir, options.extensions, options.routeNameSeparator); contextBlocks.push(` const ${contextVar} = import.meta.webpackContext('${pattern.path}', { recursive: ${pattern.recursive}, regExp: /${pattern.regExp}$/ }) ${contextVar}.keys().forEach((key) => { const pathSegments = key.split('/') const shouldIgnore = pathSegments.some(seg => /^__.*__$/.test(seg)) if (shouldIgnore) return ${excludeCheckCode ? ` ${excludeCheckCode}\n` : ""} ${routePathCode} ${routeNameCode} routes.push({ path: routePath, name, component: () => ${contextVar}(key) }) })`); } return `const routes = [] ${excludePatternsCode ? `${excludePatternsCode}\n` : ""}${contextBlocks.join("\n")} export default routes `; } /** * 根据构建工具生成对应的 Vue 路由代码 * @param options - 解析后的配置选项 * @param buildTool - 构建工具类型 * @returns 生成的路由代码字符串 */ function generateVueRoutes(options, buildTool) { return buildTool === "vite" ? generateVueViteCode(options) : generateVueRspackCode(options); } //#endregion //#region src/resolvers/index.ts /** * 路由生成器映射表 * 将 resolver 类型映射到对应的路由生成函数 */ const generators = { vue: generateVueRoutes, react: generateReactRoutes }; /** * 生成路由代码 * 根据配置的 resolver 类型选择对应的生成器 * @param options - 解析后的配置选项 * @param buildTool - 构建工具类型 * @returns 生成的路由代码字符串 */ function generateRoutes(options, buildTool) { return generators[options.resolver](options, buildTool); } //#endregion //#region src/index.ts /** * 虚拟模块 ID 映射表 * 根据不同的 resolver 类型提供不同的虚拟模块 ID */ const VIRTUAL_MODULE_IDS = { vue: ["virtual:unplugin-convention-routes/vue", "~pages"], react: ["virtual:unplugin-convention-routes/react", "~react-pages"] }; const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const createExactIdFilter = (ids) => new RegExp(`^(?:${ids.map(escapeRegExp).join("|")})$`); /** * unplugin 工厂函数 * 创建约定式路由插件的核心实现 * @param userOptions - 用户配置选项 * @param meta - unplugin 提供的元信息 * @param meta.framework - 构建工具框架类型 * @returns unplugin 插件对象 */ const unpluginFactory = (userOptions, { framework }) => { const options = resolveOptions(userOptions); const buildTool = framework === "vite" ? "vite" : "rspack"; const virtualIds = VIRTUAL_MODULE_IDS[options.resolver]; const virtualIdFilter = createExactIdFilter(virtualIds); return { name: "unplugin-convention-routes", /** * 解析模块 ID * 将虚拟模块 ID 转换为内部 ID * @param id - 模块 ID * @returns 解析后的模块 ID 或 null */ resolveId: { filter: { id: virtualIdFilter }, handler(id) { if (virtualIds.includes(id)) return virtualIds[0]; return null; } }, /** * 加载模块内容 * 为虚拟模块生成路由代码 * @param id - 模块 ID * @returns 生成的代码或 null */ load: { filter: { id: virtualIdFilter }, handler(id) { if (id === virtualIds[0]) return generateRoutes(options, buildTool); return null; } } }; }; /** * 创建 unplugin 实例 */ const unplugin$1 = /* @__PURE__ */ (0, unplugin.createUnplugin)(unpluginFactory); //#endregion Object.defineProperty(exports, "__toESM", { enumerable: true, get: function() { return __toESM; } }); Object.defineProperty(exports, "unplugin", { enumerable: true, get: function() { return unplugin$1; } }); Object.defineProperty(exports, "unpluginFactory", { enumerable: true, get: function() { return unpluginFactory; } });