@v-long/vite-plugin-view-name-map 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.cjs +2 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +20 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +89 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +86 -13
- package/dist/index.js.map +1 -1
- package/package.json +55 -50
- /package/types/{virtual.d.ts → client.d.ts} +0 -0
package/dist/client.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* virtual:view-name-map 的基础类型声明
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:
|
|
5
|
+
* - 插件侧不包含任何业务信息
|
|
6
|
+
* - 仅提供可被业务侧增强的类型锚点
|
|
7
|
+
*/
|
|
8
|
+
declare module 'virtual:view-name-map' {
|
|
9
|
+
/**
|
|
10
|
+
* 视图 name 映射表类型
|
|
11
|
+
* key:规范化路径
|
|
12
|
+
* value:组件 defineOptions 中声明的 name
|
|
13
|
+
*/
|
|
14
|
+
export type ViewNameMap = Record<string, string>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 构建期生成的视图 name 映射表
|
|
18
|
+
*/
|
|
19
|
+
export const viewNameMap: ViewNameMap;
|
|
20
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* virtual:view-name-map 的基础类型声明
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:
|
|
5
|
+
* - 插件侧不包含任何业务信息
|
|
6
|
+
* - 仅提供可被业务侧增强的类型锚点
|
|
7
|
+
*/
|
|
8
|
+
declare module 'virtual:view-name-map' {
|
|
9
|
+
/**
|
|
10
|
+
* 视图 name 映射表类型
|
|
11
|
+
* key:规范化路径
|
|
12
|
+
* value:组件 defineOptions 中声明的 name
|
|
13
|
+
*/
|
|
14
|
+
export type ViewNameMap = Record<string, string>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 构建期生成的视图 name 映射表
|
|
18
|
+
*/
|
|
19
|
+
export const viewNameMap: ViewNameMap;
|
|
20
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -28,11 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
33
|
default: () => viewNameMapPlugin
|
|
34
34
|
});
|
|
35
|
-
module.exports = __toCommonJS(
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
36
|
var import_vite = require("vite");
|
|
37
37
|
var import_path2 = __toESM(require("path"), 1);
|
|
38
38
|
|
|
@@ -43,22 +43,80 @@ var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
|
43
43
|
var import_magic_string = __toESM(require("magic-string"), 1);
|
|
44
44
|
var import_compiler_sfc = require("@vue/compiler-sfc");
|
|
45
45
|
var import_ts_morph = require("ts-morph");
|
|
46
|
+
|
|
47
|
+
// src/plugin-errors.ts
|
|
48
|
+
var PluginError = class extends Error {
|
|
49
|
+
constructor(message, details) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "PluginError";
|
|
52
|
+
this.details = details;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var ValidationError = class extends PluginError {
|
|
56
|
+
constructor(message, details) {
|
|
57
|
+
super(message, details);
|
|
58
|
+
this.name = "ValidationError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var IoError = class extends PluginError {
|
|
62
|
+
constructor(message, details) {
|
|
63
|
+
super(message, details);
|
|
64
|
+
this.name = "IoError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var LogicError = class extends PluginError {
|
|
68
|
+
constructor(message, details) {
|
|
69
|
+
super(message, details);
|
|
70
|
+
this.name = "LogicError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/generate.ts
|
|
75
|
+
function ensureGenerateOptionsValid(options) {
|
|
76
|
+
const viewsDirAny = options.viewsDir;
|
|
77
|
+
if (viewsDirAny !== void 0 && typeof viewsDirAny !== "string") {
|
|
78
|
+
throw new ValidationError("Invalid type for options.viewsDir; expected string", {
|
|
79
|
+
receivedType: typeof viewsDirAny,
|
|
80
|
+
receivedValue: viewsDirAny
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const autoGenAny = options.autoGenerateName;
|
|
84
|
+
if (autoGenAny !== void 0 && typeof autoGenAny !== "boolean") {
|
|
85
|
+
throw new ValidationError("Invalid type for options.autoGenerateName; expected boolean", {
|
|
86
|
+
receivedType: typeof autoGenAny,
|
|
87
|
+
receivedValue: autoGenAny
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
46
91
|
async function generateViewNameMap(options = {}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
ensureGenerateOptionsValid(options);
|
|
93
|
+
try {
|
|
94
|
+
const viewsDir = options.viewsDir ?? "src/views";
|
|
95
|
+
const autoGen = options.autoGenerateName ?? true;
|
|
96
|
+
const map = {};
|
|
97
|
+
const files = await (0, import_fast_glob.default)(["**/*.vue", "**/*.tsx"], { cwd: viewsDir, absolute: true });
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = await import_promises.default.readFile(file, "utf-8");
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new IoError(`Failed to read file: ${file}`, { cause: err });
|
|
104
|
+
}
|
|
105
|
+
let name = extractNameFromDefineOptionsInSfc(content);
|
|
106
|
+
if (!name) {
|
|
107
|
+
const m2 = content.match(/defineOptions\s*\(\s*\{[^\}]*name\s*:\s*['"]([^'"]+)['"][^\}]*\}\s*\)/m);
|
|
108
|
+
if (m2) name = m2[1];
|
|
109
|
+
}
|
|
110
|
+
if (!name && autoGen) {
|
|
111
|
+
name = generateNameFromFilePath(file, viewsDir);
|
|
112
|
+
}
|
|
113
|
+
if (name) map[normalizePath(file, viewsDir)] = name;
|
|
58
114
|
}
|
|
59
|
-
|
|
115
|
+
return map;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof PluginError) throw err;
|
|
118
|
+
throw new LogicError("Failed to generate view name map", { cause: err });
|
|
60
119
|
}
|
|
61
|
-
return map;
|
|
62
120
|
}
|
|
63
121
|
function injectDefineOptions(code, id, name) {
|
|
64
122
|
const { descriptor } = (0, import_compiler_sfc.parse)(code);
|
|
@@ -111,6 +169,21 @@ function extractNameFromDefineOptions(sourceFile) {
|
|
|
111
169
|
}
|
|
112
170
|
return void 0;
|
|
113
171
|
}
|
|
172
|
+
function extractNameFromDefineOptionsInSfc(content) {
|
|
173
|
+
try {
|
|
174
|
+
const { descriptor } = (0, import_compiler_sfc.parse)(content);
|
|
175
|
+
if (!descriptor.scriptSetup) return void 0;
|
|
176
|
+
const scriptContent = descriptor.scriptSetup.content;
|
|
177
|
+
const regex = /defineOptions\s*\(\s*\{\s*name\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/;
|
|
178
|
+
const m = scriptContent.match(regex);
|
|
179
|
+
if (m) return m[1];
|
|
180
|
+
const project = new import_ts_morph.Project({ useInMemoryFileSystem: true });
|
|
181
|
+
const sourceFile = project.createSourceFile("temp.ts", scriptContent);
|
|
182
|
+
return extractNameFromDefineOptions(sourceFile);
|
|
183
|
+
} catch {
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
114
187
|
function generateNameFromFilePath(file, viewsDir) {
|
|
115
188
|
const absoluteViewsDir = import_path.default.resolve(viewsDir);
|
|
116
189
|
const relativePath = import_path.default.relative(absoluteViewsDir, file);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/virtual-module.ts"],"sourcesContent":["import type {Plugin} from 'vite';\r\nimport {normalizePath} from 'vite'; // 必须使用 vite 提供的路径归一化工具\r\nimport path from 'path';\r\nimport {generateViewNameMap, injectDefineOptions, generateNameFromFilePath, ViewNameMap} from './generate';\r\nimport {createVirtualModule} from './virtual-module';\r\n\r\n/**\r\n * 插件配置项\r\n */\r\nexport interface ViewNameMapPluginOptions {\r\n /** Vue 页面根目录,默认 'src/views' */\r\n viewsDir?: string;\r\n /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */\r\n autoGenerateName?: boolean;\r\n /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */\r\n autoInjectName?: boolean;\r\n}\r\n\r\n/**\r\n * vite-plugin-view-name-map\r\n *\r\n * 构建期扫描 views 目录,提取组件 name 并生成 virtual module\r\n */\r\nexport default function viewNameMapPlugin(\r\n options: ViewNameMapPluginOptions = {}\r\n): Plugin {\r\n const virtualId = 'virtual:view-name-map';\r\n const resolvedVirtualId = '\\0' + virtualId;\r\n\r\n // 统一转换为正斜杠路径\r\n const viewsDir = options.viewsDir ?? 'src/views';\r\n const autoInjectName = options.autoInjectName ?? true;\r\n\r\n // 核心约束:如果开启了自动注入,则必须开启自动生成,否则注入没有数据源\r\n const autoGenerateName = autoInjectName ? true : (options.autoGenerateName ?? true);\r\n\r\n const root = normalizePath(process.cwd());\r\n const absoluteViewsDir = normalizePath(path.resolve(root, viewsDir));\r\n\r\n let virtualCode = '';\r\n let nameMap: ViewNameMap = {};\r\n let injectedCount = 0; // 注入计数器\r\n\r\n return {\r\n name: 'vite-plugin-view-name-map',\r\n enforce: 'pre',\r\n\r\n async buildStart(): Promise<void> {\r\n // 初始扫描\r\n nameMap = await generateViewNameMap({\r\n viewsDir: viewsDir,\r\n autoGenerateName: autoGenerateName\r\n });\r\n virtualCode = createVirtualModule(nameMap);\r\n\r\n // 打印初始化统计信息\r\n const count = Object.keys(nameMap).length;\r\n if (count > 0) {\r\n const status = autoInjectName ? '已开启自动注入' : '仅生成映射表';\r\n console.log(`\\x1b[32m[view-name-map] ${status}:扫描到 ${count} 个视图组件。\\x1b[0m`);\r\n }\r\n },\r\n\r\n resolveId(id: string): string | undefined {\r\n if (id === virtualId) return resolvedVirtualId;\r\n return undefined;\r\n },\r\n\r\n load(id: string): string | undefined {\r\n if (id === resolvedVirtualId) return virtualCode;\r\n return undefined;\r\n },\r\n\r\n async transform(code: string, id: string) {\r\n // 只有开启注入时才继续执行 transform 逻辑\r\n if (!autoInjectName) return null;\r\n\r\n const cleanId = normalizePath(id.split('?')[0]);\r\n\r\n // 过滤非目标文件\r\n if (!cleanId.endsWith('.vue') || !cleanId.startsWith(absoluteViewsDir)) {\r\n return null;\r\n }\r\n\r\n // 获取组件名称\r\n const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);\r\n\r\n try {\r\n // 执行 AST 注入\r\n const result = injectDefineOptions(code, cleanId, componentName);\r\n\r\n // 如果返回了结果,说明确实进行了注入(非 null)\r\n if (result) {\r\n injectedCount++;\r\n }\r\n\r\n return result;\r\n } catch (err) {\r\n console.error(`\\x1b[31m[view-name-map] 注入异常: ${cleanId}\\x1b[0m`, err);\r\n return null;\r\n }\r\n },\r\n\r\n // 在构建结束时打印统计信息\r\n buildEnd() {\r\n // 仅在开发/构建完成且有注入行为时输出\r\n if (autoInjectName && injectedCount > 0) {\r\n console.log(`\\x1b[36m[view-name-map] 本次运行已为 ${injectedCount} 个组件注入了 defineOptions({ name: \"...\" })。\\x1b[0m`);\r\n }\r\n }\r\n };\r\n}\r\n","import path from 'path';\r\nimport fs from 'fs/promises';\r\nimport fg from 'fast-glob';\r\nimport MagicString from 'magic-string'; // 需要安装: npm install magic-string\r\nimport {parse} from '@vue/compiler-sfc'; // Vue 自带\r\nimport {CallExpression, Expression, Node, ObjectLiteralExpression, Project, SourceFile, SyntaxKind} from 'ts-morph';\r\n\r\n/** 视图 name 映射表类型 */\r\nexport type ViewNameMap = Record<string, string>;\r\n\r\n/** 构建期生成逻辑配置 */\r\nexport interface GenerateOptions {\r\n viewsDir?: string;\r\n autoGenerateName?: boolean;\r\n}\r\n\r\n/**\r\n * 扫描 views 目录生成组件 name 映射表\r\n */\r\nexport async function generateViewNameMap(\r\n options: GenerateOptions = {}\r\n): Promise<ViewNameMap> {\r\n const viewsDir: string = options.viewsDir ?? 'src/views';\r\n const autoGen: boolean = options.autoGenerateName ?? true;\r\n\r\n const map: ViewNameMap = {};\r\n const files: string[] = await fg(['**/*.vue', '**/*.tsx'], {cwd: viewsDir, absolute: true});\r\n\r\n const project: Project = new Project({useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true});\r\n\r\n for (const file of files) {\r\n const content: string = await fs.readFile(file, 'utf-8');\r\n const sourceFile: SourceFile = project.createSourceFile(file, content, {overwrite: true});\r\n\r\n let name: string | undefined = extractNameFromDefineOptions(sourceFile);\r\n\r\n if (!name && autoGen) {\r\n name = generateNameFromFilePath(file, viewsDir);\r\n }\r\n\r\n // 修改点:传入 viewsDir 进行相对化处理\r\n if (name) map[normalizePath(file, viewsDir)] = name;\r\n }\r\n\r\n return map;\r\n}\r\n\r\n/**\r\n * 在 transform 阶段注入 defineOptions (不写入文件)\r\n */\r\nexport function injectDefineOptions(code: string, id: string, name: string) {\r\n const {descriptor} = parse(code);\r\n if (!descriptor.scriptSetup) return null;\r\n\r\n const content = descriptor.scriptSetup.content;\r\n const startOffset = descriptor.scriptSetup.loc.start.offset;\r\n\r\n // 使用 ts-morph 分析 scriptSetup 内容\r\n const project = new Project({useInMemoryFileSystem: true});\r\n const sourceFile = project.createSourceFile('temp.ts', content);\r\n\r\n // 检查是否已有 defineOptions\r\n const hasName = !!extractNameFromDefineOptions(sourceFile);\r\n if (hasName) return null;\r\n\r\n const s = new MagicString(code);\r\n const defineOptionsCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)\r\n .find(c => c.getExpression().getText() === 'defineOptions');\r\n\r\n if (defineOptionsCall) {\r\n // 情况 A: 已有 defineOptions 但没写 name 属性\r\n const arg = defineOptionsCall.getArguments()[0];\r\n if (Node.isObjectLiteralExpression(arg)) {\r\n const properties = arg.getProperties();\r\n const pos = startOffset + arg.getStart() + 1; // '{' 之后\r\n\r\n if (properties.length === 0) {\r\n // 空对象直接插入\r\n s.appendRight(pos, ` name: '${name}' `);\r\n } else {\r\n // 已有属性,必须补充逗号分隔\r\n s.appendRight(pos, ` name: '${name}', `);\r\n }\r\n }\r\n } else {\r\n // 情况 B: 完全没有 defineOptions,在 scriptSetup 顶部注入\r\n s.appendLeft(startOffset, `\\ndefineOptions({ name: '${name}' });\\n`);\r\n }\r\n\r\n return {\r\n code: s.toString(),\r\n map: s.generateMap({source: id, includeContent: true, hires: true})\r\n };\r\n}\r\n\r\n/**\r\n * 从 defineOptions 宏定义中提取组件 name 属性值\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\r\n // 获取源文件中所有的调用表达式\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression: Expression = call.getExpression();\r\n // 匹配 defineOptions 调用\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const args = call.getArguments();\r\n if (args.length === 0) continue;\r\n\r\n // 获取第一个对象字面量参数\r\n const obj: ObjectLiteralExpression | undefined = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n // 查找 name 属性并确保其为标准的 key: value 赋值形式\r\n const nameProp = obj.getProperty('name');\r\n if (nameProp && Node.isPropertyAssignment(nameProp)) {\r\n const initializer = nameProp.getInitializer();\r\n if (initializer && Node.isStringLiteral(initializer)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * 根据文件路径生成组件 name (PascalCase)\r\n * 规则:\r\n * - 保持层级关系\r\n * - 移除 index 后缀\r\n * - 自动处理路径中的横杠、下划线为大驼峰\r\n */\r\nexport function generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const absoluteViewsDir: string = path.resolve(viewsDir);\r\n const relativePath: string = path.relative(absoluteViewsDir, file);\r\n const {dir, name} = path.parse(relativePath);\r\n\r\n // 拆分层级\r\n const segments: string[] = dir ? dir.split(path.sep) : [];\r\n if (name !== 'index') {\r\n segments.push(name);\r\n }\r\n\r\n // 转换每一级为 PascalCase 并拼接\r\n return segments\r\n .map((s: string) => s\r\n // 处理内部横杠或下划线: sys-user -> sysUser\r\n .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())\r\n // 首字母大写: sysUser -> SysUser\r\n .replace(/^[a-z]/, (c: string) => c.toUpperCase())\r\n )\r\n .join('') || 'Index';\r\n}\r\n\r\n/**\r\n * 路径统一处理:生成的 Key 包含 viewsDir 目录名\r\n * 示例:/Users/me/project/src/views/User/Index.vue -> src/views/User/Index.vue\r\n */\r\nfunction normalizePath(file: string, viewsDir: string): string {\r\n // 1. 获取 viewsDir 的基础名称 (例如 'src/views')\r\n // 2. 获取文件相对于 viewsDir 的部分\r\n // 3. 拼接并统一分隔符\r\n const relativePath: string = path.relative(path.resolve(viewsDir), file);\r\n const combinedPath: string = path.join(viewsDir, relativePath);\r\n\r\n return combinedPath.split(path.sep).join('/');\r\n}\r\n","import type { ViewNameMap } from './generate';\r\n\r\n/**\r\n * 根据扫描结果生成 virtual module 的源码字符串\r\n *\r\n * 设计说明:\r\n * - 不使用 default export,便于未来扩展更多具名导出\r\n * - 同时更利于 TypeScript module augmentation\r\n */\r\nexport function createVirtualModule(map: ViewNameMap): string {\r\n return `\r\nexport const viewNameMap = ${JSON.stringify(map, null, 2)};\r\n`;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAA4B;AAC5B,IAAAA,eAAiB;;;ACFjB,kBAAiB;AACjB,sBAAe;AACf,uBAAe;AACf,0BAAwB;AACxB,0BAAoB;AACpB,sBAAyG;AAczG,eAAsB,oBAClB,UAA2B,CAAC,GACR;AACpB,QAAM,WAAmB,QAAQ,YAAY;AAC7C,QAAM,UAAmB,QAAQ,oBAAoB;AAErD,QAAM,MAAmB,CAAC;AAC1B,QAAM,QAAkB,UAAM,iBAAAC,SAAG,CAAC,YAAY,UAAU,GAAG,EAAC,KAAK,UAAU,UAAU,KAAI,CAAC;AAE1F,QAAM,UAAmB,IAAI,wBAAQ,EAAC,uBAAuB,MAAM,6BAA6B,KAAI,CAAC;AAErG,aAAW,QAAQ,OAAO;AACtB,UAAM,UAAkB,MAAM,gBAAAC,QAAG,SAAS,MAAM,OAAO;AACvD,UAAM,aAAyB,QAAQ,iBAAiB,MAAM,SAAS,EAAC,WAAW,KAAI,CAAC;AAExF,QAAI,OAA2B,6BAA6B,UAAU;AAEtE,QAAI,CAAC,QAAQ,SAAS;AAClB,aAAO,yBAAyB,MAAM,QAAQ;AAAA,IAClD;AAGA,QAAI,KAAM,KAAI,cAAc,MAAM,QAAQ,CAAC,IAAI;AAAA,EACnD;AAEA,SAAO;AACX;AAKO,SAAS,oBAAoB,MAAc,IAAY,MAAc;AACxE,QAAM,EAAC,WAAU,QAAI,2BAAM,IAAI;AAC/B,MAAI,CAAC,WAAW,YAAa,QAAO;AAEpC,QAAM,UAAU,WAAW,YAAY;AACvC,QAAM,cAAc,WAAW,YAAY,IAAI,MAAM;AAGrD,QAAM,UAAU,IAAI,wBAAQ,EAAC,uBAAuB,KAAI,CAAC;AACzD,QAAM,aAAa,QAAQ,iBAAiB,WAAW,OAAO;AAG9D,QAAM,UAAU,CAAC,CAAC,6BAA6B,UAAU;AACzD,MAAI,QAAS,QAAO;AAEpB,QAAM,IAAI,IAAI,oBAAAC,QAAY,IAAI;AAC9B,QAAM,oBAAoB,WAAW,qBAAqB,2BAAW,cAAc,EAC9E,KAAK,OAAK,EAAE,cAAc,EAAE,QAAQ,MAAM,eAAe;AAE9D,MAAI,mBAAmB;AAEnB,UAAM,MAAM,kBAAkB,aAAa,EAAE,CAAC;AAC9C,QAAI,qBAAK,0BAA0B,GAAG,GAAG;AACrC,YAAM,aAAa,IAAI,cAAc;AACrC,YAAM,MAAM,cAAc,IAAI,SAAS,IAAI;AAE3C,UAAI,WAAW,WAAW,GAAG;AAEzB,UAAE,YAAY,KAAK,WAAW,IAAI,IAAI;AAAA,MAC1C,OAAO;AAEH,UAAE,YAAY,KAAK,WAAW,IAAI,KAAK;AAAA,MAC3C;AAAA,IACJ;AAAA,EACJ,OAAO;AAEH,MAAE,WAAW,aAAa;AAAA,yBAA4B,IAAI;AAAA,CAAS;AAAA,EACvE;AAEA,SAAO;AAAA,IACH,MAAM,EAAE,SAAS;AAAA,IACjB,KAAK,EAAE,YAAY,EAAC,QAAQ,IAAI,gBAAgB,MAAM,OAAO,KAAI,CAAC;AAAA,EACtE;AACJ;AAKA,SAAS,6BAA6B,YAA4C;AAE9E,QAAM,QAA0B,WAAW,qBAAqB,2BAAW,cAAc;AAEzF,aAAW,QAAQ,OAAO;AACtB,UAAM,aAAyB,KAAK,cAAc;AAElD,QAAI,CAAC,cAAc,WAAW,QAAQ,MAAM,gBAAiB;AAE7D,UAAM,OAAO,KAAK,aAAa;AAC/B,QAAI,KAAK,WAAW,EAAG;AAGvB,UAAM,MAA2C,KAAK,CAAC,GAAG,OAAO,2BAAW,uBAAuB;AACnG,QAAI,CAAC,IAAK;AAGV,UAAM,WAAW,IAAI,YAAY,MAAM;AACvC,QAAI,YAAY,qBAAK,qBAAqB,QAAQ,GAAG;AACjD,YAAM,cAAc,SAAS,eAAe;AAC5C,UAAI,eAAe,qBAAK,gBAAgB,WAAW,GAAG;AAClD,eAAO,YAAY,eAAe;AAAA,MACtC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AASO,SAAS,yBAAyB,MAAc,UAA0B;AAC7E,QAAM,mBAA2B,YAAAC,QAAK,QAAQ,QAAQ;AACtD,QAAM,eAAuB,YAAAA,QAAK,SAAS,kBAAkB,IAAI;AACjE,QAAM,EAAC,KAAK,KAAI,IAAI,YAAAA,QAAK,MAAM,YAAY;AAG3C,QAAM,WAAqB,MAAM,IAAI,MAAM,YAAAA,QAAK,GAAG,IAAI,CAAC;AACxD,MAAI,SAAS,SAAS;AAClB,aAAS,KAAK,IAAI;AAAA,EACtB;AAGA,SAAO,SACF;AAAA,IAAI,CAAC,MAAc,EAEf,QAAQ,YAAY,CAAC,GAAW,MAAc,EAAE,YAAY,CAAC,EAE7D,QAAQ,UAAU,CAAC,MAAc,EAAE,YAAY,CAAC;AAAA,EACrD,EACC,KAAK,EAAE,KAAK;AACrB;AAMA,SAAS,cAAc,MAAc,UAA0B;AAI3D,QAAM,eAAuB,YAAAA,QAAK,SAAS,YAAAA,QAAK,QAAQ,QAAQ,GAAG,IAAI;AACvE,QAAM,eAAuB,YAAAA,QAAK,KAAK,UAAU,YAAY;AAE7D,SAAO,aAAa,MAAM,YAAAA,QAAK,GAAG,EAAE,KAAK,GAAG;AAChD;;;AC/JO,SAAS,oBAAoB,KAA0B;AAC1D,SAAO;AAAA,6BACkB,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA;AAEzD;;;AFUe,SAAR,kBACH,UAAoC,CAAC,GAC/B;AACN,QAAM,YAAY;AAClB,QAAM,oBAAoB,OAAO;AAGjC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,iBAAiB,QAAQ,kBAAkB;AAGjD,QAAM,mBAAmB,iBAAiB,OAAQ,QAAQ,oBAAoB;AAE9E,QAAM,WAAO,2BAAc,QAAQ,IAAI,CAAC;AACxC,QAAM,uBAAmB,2BAAc,aAAAC,QAAK,QAAQ,MAAM,QAAQ,CAAC;AAEnE,MAAI,cAAc;AAClB,MAAI,UAAuB,CAAC;AAC5B,MAAI,gBAAgB;AAEpB,SAAO;AAAA,IACH,MAAM;AAAA,IACN,SAAS;AAAA,IAET,MAAM,aAA4B;AAE9B,gBAAU,MAAM,oBAAoB;AAAA,QAChC;AAAA,QACA;AAAA,MACJ,CAAC;AACD,oBAAc,oBAAoB,OAAO;AAGzC,YAAM,QAAQ,OAAO,KAAK,OAAO,EAAE;AACnC,UAAI,QAAQ,GAAG;AACX,cAAM,SAAS,iBAAiB,+CAAY;AAC5C,gBAAQ,IAAI,2BAA2B,MAAM,4BAAQ,KAAK,8CAAgB;AAAA,MAC9E;AAAA,IACJ;AAAA,IAEA,UAAU,IAAgC;AACtC,UAAI,OAAO,UAAW,QAAO;AAC7B,aAAO;AAAA,IACX;AAAA,IAEA,KAAK,IAAgC;AACjC,UAAI,OAAO,kBAAmB,QAAO;AACrC,aAAO;AAAA,IACX;AAAA,IAEA,MAAM,UAAU,MAAc,IAAY;AAEtC,UAAI,CAAC,eAAgB,QAAO;AAE5B,YAAM,cAAU,2BAAc,GAAG,MAAM,GAAG,EAAE,CAAC,CAAC;AAG9C,UAAI,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,WAAW,gBAAgB,GAAG;AACpE,eAAO;AAAA,MACX;AAGA,YAAM,gBAAgB,yBAAyB,SAAS,gBAAgB;AAExE,UAAI;AAEA,cAAM,SAAS,oBAAoB,MAAM,SAAS,aAAa;AAG/D,YAAI,QAAQ;AACR;AAAA,QACJ;AAEA,eAAO;AAAA,MACX,SAAS,KAAK;AACV,gBAAQ,MAAM,qDAAiC,OAAO,WAAW,GAAG;AACpE,eAAO;AAAA,MACX;AAAA,IACJ;AAAA;AAAA,IAGA,WAAW;AAEP,UAAI,kBAAkB,gBAAgB,GAAG;AACrC,gBAAQ,IAAI,gEAAkC,aAAa,mFAAgD;AAAA,MAC/G;AAAA,IACJ;AAAA,EACJ;AACJ;","names":["import_path","fg","fs","MagicString","path","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/plugin-errors.ts","../src/virtual-module.ts"],"sourcesContent":["import type {Plugin} from 'vite';\r\nimport {normalizePath} from 'vite'; // 必须使用 vite 提供的路径归一化工具\r\nimport path from 'path';\r\nimport {generateViewNameMap, injectDefineOptions, generateNameFromFilePath, ViewNameMap} from './generate';\r\nimport {createVirtualModule} from './virtual-module';\r\n\r\n/**\r\n * 插件配置项\r\n */\r\nexport interface ViewNameMapPluginOptions {\r\n /** Vue 页面根目录,默认 'src/views' */\r\n viewsDir?: string;\r\n /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */\r\n autoGenerateName?: boolean;\r\n /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */\r\n autoInjectName?: boolean;\r\n}\r\n\r\n/**\r\n * vite-plugin-view-name-map\r\n *\r\n * 构建期扫描 views 目录,提取组件 name 并生成 virtual module\r\n */\r\nexport default function viewNameMapPlugin(\r\n options: ViewNameMapPluginOptions = {}\r\n): Plugin {\r\n const virtualId = 'virtual:view-name-map';\r\n const resolvedVirtualId = '\\0' + virtualId;\r\n\r\n // 统一转换为正斜杠路径\r\n const viewsDir = options.viewsDir ?? 'src/views';\r\n const autoInjectName = options.autoInjectName ?? true;\r\n\r\n // 核心约束:如果开启了自动注入,则必须开启自动生成,否则注入没有数据源\r\n const autoGenerateName = autoInjectName ? true : (options.autoGenerateName ?? true);\r\n\r\n const root = normalizePath(process.cwd());\r\n const absoluteViewsDir = normalizePath(path.resolve(root, viewsDir));\r\n\r\n let virtualCode = '';\r\n let nameMap: ViewNameMap = {};\r\n let injectedCount = 0; // 注入计数器\r\n\r\n return {\r\n name: 'vite-plugin-view-name-map',\r\n enforce: 'pre',\r\n\r\n async buildStart(): Promise<void> {\r\n // 初始扫描\r\n nameMap = await generateViewNameMap({\r\n viewsDir: viewsDir,\r\n autoGenerateName: autoGenerateName\r\n });\r\n virtualCode = createVirtualModule(nameMap);\r\n\r\n // 打印初始化统计信息\r\n const count = Object.keys(nameMap).length;\r\n if (count > 0) {\r\n const status = autoInjectName ? '已开启自动注入' : '仅生成映射表';\r\n console.log(`\\x1b[32m[view-name-map] ${status}:扫描到 ${count} 个视图组件。\\x1b[0m`);\r\n }\r\n },\r\n\r\n resolveId(id: string): string | undefined {\r\n if (id === virtualId) return resolvedVirtualId;\r\n return undefined;\r\n },\r\n\r\n load(id: string): string | undefined {\r\n if (id === resolvedVirtualId) return virtualCode;\r\n return undefined;\r\n },\r\n\r\n async transform(code: string, id: string) {\r\n // 只有开启注入时才继续执行 transform 逻辑\r\n if (!autoInjectName) return null;\r\n\r\n const cleanId = normalizePath(id.split('?')[0]);\r\n\r\n // 过滤非目标文件\r\n if (!cleanId.endsWith('.vue') || !cleanId.startsWith(absoluteViewsDir)) {\r\n return null;\r\n }\r\n\r\n // 获取组件名称\r\n const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);\r\n\r\n try {\r\n // 执行 AST 注入\r\n const result = injectDefineOptions(code, cleanId, componentName);\r\n\r\n // 如果返回了结果,说明确实进行了注入(非 null)\r\n if (result) {\r\n injectedCount++;\r\n }\r\n\r\n return result;\r\n } catch (err) {\r\n console.error(`\\x1b[31m[view-name-map] 注入异常: ${cleanId}\\x1b[0m`, err);\r\n return null;\r\n }\r\n },\r\n\r\n // 在构建结束时打印统计信息\r\n buildEnd() {\r\n // 仅在开发/构建完成且有注入行为时输出\r\n if (autoInjectName && injectedCount > 0) {\r\n console.log(`\\x1b[36m[view-name-map] 本次运行已为 ${injectedCount} 个组件注入了 defineOptions({ name: \"...\" })。\\x1b[0m`);\r\n }\r\n }\r\n };\r\n}\r\n","import path from 'path';\nimport fs from 'fs/promises';\nimport fg from 'fast-glob';\nimport MagicString from 'magic-string'; // 需要安装: npm install magic-string\nimport {parse} from '@vue/compiler-sfc'; // Vue 自带\nimport {CallExpression, Expression, Node, ObjectLiteralExpression, Project, SourceFile, SyntaxKind} from 'ts-morph';\nimport {PluginError, ValidationError, IoError, LogicError} from './plugin-errors';\n\r\n/** 视图 name 映射表类型 */\r\nexport type ViewNameMap = Record<string, string>;\r\n\r\n/** 构建期生成逻辑配置 */\r\nexport interface GenerateOptions {\n viewsDir?: string;\n autoGenerateName?: boolean;\n}\n\n// 内部的配置校验,确保输入类型及字段符合期望,降低运行时错误\nfunction ensureGenerateOptionsValid(options: GenerateOptions): void {\n // Runtime type checks using loose typing to avoid TypeScript narrowing pitfalls\n const viewsDirAny: any = (options as any).viewsDir;\n if (viewsDirAny !== undefined && typeof viewsDirAny !== 'string') {\n throw new ValidationError('Invalid type for options.viewsDir; expected string', {\n receivedType: typeof viewsDirAny,\n receivedValue: viewsDirAny,\n });\n }\n const autoGenAny: any = (options as any).autoGenerateName;\n if (autoGenAny !== undefined && typeof autoGenAny !== 'boolean') {\n throw new ValidationError('Invalid type for options.autoGenerateName; expected boolean', {\n receivedType: typeof autoGenAny,\n receivedValue: autoGenAny,\n });\n }\n}\n\r\n/**\r\n * 扫描 views 目录生成组件 name 映射表\r\n */\r\nexport async function generateViewNameMap(\n options: GenerateOptions = {}\n): Promise<ViewNameMap> {\n // 参数校验,尽早抛出可追踪的错误\n ensureGenerateOptionsValid(options);\n\n try {\n const viewsDir: string = options.viewsDir ?? 'src/views';\n const autoGen: boolean = options.autoGenerateName ?? true;\n\n const map: ViewNameMap = {};\n const files: string[] = await fg(['**/*.vue', '**/*.tsx'], {cwd: viewsDir, absolute: true});\n\n for (const file of files) {\n let content: string;\n try {\n content = await fs.readFile(file, 'utf-8');\n } catch (err) {\n throw new IoError(`Failed to read file: ${file}`, { cause: err });\n }\n\n // 优先尝试从 Vue SFC 的 scriptSetup 提取 defineOptions.name\n let name: string | undefined = extractNameFromDefineOptionsInSfc(content);\n // 兜底:更直接的全文搜索作为额外鲁棒性补充\n if (!name) {\n const m2 = content.match(/defineOptions\\s*\\(\\s*\\{[^\\}]*name\\s*:\\s*['\"]([^'\"]+)['\"][^\\}]*\\}\\s*\\)/m);\n if (m2) name = m2[1];\n }\n\n // 兜底:若未从 defineOptions 提取,且开启自动命名,则基于文件路径生成名称\n if (!name && autoGen) {\n name = generateNameFromFilePath(file, viewsDir);\n }\n\n if (name) map[normalizePath(file, viewsDir)] = name;\n }\n\n return map;\n } catch (err) {\n // 分类错误并提供上下文,便于定位问题\n if (err instanceof PluginError) throw err;\n throw new LogicError('Failed to generate view name map', { cause: err });\n }\n}\n\r\n/**\r\n * 在 transform 阶段注入 defineOptions (不写入文件)\r\n */\r\nexport function injectDefineOptions(code: string, id: string, name: string) {\n const {descriptor} = parse(code);\r\n if (!descriptor.scriptSetup) return null;\r\n\r\n const content = descriptor.scriptSetup.content;\r\n const startOffset = descriptor.scriptSetup.loc.start.offset;\r\n\r\n // 使用 ts-morph 分析 scriptSetup 内容\r\n const project = new Project({useInMemoryFileSystem: true});\r\n const sourceFile = project.createSourceFile('temp.ts', content);\r\n\r\n // 检查是否已有 defineOptions\r\n const hasName = !!extractNameFromDefineOptions(sourceFile);\r\n if (hasName) return null;\r\n\r\n const s = new MagicString(code);\r\n const defineOptionsCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)\r\n .find(c => c.getExpression().getText() === 'defineOptions');\r\n\r\n if (defineOptionsCall) {\r\n // 情况 A: 已有 defineOptions 但没写 name 属性\r\n const arg = defineOptionsCall.getArguments()[0];\r\n if (Node.isObjectLiteralExpression(arg)) {\r\n const properties = arg.getProperties();\r\n const pos = startOffset + arg.getStart() + 1; // '{' 之后\r\n\r\n if (properties.length === 0) {\r\n // 空对象直接插入\r\n s.appendRight(pos, ` name: '${name}' `);\r\n } else {\r\n // 已有属性,必须补充逗号分隔\r\n s.appendRight(pos, ` name: '${name}', `);\r\n }\r\n }\r\n } else {\r\n // 情况 B: 完全没有 defineOptions,在 scriptSetup 顶部注入\r\n s.appendLeft(startOffset, `\\ndefineOptions({ name: '${name}' });\\n`);\r\n }\r\n\r\n return {\n code: s.toString(),\n map: s.generateMap({source: id, includeContent: true, hires: true})\n };\n}\n\r\n/**\r\n * 从 defineOptions 宏定义中提取组件 name 属性值\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\n // 获取源文件中所有的调用表达式\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression: Expression = call.getExpression();\r\n // 匹配 defineOptions 调用\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const args = call.getArguments();\r\n if (args.length === 0) continue;\r\n\r\n // 获取第一个对象字面量参数\r\n const obj: ObjectLiteralExpression | undefined = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n // 查找 name 属性并确保其为标准的 key: value 赋值形式\r\n const nameProp = obj.getProperty('name');\r\n if (nameProp && Node.isPropertyAssignment(nameProp)) {\r\n const initializer = nameProp.getInitializer();\r\n if (initializer && Node.isStringLiteral(initializer)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\n\n/**\n * Try to extract name from defineOptions within a Vue SFC's scriptSetup block.\n * It parses the <script setup> content using ts-morph to find defineOptions({ name: '...' }).\n */\nfunction extractNameFromDefineOptionsInSfc(content: string): string | undefined {\n try {\n const {descriptor} = parse(content);\n if (!descriptor.scriptSetup) return undefined;\n const scriptContent: string = descriptor.scriptSetup.content;\n // 直接正则匹配,兜底提升鲁棒性\n const regex = /defineOptions\\s*\\(\\s*\\{\\s*name\\s*:\\s*['\"]([^'\"]+)['\"]\\s*\\}\\s*\\)/;\n const m = scriptContent.match(regex);\n if (m) return m[1];\n\n // 兜底:再尝试通过 ts-morph 解析脚本块\n const project = new Project({useInMemoryFileSystem: true});\n const sourceFile = project.createSourceFile('temp.ts', scriptContent);\n return extractNameFromDefineOptions(sourceFile);\n } catch {\n return undefined;\n }\n}\n\n/**\r\n * 根据文件路径生成组件 name (PascalCase)\r\n * 规则:\r\n * - 保持层级关系\r\n * - 移除 index 后缀\r\n * - 自动处理路径中的横杠、下划线为大驼峰\r\n */\r\nexport function generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const absoluteViewsDir: string = path.resolve(viewsDir);\r\n const relativePath: string = path.relative(absoluteViewsDir, file);\r\n const {dir, name} = path.parse(relativePath);\r\n\r\n // 拆分层级\r\n const segments: string[] = dir ? dir.split(path.sep) : [];\r\n if (name !== 'index') {\r\n segments.push(name);\r\n }\r\n\r\n // 转换每一级为 PascalCase 并拼接\r\n return segments\r\n .map((s: string) => s\r\n // 处理内部横杠或下划线: sys-user -> sysUser\r\n .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())\r\n // 首字母大写: sysUser -> SysUser\r\n .replace(/^[a-z]/, (c: string) => c.toUpperCase())\r\n )\r\n .join('') || 'Index';\r\n}\r\n\r\n/**\r\n * 路径统一处理:生成的 Key 包含 viewsDir 目录名\r\n * 示例:/Users/me/project/src/views/User/Index.vue -> src/views/User/Index.vue\r\n */\r\nfunction normalizePath(file: string, viewsDir: string): string {\r\n // 1. 获取 viewsDir 的基础名称 (例如 'src/views')\r\n // 2. 获取文件相对于 viewsDir 的部分\r\n // 3. 拼接并统一分隔符\r\n const relativePath: string = path.relative(path.resolve(viewsDir), file);\r\n const combinedPath: string = path.join(viewsDir, relativePath);\r\n\r\n return combinedPath.split(path.sep).join('/');\r\n}\r\n","// Centralized plugin error types for better error handling and debugging\n\nexport class PluginError extends Error {\n public details?: any;\n constructor(message: string, details?: any) {\n super(message);\n this.name = 'PluginError';\n this.details = details;\n }\n}\n\nexport class ValidationError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'ValidationError';\n }\n}\n\nexport class IoError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'IoError';\n }\n}\n\nexport class LogicError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'LogicError';\n }\n}\n","import type { ViewNameMap } from './generate';\r\n\r\n/**\r\n * 根据扫描结果生成 virtual module 的源码字符串\r\n *\r\n * 设计说明:\r\n * - 不使用 default export,便于未来扩展更多具名导出\r\n * - 同时更利于 TypeScript module augmentation\r\n */\r\nexport function createVirtualModule(map: ViewNameMap): string {\r\n return `\r\nexport const viewNameMap = ${JSON.stringify(map, null, 2)};\r\n`;\r\n}\r\n"],"mappings":"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","names":["import_path","fg","fs","MagicString","path","path"]}
|
package/dist/index.js
CHANGED
|
@@ -9,22 +9,80 @@ import fg from "fast-glob";
|
|
|
9
9
|
import MagicString from "magic-string";
|
|
10
10
|
import { parse } from "@vue/compiler-sfc";
|
|
11
11
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
12
|
+
|
|
13
|
+
// src/plugin-errors.ts
|
|
14
|
+
var PluginError = class extends Error {
|
|
15
|
+
constructor(message, details) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "PluginError";
|
|
18
|
+
this.details = details;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var ValidationError = class extends PluginError {
|
|
22
|
+
constructor(message, details) {
|
|
23
|
+
super(message, details);
|
|
24
|
+
this.name = "ValidationError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var IoError = class extends PluginError {
|
|
28
|
+
constructor(message, details) {
|
|
29
|
+
super(message, details);
|
|
30
|
+
this.name = "IoError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var LogicError = class extends PluginError {
|
|
34
|
+
constructor(message, details) {
|
|
35
|
+
super(message, details);
|
|
36
|
+
this.name = "LogicError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/generate.ts
|
|
41
|
+
function ensureGenerateOptionsValid(options) {
|
|
42
|
+
const viewsDirAny = options.viewsDir;
|
|
43
|
+
if (viewsDirAny !== void 0 && typeof viewsDirAny !== "string") {
|
|
44
|
+
throw new ValidationError("Invalid type for options.viewsDir; expected string", {
|
|
45
|
+
receivedType: typeof viewsDirAny,
|
|
46
|
+
receivedValue: viewsDirAny
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const autoGenAny = options.autoGenerateName;
|
|
50
|
+
if (autoGenAny !== void 0 && typeof autoGenAny !== "boolean") {
|
|
51
|
+
throw new ValidationError("Invalid type for options.autoGenerateName; expected boolean", {
|
|
52
|
+
receivedType: typeof autoGenAny,
|
|
53
|
+
receivedValue: autoGenAny
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
12
57
|
async function generateViewNameMap(options = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
ensureGenerateOptionsValid(options);
|
|
59
|
+
try {
|
|
60
|
+
const viewsDir = options.viewsDir ?? "src/views";
|
|
61
|
+
const autoGen = options.autoGenerateName ?? true;
|
|
62
|
+
const map = {};
|
|
63
|
+
const files = await fg(["**/*.vue", "**/*.tsx"], { cwd: viewsDir, absolute: true });
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
let content;
|
|
66
|
+
try {
|
|
67
|
+
content = await fs.readFile(file, "utf-8");
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new IoError(`Failed to read file: ${file}`, { cause: err });
|
|
70
|
+
}
|
|
71
|
+
let name = extractNameFromDefineOptionsInSfc(content);
|
|
72
|
+
if (!name) {
|
|
73
|
+
const m2 = content.match(/defineOptions\s*\(\s*\{[^\}]*name\s*:\s*['"]([^'"]+)['"][^\}]*\}\s*\)/m);
|
|
74
|
+
if (m2) name = m2[1];
|
|
75
|
+
}
|
|
76
|
+
if (!name && autoGen) {
|
|
77
|
+
name = generateNameFromFilePath(file, viewsDir);
|
|
78
|
+
}
|
|
79
|
+
if (name) map[normalizePath(file, viewsDir)] = name;
|
|
24
80
|
}
|
|
25
|
-
|
|
81
|
+
return map;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof PluginError) throw err;
|
|
84
|
+
throw new LogicError("Failed to generate view name map", { cause: err });
|
|
26
85
|
}
|
|
27
|
-
return map;
|
|
28
86
|
}
|
|
29
87
|
function injectDefineOptions(code, id, name) {
|
|
30
88
|
const { descriptor } = parse(code);
|
|
@@ -77,6 +135,21 @@ function extractNameFromDefineOptions(sourceFile) {
|
|
|
77
135
|
}
|
|
78
136
|
return void 0;
|
|
79
137
|
}
|
|
138
|
+
function extractNameFromDefineOptionsInSfc(content) {
|
|
139
|
+
try {
|
|
140
|
+
const { descriptor } = parse(content);
|
|
141
|
+
if (!descriptor.scriptSetup) return void 0;
|
|
142
|
+
const scriptContent = descriptor.scriptSetup.content;
|
|
143
|
+
const regex = /defineOptions\s*\(\s*\{\s*name\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/;
|
|
144
|
+
const m = scriptContent.match(regex);
|
|
145
|
+
if (m) return m[1];
|
|
146
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
147
|
+
const sourceFile = project.createSourceFile("temp.ts", scriptContent);
|
|
148
|
+
return extractNameFromDefineOptions(sourceFile);
|
|
149
|
+
} catch {
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
80
153
|
function generateNameFromFilePath(file, viewsDir) {
|
|
81
154
|
const absoluteViewsDir = path.resolve(viewsDir);
|
|
82
155
|
const relativePath = path.relative(absoluteViewsDir, file);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/virtual-module.ts"],"sourcesContent":["import type {Plugin} from 'vite';\r\nimport {normalizePath} from 'vite'; // 必须使用 vite 提供的路径归一化工具\r\nimport path from 'path';\r\nimport {generateViewNameMap, injectDefineOptions, generateNameFromFilePath, ViewNameMap} from './generate';\r\nimport {createVirtualModule} from './virtual-module';\r\n\r\n/**\r\n * 插件配置项\r\n */\r\nexport interface ViewNameMapPluginOptions {\r\n /** Vue 页面根目录,默认 'src/views' */\r\n viewsDir?: string;\r\n /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */\r\n autoGenerateName?: boolean;\r\n /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */\r\n autoInjectName?: boolean;\r\n}\r\n\r\n/**\r\n * vite-plugin-view-name-map\r\n *\r\n * 构建期扫描 views 目录,提取组件 name 并生成 virtual module\r\n */\r\nexport default function viewNameMapPlugin(\r\n options: ViewNameMapPluginOptions = {}\r\n): Plugin {\r\n const virtualId = 'virtual:view-name-map';\r\n const resolvedVirtualId = '\\0' + virtualId;\r\n\r\n // 统一转换为正斜杠路径\r\n const viewsDir = options.viewsDir ?? 'src/views';\r\n const autoInjectName = options.autoInjectName ?? true;\r\n\r\n // 核心约束:如果开启了自动注入,则必须开启自动生成,否则注入没有数据源\r\n const autoGenerateName = autoInjectName ? true : (options.autoGenerateName ?? true);\r\n\r\n const root = normalizePath(process.cwd());\r\n const absoluteViewsDir = normalizePath(path.resolve(root, viewsDir));\r\n\r\n let virtualCode = '';\r\n let nameMap: ViewNameMap = {};\r\n let injectedCount = 0; // 注入计数器\r\n\r\n return {\r\n name: 'vite-plugin-view-name-map',\r\n enforce: 'pre',\r\n\r\n async buildStart(): Promise<void> {\r\n // 初始扫描\r\n nameMap = await generateViewNameMap({\r\n viewsDir: viewsDir,\r\n autoGenerateName: autoGenerateName\r\n });\r\n virtualCode = createVirtualModule(nameMap);\r\n\r\n // 打印初始化统计信息\r\n const count = Object.keys(nameMap).length;\r\n if (count > 0) {\r\n const status = autoInjectName ? '已开启自动注入' : '仅生成映射表';\r\n console.log(`\\x1b[32m[view-name-map] ${status}:扫描到 ${count} 个视图组件。\\x1b[0m`);\r\n }\r\n },\r\n\r\n resolveId(id: string): string | undefined {\r\n if (id === virtualId) return resolvedVirtualId;\r\n return undefined;\r\n },\r\n\r\n load(id: string): string | undefined {\r\n if (id === resolvedVirtualId) return virtualCode;\r\n return undefined;\r\n },\r\n\r\n async transform(code: string, id: string) {\r\n // 只有开启注入时才继续执行 transform 逻辑\r\n if (!autoInjectName) return null;\r\n\r\n const cleanId = normalizePath(id.split('?')[0]);\r\n\r\n // 过滤非目标文件\r\n if (!cleanId.endsWith('.vue') || !cleanId.startsWith(absoluteViewsDir)) {\r\n return null;\r\n }\r\n\r\n // 获取组件名称\r\n const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);\r\n\r\n try {\r\n // 执行 AST 注入\r\n const result = injectDefineOptions(code, cleanId, componentName);\r\n\r\n // 如果返回了结果,说明确实进行了注入(非 null)\r\n if (result) {\r\n injectedCount++;\r\n }\r\n\r\n return result;\r\n } catch (err) {\r\n console.error(`\\x1b[31m[view-name-map] 注入异常: ${cleanId}\\x1b[0m`, err);\r\n return null;\r\n }\r\n },\r\n\r\n // 在构建结束时打印统计信息\r\n buildEnd() {\r\n // 仅在开发/构建完成且有注入行为时输出\r\n if (autoInjectName && injectedCount > 0) {\r\n console.log(`\\x1b[36m[view-name-map] 本次运行已为 ${injectedCount} 个组件注入了 defineOptions({ name: \"...\" })。\\x1b[0m`);\r\n }\r\n }\r\n };\r\n}\r\n","import path from 'path';\r\nimport fs from 'fs/promises';\r\nimport fg from 'fast-glob';\r\nimport MagicString from 'magic-string'; // 需要安装: npm install magic-string\r\nimport {parse} from '@vue/compiler-sfc'; // Vue 自带\r\nimport {CallExpression, Expression, Node, ObjectLiteralExpression, Project, SourceFile, SyntaxKind} from 'ts-morph';\r\n\r\n/** 视图 name 映射表类型 */\r\nexport type ViewNameMap = Record<string, string>;\r\n\r\n/** 构建期生成逻辑配置 */\r\nexport interface GenerateOptions {\r\n viewsDir?: string;\r\n autoGenerateName?: boolean;\r\n}\r\n\r\n/**\r\n * 扫描 views 目录生成组件 name 映射表\r\n */\r\nexport async function generateViewNameMap(\r\n options: GenerateOptions = {}\r\n): Promise<ViewNameMap> {\r\n const viewsDir: string = options.viewsDir ?? 'src/views';\r\n const autoGen: boolean = options.autoGenerateName ?? true;\r\n\r\n const map: ViewNameMap = {};\r\n const files: string[] = await fg(['**/*.vue', '**/*.tsx'], {cwd: viewsDir, absolute: true});\r\n\r\n const project: Project = new Project({useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true});\r\n\r\n for (const file of files) {\r\n const content: string = await fs.readFile(file, 'utf-8');\r\n const sourceFile: SourceFile = project.createSourceFile(file, content, {overwrite: true});\r\n\r\n let name: string | undefined = extractNameFromDefineOptions(sourceFile);\r\n\r\n if (!name && autoGen) {\r\n name = generateNameFromFilePath(file, viewsDir);\r\n }\r\n\r\n // 修改点:传入 viewsDir 进行相对化处理\r\n if (name) map[normalizePath(file, viewsDir)] = name;\r\n }\r\n\r\n return map;\r\n}\r\n\r\n/**\r\n * 在 transform 阶段注入 defineOptions (不写入文件)\r\n */\r\nexport function injectDefineOptions(code: string, id: string, name: string) {\r\n const {descriptor} = parse(code);\r\n if (!descriptor.scriptSetup) return null;\r\n\r\n const content = descriptor.scriptSetup.content;\r\n const startOffset = descriptor.scriptSetup.loc.start.offset;\r\n\r\n // 使用 ts-morph 分析 scriptSetup 内容\r\n const project = new Project({useInMemoryFileSystem: true});\r\n const sourceFile = project.createSourceFile('temp.ts', content);\r\n\r\n // 检查是否已有 defineOptions\r\n const hasName = !!extractNameFromDefineOptions(sourceFile);\r\n if (hasName) return null;\r\n\r\n const s = new MagicString(code);\r\n const defineOptionsCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)\r\n .find(c => c.getExpression().getText() === 'defineOptions');\r\n\r\n if (defineOptionsCall) {\r\n // 情况 A: 已有 defineOptions 但没写 name 属性\r\n const arg = defineOptionsCall.getArguments()[0];\r\n if (Node.isObjectLiteralExpression(arg)) {\r\n const properties = arg.getProperties();\r\n const pos = startOffset + arg.getStart() + 1; // '{' 之后\r\n\r\n if (properties.length === 0) {\r\n // 空对象直接插入\r\n s.appendRight(pos, ` name: '${name}' `);\r\n } else {\r\n // 已有属性,必须补充逗号分隔\r\n s.appendRight(pos, ` name: '${name}', `);\r\n }\r\n }\r\n } else {\r\n // 情况 B: 完全没有 defineOptions,在 scriptSetup 顶部注入\r\n s.appendLeft(startOffset, `\\ndefineOptions({ name: '${name}' });\\n`);\r\n }\r\n\r\n return {\r\n code: s.toString(),\r\n map: s.generateMap({source: id, includeContent: true, hires: true})\r\n };\r\n}\r\n\r\n/**\r\n * 从 defineOptions 宏定义中提取组件 name 属性值\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\r\n // 获取源文件中所有的调用表达式\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression: Expression = call.getExpression();\r\n // 匹配 defineOptions 调用\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const args = call.getArguments();\r\n if (args.length === 0) continue;\r\n\r\n // 获取第一个对象字面量参数\r\n const obj: ObjectLiteralExpression | undefined = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n // 查找 name 属性并确保其为标准的 key: value 赋值形式\r\n const nameProp = obj.getProperty('name');\r\n if (nameProp && Node.isPropertyAssignment(nameProp)) {\r\n const initializer = nameProp.getInitializer();\r\n if (initializer && Node.isStringLiteral(initializer)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * 根据文件路径生成组件 name (PascalCase)\r\n * 规则:\r\n * - 保持层级关系\r\n * - 移除 index 后缀\r\n * - 自动处理路径中的横杠、下划线为大驼峰\r\n */\r\nexport function generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const absoluteViewsDir: string = path.resolve(viewsDir);\r\n const relativePath: string = path.relative(absoluteViewsDir, file);\r\n const {dir, name} = path.parse(relativePath);\r\n\r\n // 拆分层级\r\n const segments: string[] = dir ? dir.split(path.sep) : [];\r\n if (name !== 'index') {\r\n segments.push(name);\r\n }\r\n\r\n // 转换每一级为 PascalCase 并拼接\r\n return segments\r\n .map((s: string) => s\r\n // 处理内部横杠或下划线: sys-user -> sysUser\r\n .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())\r\n // 首字母大写: sysUser -> SysUser\r\n .replace(/^[a-z]/, (c: string) => c.toUpperCase())\r\n )\r\n .join('') || 'Index';\r\n}\r\n\r\n/**\r\n * 路径统一处理:生成的 Key 包含 viewsDir 目录名\r\n * 示例:/Users/me/project/src/views/User/Index.vue -> src/views/User/Index.vue\r\n */\r\nfunction normalizePath(file: string, viewsDir: string): string {\r\n // 1. 获取 viewsDir 的基础名称 (例如 'src/views')\r\n // 2. 获取文件相对于 viewsDir 的部分\r\n // 3. 拼接并统一分隔符\r\n const relativePath: string = path.relative(path.resolve(viewsDir), file);\r\n const combinedPath: string = path.join(viewsDir, relativePath);\r\n\r\n return combinedPath.split(path.sep).join('/');\r\n}\r\n","import type { ViewNameMap } from './generate';\r\n\r\n/**\r\n * 根据扫描结果生成 virtual module 的源码字符串\r\n *\r\n * 设计说明:\r\n * - 不使用 default export,便于未来扩展更多具名导出\r\n * - 同时更利于 TypeScript module augmentation\r\n */\r\nexport function createVirtualModule(map: ViewNameMap): string {\r\n return `\r\nexport const viewNameMap = ${JSON.stringify(map, null, 2)};\r\n`;\r\n}\r\n"],"mappings":"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","names":["normalizePath","path","normalizePath","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/plugin-errors.ts","../src/virtual-module.ts"],"sourcesContent":["import type {Plugin} from 'vite';\r\nimport {normalizePath} from 'vite'; // 必须使用 vite 提供的路径归一化工具\r\nimport path from 'path';\r\nimport {generateViewNameMap, injectDefineOptions, generateNameFromFilePath, ViewNameMap} from './generate';\r\nimport {createVirtualModule} from './virtual-module';\r\n\r\n/**\r\n * 插件配置项\r\n */\r\nexport interface ViewNameMapPluginOptions {\r\n /** Vue 页面根目录,默认 'src/views' */\r\n viewsDir?: string;\r\n /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */\r\n autoGenerateName?: boolean;\r\n /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */\r\n autoInjectName?: boolean;\r\n}\r\n\r\n/**\r\n * vite-plugin-view-name-map\r\n *\r\n * 构建期扫描 views 目录,提取组件 name 并生成 virtual module\r\n */\r\nexport default function viewNameMapPlugin(\r\n options: ViewNameMapPluginOptions = {}\r\n): Plugin {\r\n const virtualId = 'virtual:view-name-map';\r\n const resolvedVirtualId = '\\0' + virtualId;\r\n\r\n // 统一转换为正斜杠路径\r\n const viewsDir = options.viewsDir ?? 'src/views';\r\n const autoInjectName = options.autoInjectName ?? true;\r\n\r\n // 核心约束:如果开启了自动注入,则必须开启自动生成,否则注入没有数据源\r\n const autoGenerateName = autoInjectName ? true : (options.autoGenerateName ?? true);\r\n\r\n const root = normalizePath(process.cwd());\r\n const absoluteViewsDir = normalizePath(path.resolve(root, viewsDir));\r\n\r\n let virtualCode = '';\r\n let nameMap: ViewNameMap = {};\r\n let injectedCount = 0; // 注入计数器\r\n\r\n return {\r\n name: 'vite-plugin-view-name-map',\r\n enforce: 'pre',\r\n\r\n async buildStart(): Promise<void> {\r\n // 初始扫描\r\n nameMap = await generateViewNameMap({\r\n viewsDir: viewsDir,\r\n autoGenerateName: autoGenerateName\r\n });\r\n virtualCode = createVirtualModule(nameMap);\r\n\r\n // 打印初始化统计信息\r\n const count = Object.keys(nameMap).length;\r\n if (count > 0) {\r\n const status = autoInjectName ? '已开启自动注入' : '仅生成映射表';\r\n console.log(`\\x1b[32m[view-name-map] ${status}:扫描到 ${count} 个视图组件。\\x1b[0m`);\r\n }\r\n },\r\n\r\n resolveId(id: string): string | undefined {\r\n if (id === virtualId) return resolvedVirtualId;\r\n return undefined;\r\n },\r\n\r\n load(id: string): string | undefined {\r\n if (id === resolvedVirtualId) return virtualCode;\r\n return undefined;\r\n },\r\n\r\n async transform(code: string, id: string) {\r\n // 只有开启注入时才继续执行 transform 逻辑\r\n if (!autoInjectName) return null;\r\n\r\n const cleanId = normalizePath(id.split('?')[0]);\r\n\r\n // 过滤非目标文件\r\n if (!cleanId.endsWith('.vue') || !cleanId.startsWith(absoluteViewsDir)) {\r\n return null;\r\n }\r\n\r\n // 获取组件名称\r\n const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);\r\n\r\n try {\r\n // 执行 AST 注入\r\n const result = injectDefineOptions(code, cleanId, componentName);\r\n\r\n // 如果返回了结果,说明确实进行了注入(非 null)\r\n if (result) {\r\n injectedCount++;\r\n }\r\n\r\n return result;\r\n } catch (err) {\r\n console.error(`\\x1b[31m[view-name-map] 注入异常: ${cleanId}\\x1b[0m`, err);\r\n return null;\r\n }\r\n },\r\n\r\n // 在构建结束时打印统计信息\r\n buildEnd() {\r\n // 仅在开发/构建完成且有注入行为时输出\r\n if (autoInjectName && injectedCount > 0) {\r\n console.log(`\\x1b[36m[view-name-map] 本次运行已为 ${injectedCount} 个组件注入了 defineOptions({ name: \"...\" })。\\x1b[0m`);\r\n }\r\n }\r\n };\r\n}\r\n","import path from 'path';\nimport fs from 'fs/promises';\nimport fg from 'fast-glob';\nimport MagicString from 'magic-string'; // 需要安装: npm install magic-string\nimport {parse} from '@vue/compiler-sfc'; // Vue 自带\nimport {CallExpression, Expression, Node, ObjectLiteralExpression, Project, SourceFile, SyntaxKind} from 'ts-morph';\nimport {PluginError, ValidationError, IoError, LogicError} from './plugin-errors';\n\r\n/** 视图 name 映射表类型 */\r\nexport type ViewNameMap = Record<string, string>;\r\n\r\n/** 构建期生成逻辑配置 */\r\nexport interface GenerateOptions {\n viewsDir?: string;\n autoGenerateName?: boolean;\n}\n\n// 内部的配置校验,确保输入类型及字段符合期望,降低运行时错误\nfunction ensureGenerateOptionsValid(options: GenerateOptions): void {\n // Runtime type checks using loose typing to avoid TypeScript narrowing pitfalls\n const viewsDirAny: any = (options as any).viewsDir;\n if (viewsDirAny !== undefined && typeof viewsDirAny !== 'string') {\n throw new ValidationError('Invalid type for options.viewsDir; expected string', {\n receivedType: typeof viewsDirAny,\n receivedValue: viewsDirAny,\n });\n }\n const autoGenAny: any = (options as any).autoGenerateName;\n if (autoGenAny !== undefined && typeof autoGenAny !== 'boolean') {\n throw new ValidationError('Invalid type for options.autoGenerateName; expected boolean', {\n receivedType: typeof autoGenAny,\n receivedValue: autoGenAny,\n });\n }\n}\n\r\n/**\r\n * 扫描 views 目录生成组件 name 映射表\r\n */\r\nexport async function generateViewNameMap(\n options: GenerateOptions = {}\n): Promise<ViewNameMap> {\n // 参数校验,尽早抛出可追踪的错误\n ensureGenerateOptionsValid(options);\n\n try {\n const viewsDir: string = options.viewsDir ?? 'src/views';\n const autoGen: boolean = options.autoGenerateName ?? true;\n\n const map: ViewNameMap = {};\n const files: string[] = await fg(['**/*.vue', '**/*.tsx'], {cwd: viewsDir, absolute: true});\n\n for (const file of files) {\n let content: string;\n try {\n content = await fs.readFile(file, 'utf-8');\n } catch (err) {\n throw new IoError(`Failed to read file: ${file}`, { cause: err });\n }\n\n // 优先尝试从 Vue SFC 的 scriptSetup 提取 defineOptions.name\n let name: string | undefined = extractNameFromDefineOptionsInSfc(content);\n // 兜底:更直接的全文搜索作为额外鲁棒性补充\n if (!name) {\n const m2 = content.match(/defineOptions\\s*\\(\\s*\\{[^\\}]*name\\s*:\\s*['\"]([^'\"]+)['\"][^\\}]*\\}\\s*\\)/m);\n if (m2) name = m2[1];\n }\n\n // 兜底:若未从 defineOptions 提取,且开启自动命名,则基于文件路径生成名称\n if (!name && autoGen) {\n name = generateNameFromFilePath(file, viewsDir);\n }\n\n if (name) map[normalizePath(file, viewsDir)] = name;\n }\n\n return map;\n } catch (err) {\n // 分类错误并提供上下文,便于定位问题\n if (err instanceof PluginError) throw err;\n throw new LogicError('Failed to generate view name map', { cause: err });\n }\n}\n\r\n/**\r\n * 在 transform 阶段注入 defineOptions (不写入文件)\r\n */\r\nexport function injectDefineOptions(code: string, id: string, name: string) {\n const {descriptor} = parse(code);\r\n if (!descriptor.scriptSetup) return null;\r\n\r\n const content = descriptor.scriptSetup.content;\r\n const startOffset = descriptor.scriptSetup.loc.start.offset;\r\n\r\n // 使用 ts-morph 分析 scriptSetup 内容\r\n const project = new Project({useInMemoryFileSystem: true});\r\n const sourceFile = project.createSourceFile('temp.ts', content);\r\n\r\n // 检查是否已有 defineOptions\r\n const hasName = !!extractNameFromDefineOptions(sourceFile);\r\n if (hasName) return null;\r\n\r\n const s = new MagicString(code);\r\n const defineOptionsCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)\r\n .find(c => c.getExpression().getText() === 'defineOptions');\r\n\r\n if (defineOptionsCall) {\r\n // 情况 A: 已有 defineOptions 但没写 name 属性\r\n const arg = defineOptionsCall.getArguments()[0];\r\n if (Node.isObjectLiteralExpression(arg)) {\r\n const properties = arg.getProperties();\r\n const pos = startOffset + arg.getStart() + 1; // '{' 之后\r\n\r\n if (properties.length === 0) {\r\n // 空对象直接插入\r\n s.appendRight(pos, ` name: '${name}' `);\r\n } else {\r\n // 已有属性,必须补充逗号分隔\r\n s.appendRight(pos, ` name: '${name}', `);\r\n }\r\n }\r\n } else {\r\n // 情况 B: 完全没有 defineOptions,在 scriptSetup 顶部注入\r\n s.appendLeft(startOffset, `\\ndefineOptions({ name: '${name}' });\\n`);\r\n }\r\n\r\n return {\n code: s.toString(),\n map: s.generateMap({source: id, includeContent: true, hires: true})\n };\n}\n\r\n/**\r\n * 从 defineOptions 宏定义中提取组件 name 属性值\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\n // 获取源文件中所有的调用表达式\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression: Expression = call.getExpression();\r\n // 匹配 defineOptions 调用\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const args = call.getArguments();\r\n if (args.length === 0) continue;\r\n\r\n // 获取第一个对象字面量参数\r\n const obj: ObjectLiteralExpression | undefined = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n // 查找 name 属性并确保其为标准的 key: value 赋值形式\r\n const nameProp = obj.getProperty('name');\r\n if (nameProp && Node.isPropertyAssignment(nameProp)) {\r\n const initializer = nameProp.getInitializer();\r\n if (initializer && Node.isStringLiteral(initializer)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\n\n/**\n * Try to extract name from defineOptions within a Vue SFC's scriptSetup block.\n * It parses the <script setup> content using ts-morph to find defineOptions({ name: '...' }).\n */\nfunction extractNameFromDefineOptionsInSfc(content: string): string | undefined {\n try {\n const {descriptor} = parse(content);\n if (!descriptor.scriptSetup) return undefined;\n const scriptContent: string = descriptor.scriptSetup.content;\n // 直接正则匹配,兜底提升鲁棒性\n const regex = /defineOptions\\s*\\(\\s*\\{\\s*name\\s*:\\s*['\"]([^'\"]+)['\"]\\s*\\}\\s*\\)/;\n const m = scriptContent.match(regex);\n if (m) return m[1];\n\n // 兜底:再尝试通过 ts-morph 解析脚本块\n const project = new Project({useInMemoryFileSystem: true});\n const sourceFile = project.createSourceFile('temp.ts', scriptContent);\n return extractNameFromDefineOptions(sourceFile);\n } catch {\n return undefined;\n }\n}\n\n/**\r\n * 根据文件路径生成组件 name (PascalCase)\r\n * 规则:\r\n * - 保持层级关系\r\n * - 移除 index 后缀\r\n * - 自动处理路径中的横杠、下划线为大驼峰\r\n */\r\nexport function generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const absoluteViewsDir: string = path.resolve(viewsDir);\r\n const relativePath: string = path.relative(absoluteViewsDir, file);\r\n const {dir, name} = path.parse(relativePath);\r\n\r\n // 拆分层级\r\n const segments: string[] = dir ? dir.split(path.sep) : [];\r\n if (name !== 'index') {\r\n segments.push(name);\r\n }\r\n\r\n // 转换每一级为 PascalCase 并拼接\r\n return segments\r\n .map((s: string) => s\r\n // 处理内部横杠或下划线: sys-user -> sysUser\r\n .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())\r\n // 首字母大写: sysUser -> SysUser\r\n .replace(/^[a-z]/, (c: string) => c.toUpperCase())\r\n )\r\n .join('') || 'Index';\r\n}\r\n\r\n/**\r\n * 路径统一处理:生成的 Key 包含 viewsDir 目录名\r\n * 示例:/Users/me/project/src/views/User/Index.vue -> src/views/User/Index.vue\r\n */\r\nfunction normalizePath(file: string, viewsDir: string): string {\r\n // 1. 获取 viewsDir 的基础名称 (例如 'src/views')\r\n // 2. 获取文件相对于 viewsDir 的部分\r\n // 3. 拼接并统一分隔符\r\n const relativePath: string = path.relative(path.resolve(viewsDir), file);\r\n const combinedPath: string = path.join(viewsDir, relativePath);\r\n\r\n return combinedPath.split(path.sep).join('/');\r\n}\r\n","// Centralized plugin error types for better error handling and debugging\n\nexport class PluginError extends Error {\n public details?: any;\n constructor(message: string, details?: any) {\n super(message);\n this.name = 'PluginError';\n this.details = details;\n }\n}\n\nexport class ValidationError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'ValidationError';\n }\n}\n\nexport class IoError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'IoError';\n }\n}\n\nexport class LogicError extends PluginError {\n constructor(message: string, details?: any) {\n super(message, details);\n this.name = 'LogicError';\n }\n}\n","import type { ViewNameMap } from './generate';\r\n\r\n/**\r\n * 根据扫描结果生成 virtual module 的源码字符串\r\n *\r\n * 设计说明:\r\n * - 不使用 default export,便于未来扩展更多具名导出\r\n * - 同时更利于 TypeScript module augmentation\r\n */\r\nexport function createVirtualModule(map: ViewNameMap): string {\r\n return `\r\nexport const viewNameMap = ${JSON.stringify(map, null, 2)};\r\n`;\r\n}\r\n"],"mappings":"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","names":["normalizePath","path","normalizePath","path"]}
|
package/package.json
CHANGED
|
@@ -1,50 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@v-long/vite-plugin-view-name-map",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "Vite plugin to generate view-name-map from Vue SFC defineOptions",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"vite",
|
|
8
|
-
"vue",
|
|
9
|
-
"defineOptions",
|
|
10
|
-
"keep-alive"
|
|
11
|
-
],
|
|
12
|
-
"author": "v-long",
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"main": "./dist/index.cjs",
|
|
15
|
-
"module": "./dist/index.js",
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"files": [
|
|
18
|
-
"dist",
|
|
19
|
-
"types"
|
|
20
|
-
],
|
|
21
|
-
"exports": {
|
|
22
|
-
".": {
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"import": "./dist/index.js",
|
|
25
|
-
"require": "./dist/index.cjs"
|
|
26
|
-
},
|
|
27
|
-
"./client": {
|
|
28
|
-
"types": "./dist/
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"peerDependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"fast-glob": "^3.3.0",
|
|
37
|
-
"magic-string": "^0.30.21",
|
|
38
|
-
"ts-morph": "^20.0.0"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/node": "^25.2.2",
|
|
42
|
-
"@vue/compiler-sfc": "^3.5.28",
|
|
43
|
-
"tsup": "^8.0.0",
|
|
44
|
-
"typescript": "^5.0.0"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@v-long/vite-plugin-view-name-map",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Vite plugin to generate view-name-map from Vue SFC defineOptions",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vite",
|
|
8
|
+
"vue",
|
|
9
|
+
"defineOptions",
|
|
10
|
+
"keep-alive"
|
|
11
|
+
],
|
|
12
|
+
"author": "v-long",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"types"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/client.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@vue/compiler-sfc": "^3.0.0",
|
|
33
|
+
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"fast-glob": "^3.3.0",
|
|
37
|
+
"magic-string": "^0.30.21",
|
|
38
|
+
"ts-morph": "^20.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.2.2",
|
|
42
|
+
"@vue/compiler-sfc": "^3.5.28",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"prepublishOnly": "pnpm run build"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
File without changes
|