@v-long/vite-plugin-view-name-map 0.1.4 → 0.1.6

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/index.cjs CHANGED
@@ -33,11 +33,15 @@ __export(index_exports, {
33
33
  default: () => viewNameMapPlugin
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
+ var import_vite = require("vite");
37
+ var import_path2 = __toESM(require("path"), 1);
36
38
 
37
39
  // src/generate.ts
38
40
  var import_path = __toESM(require("path"), 1);
39
41
  var import_promises = __toESM(require("fs/promises"), 1);
40
42
  var import_fast_glob = __toESM(require("fast-glob"), 1);
43
+ var import_magic_string = __toESM(require("magic-string"), 1);
44
+ var import_compiler_sfc = require("@vue/compiler-sfc");
41
45
  var import_ts_morph = require("ts-morph");
42
46
  async function generateViewNameMap(options = {}) {
43
47
  const viewsDir = options.viewsDir ?? "src/views";
@@ -52,38 +56,77 @@ async function generateViewNameMap(options = {}) {
52
56
  if (!name && autoGen) {
53
57
  name = generateNameFromFilePath(file, viewsDir);
54
58
  }
55
- if (name) map[normalizePath(file)] = name;
59
+ if (name) map[normalizePath(file, viewsDir)] = name;
56
60
  }
57
61
  return map;
58
62
  }
63
+ function injectDefineOptions(code, id, name) {
64
+ const { descriptor } = (0, import_compiler_sfc.parse)(code);
65
+ if (!descriptor.scriptSetup) return null;
66
+ const content = descriptor.scriptSetup.content;
67
+ const startOffset = descriptor.scriptSetup.loc.start.offset;
68
+ const project = new import_ts_morph.Project({ useInMemoryFileSystem: true });
69
+ const sourceFile = project.createSourceFile("temp.ts", content);
70
+ const hasName = !!extractNameFromDefineOptions(sourceFile);
71
+ if (hasName) return null;
72
+ const s = new import_magic_string.default(code);
73
+ const defineOptionsCall = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineOptions");
74
+ if (defineOptionsCall) {
75
+ const arg = defineOptionsCall.getArguments()[0];
76
+ if (import_ts_morph.Node.isObjectLiteralExpression(arg)) {
77
+ const properties = arg.getProperties();
78
+ const pos = startOffset + arg.getStart() + 1;
79
+ if (properties.length === 0) {
80
+ s.appendRight(pos, ` name: '${name}' `);
81
+ } else {
82
+ s.appendRight(pos, ` name: '${name}', `);
83
+ }
84
+ }
85
+ } else {
86
+ s.appendLeft(startOffset, `
87
+ defineOptions({ name: '${name}' });
88
+ `);
89
+ }
90
+ return {
91
+ code: s.toString(),
92
+ map: s.generateMap({ source: id, includeContent: true, hires: true })
93
+ };
94
+ }
59
95
  function extractNameFromDefineOptions(sourceFile) {
60
96
  const calls = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
61
97
  for (const call of calls) {
62
98
  const expression = call.getExpression();
63
99
  if (!expression || expression.getText() !== "defineOptions") continue;
64
- const arg = call.getArguments()[0];
65
- if (!arg || !arg.asKind) continue;
66
- const obj = arg.asKind(import_ts_morph.SyntaxKind.ObjectLiteralExpression);
100
+ const args = call.getArguments();
101
+ if (args.length === 0) continue;
102
+ const obj = args[0]?.asKind(import_ts_morph.SyntaxKind.ObjectLiteralExpression);
67
103
  if (!obj) continue;
68
104
  const nameProp = obj.getProperty("name");
69
- if (!nameProp || !("getInitializer" in nameProp)) continue;
70
- const initializer = nameProp.getInitializer?.();
71
- if (!initializer || !initializer.isKind) continue;
72
- if (initializer.isKind(import_ts_morph.SyntaxKind.StringLiteral)) {
73
- return initializer.getLiteralText();
105
+ if (nameProp && import_ts_morph.Node.isPropertyAssignment(nameProp)) {
106
+ const initializer = nameProp.getInitializer();
107
+ if (initializer && import_ts_morph.Node.isStringLiteral(initializer)) {
108
+ return initializer.getLiteralText();
109
+ }
74
110
  }
75
111
  }
76
112
  return void 0;
77
113
  }
78
114
  function generateNameFromFilePath(file, viewsDir) {
79
- const relative = import_path.default.relative(viewsDir, file);
80
- const parsed = import_path.default.parse(relative);
81
- let name = parsed.name === "index" ? parsed.dir.split(import_path.default.sep).pop() ?? "Unknown" : parsed.name;
82
- name = name.replace(/(^|[-_\/\\])([a-z])/g, (_, __, c) => c.toUpperCase());
83
- return name;
115
+ const absoluteViewsDir = import_path.default.resolve(viewsDir);
116
+ const relativePath = import_path.default.relative(absoluteViewsDir, file);
117
+ const { dir, name } = import_path.default.parse(relativePath);
118
+ const segments = dir ? dir.split(import_path.default.sep) : [];
119
+ if (name !== "index") {
120
+ segments.push(name);
121
+ }
122
+ return segments.map(
123
+ (s) => s.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase())
124
+ ).join("") || "Index";
84
125
  }
85
- function normalizePath(p) {
86
- return p.split(import_path.default.sep).join("/");
126
+ function normalizePath(file, viewsDir) {
127
+ const relativePath = import_path.default.relative(import_path.default.resolve(viewsDir), file);
128
+ const combinedPath = import_path.default.join(viewsDir, relativePath);
129
+ return combinedPath.split(import_path.default.sep).join("/");
87
130
  }
88
131
 
89
132
  // src/virtual-module.ts
@@ -97,24 +140,60 @@ export const viewNameMap = ${JSON.stringify(map, null, 2)};
97
140
  function viewNameMapPlugin(options = {}) {
98
141
  const virtualId = "virtual:view-name-map";
99
142
  const resolvedVirtualId = "\0" + virtualId;
100
- let code = "";
143
+ const viewsDir = options.viewsDir ?? "src/views";
144
+ const autoInjectName = options.autoInjectName ?? true;
145
+ const autoGenerateName = autoInjectName ? true : options.autoGenerateName ?? true;
146
+ const root = (0, import_vite.normalizePath)(process.cwd());
147
+ const absoluteViewsDir = (0, import_vite.normalizePath)(import_path2.default.resolve(root, viewsDir));
148
+ let virtualCode = "";
149
+ let nameMap = {};
150
+ let injectedCount = 0;
101
151
  return {
102
152
  name: "vite-plugin-view-name-map",
103
153
  enforce: "pre",
104
154
  async buildStart() {
105
- const map = await generateViewNameMap({
106
- viewsDir: options.viewsDir,
107
- autoGenerateName: options.autoGenerateName ?? true
155
+ nameMap = await generateViewNameMap({
156
+ viewsDir,
157
+ autoGenerateName
108
158
  });
109
- code = createVirtualModule(map);
159
+ virtualCode = createVirtualModule(nameMap);
160
+ const count = Object.keys(nameMap).length;
161
+ if (count > 0) {
162
+ const status = autoInjectName ? "\u5DF2\u5F00\u542F\u81EA\u52A8\u6CE8\u5165" : "\u4EC5\u751F\u6210\u6620\u5C04\u8868";
163
+ console.log(`\x1B[32m[view-name-map] ${status}\uFF1A\u626B\u63CF\u5230 ${count} \u4E2A\u89C6\u56FE\u7EC4\u4EF6\u3002\x1B[0m`);
164
+ }
110
165
  },
111
166
  resolveId(id) {
112
167
  if (id === virtualId) return resolvedVirtualId;
113
168
  return void 0;
114
169
  },
115
170
  load(id) {
116
- if (id === resolvedVirtualId) return code;
171
+ if (id === resolvedVirtualId) return virtualCode;
117
172
  return void 0;
173
+ },
174
+ async transform(code, id) {
175
+ if (!autoInjectName) return null;
176
+ const cleanId = (0, import_vite.normalizePath)(id.split("?")[0]);
177
+ if (!cleanId.endsWith(".vue") || !cleanId.startsWith(absoluteViewsDir)) {
178
+ return null;
179
+ }
180
+ const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);
181
+ try {
182
+ const result = injectDefineOptions(code, cleanId, componentName);
183
+ if (result) {
184
+ injectedCount++;
185
+ }
186
+ return result;
187
+ } catch (err) {
188
+ console.error(`\x1B[31m[view-name-map] \u6CE8\u5165\u5F02\u5E38: ${cleanId}\x1B[0m`, err);
189
+ return null;
190
+ }
191
+ },
192
+ // 在构建结束时打印统计信息
193
+ buildEnd() {
194
+ if (autoInjectName && injectedCount > 0) {
195
+ console.log(`\x1B[36m[view-name-map] \u672C\u6B21\u8FD0\u884C\u5DF2\u4E3A ${injectedCount} \u4E2A\u7EC4\u4EF6\u6CE8\u5165\u4E86 defineOptions({ name: "..." })\u3002\x1B[0m`);
196
+ }
118
197
  }
119
198
  };
120
199
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/virtual-module.ts"],"sourcesContent":["import type { Plugin } from 'vite';\r\nimport { generateViewNameMap, 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}\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 let code = '';\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 const map: ViewNameMap = await generateViewNameMap({\r\n viewsDir: options.viewsDir,\r\n autoGenerateName: options.autoGenerateName ?? true\r\n });\r\n code = createVirtualModule(map);\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 code;\r\n return undefined;\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 { Project, SourceFile, SyntaxKind, ObjectLiteralExpression, CallExpression } 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 if (name) map[normalizePath(file)] = name;\r\n }\r\n\r\n return map;\r\n}\r\n\r\n/**\r\n * 从 defineOptions({ name }) 中提取组件 name\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression = call.getExpression();\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const arg = call.getArguments()[0];\r\n if (!arg || !arg.asKind) continue;\r\n\r\n const obj: ObjectLiteralExpression | undefined = arg.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n const nameProp = obj.getProperty('name');\r\n if (!nameProp || !('getInitializer' in nameProp)) continue;\r\n\r\n const initializer = nameProp.getInitializer?.();\r\n if (!initializer || !initializer.isKind) continue;\r\n\r\n if (initializer.isKind(SyntaxKind.StringLiteral)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * 根据文件路径生成组件 name\r\n *\r\n * 规则:\r\n * - 文件名为 index 时取父目录名\r\n * - 首字母大写,支持驼峰\r\n */\r\nfunction generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const relative: string = path.relative(viewsDir, file);\r\n const parsed = path.parse(relative);\r\n\r\n let name: string = parsed.name === 'index' ? parsed.dir.split(path.sep).pop() ?? 'Unknown' : parsed.name;\r\n // 驼峰化处理\r\n name = name.replace(/(^|[-_\\/\\\\])([a-z])/g, (_, __, c) => c.toUpperCase());\r\n\r\n return name;\r\n}\r\n\r\n/** 路径统一分隔符 */\r\nfunction normalizePath(p: string): string {\r\n return p.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":["fg","fs","path"]}
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":["import_path","fg","fs","MagicString","path","path"]}
package/dist/index.d.cts CHANGED
@@ -6,8 +6,10 @@ import { Plugin } from 'vite';
6
6
  interface ViewNameMapPluginOptions {
7
7
  /** Vue 页面根目录,默认 'src/views' */
8
8
  viewsDir?: string;
9
- /** 是否自动生成未声明 name 的组件 name,默认 true */
9
+ /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */
10
10
  autoGenerateName?: boolean;
11
+ /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */
12
+ autoInjectName?: boolean;
11
13
  }
12
14
  /**
13
15
  * vite-plugin-view-name-map
package/dist/index.d.ts CHANGED
@@ -6,8 +6,10 @@ import { Plugin } from 'vite';
6
6
  interface ViewNameMapPluginOptions {
7
7
  /** Vue 页面根目录,默认 'src/views' */
8
8
  viewsDir?: string;
9
- /** 是否自动生成未声明 name 的组件 name,默认 true */
9
+ /** 是否自动生成未声明 name 的组件 name(影响虚拟模块映射表),默认 true */
10
10
  autoGenerateName?: boolean;
11
+ /** 是否自动注入 defineOptions 到组件中(影响运行时组件 name),默认 true */
12
+ autoInjectName?: boolean;
11
13
  }
12
14
  /**
13
15
  * vite-plugin-view-name-map
package/dist/index.js CHANGED
@@ -1,8 +1,14 @@
1
+ // src/index.ts
2
+ import { normalizePath as normalizePath2 } from "vite";
3
+ import path2 from "path";
4
+
1
5
  // src/generate.ts
2
6
  import path from "path";
3
7
  import fs from "fs/promises";
4
8
  import fg from "fast-glob";
5
- import { Project, SyntaxKind } from "ts-morph";
9
+ import MagicString from "magic-string";
10
+ import { parse } from "@vue/compiler-sfc";
11
+ import { Node, Project, SyntaxKind } from "ts-morph";
6
12
  async function generateViewNameMap(options = {}) {
7
13
  const viewsDir = options.viewsDir ?? "src/views";
8
14
  const autoGen = options.autoGenerateName ?? true;
@@ -16,38 +22,77 @@ async function generateViewNameMap(options = {}) {
16
22
  if (!name && autoGen) {
17
23
  name = generateNameFromFilePath(file, viewsDir);
18
24
  }
19
- if (name) map[normalizePath(file)] = name;
25
+ if (name) map[normalizePath(file, viewsDir)] = name;
20
26
  }
21
27
  return map;
22
28
  }
29
+ function injectDefineOptions(code, id, name) {
30
+ const { descriptor } = parse(code);
31
+ if (!descriptor.scriptSetup) return null;
32
+ const content = descriptor.scriptSetup.content;
33
+ const startOffset = descriptor.scriptSetup.loc.start.offset;
34
+ const project = new Project({ useInMemoryFileSystem: true });
35
+ const sourceFile = project.createSourceFile("temp.ts", content);
36
+ const hasName = !!extractNameFromDefineOptions(sourceFile);
37
+ if (hasName) return null;
38
+ const s = new MagicString(code);
39
+ const defineOptionsCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineOptions");
40
+ if (defineOptionsCall) {
41
+ const arg = defineOptionsCall.getArguments()[0];
42
+ if (Node.isObjectLiteralExpression(arg)) {
43
+ const properties = arg.getProperties();
44
+ const pos = startOffset + arg.getStart() + 1;
45
+ if (properties.length === 0) {
46
+ s.appendRight(pos, ` name: '${name}' `);
47
+ } else {
48
+ s.appendRight(pos, ` name: '${name}', `);
49
+ }
50
+ }
51
+ } else {
52
+ s.appendLeft(startOffset, `
53
+ defineOptions({ name: '${name}' });
54
+ `);
55
+ }
56
+ return {
57
+ code: s.toString(),
58
+ map: s.generateMap({ source: id, includeContent: true, hires: true })
59
+ };
60
+ }
23
61
  function extractNameFromDefineOptions(sourceFile) {
24
62
  const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
25
63
  for (const call of calls) {
26
64
  const expression = call.getExpression();
27
65
  if (!expression || expression.getText() !== "defineOptions") continue;
28
- const arg = call.getArguments()[0];
29
- if (!arg || !arg.asKind) continue;
30
- const obj = arg.asKind(SyntaxKind.ObjectLiteralExpression);
66
+ const args = call.getArguments();
67
+ if (args.length === 0) continue;
68
+ const obj = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
31
69
  if (!obj) continue;
32
70
  const nameProp = obj.getProperty("name");
33
- if (!nameProp || !("getInitializer" in nameProp)) continue;
34
- const initializer = nameProp.getInitializer?.();
35
- if (!initializer || !initializer.isKind) continue;
36
- if (initializer.isKind(SyntaxKind.StringLiteral)) {
37
- return initializer.getLiteralText();
71
+ if (nameProp && Node.isPropertyAssignment(nameProp)) {
72
+ const initializer = nameProp.getInitializer();
73
+ if (initializer && Node.isStringLiteral(initializer)) {
74
+ return initializer.getLiteralText();
75
+ }
38
76
  }
39
77
  }
40
78
  return void 0;
41
79
  }
42
80
  function generateNameFromFilePath(file, viewsDir) {
43
- const relative = path.relative(viewsDir, file);
44
- const parsed = path.parse(relative);
45
- let name = parsed.name === "index" ? parsed.dir.split(path.sep).pop() ?? "Unknown" : parsed.name;
46
- name = name.replace(/(^|[-_\/\\])([a-z])/g, (_, __, c) => c.toUpperCase());
47
- return name;
81
+ const absoluteViewsDir = path.resolve(viewsDir);
82
+ const relativePath = path.relative(absoluteViewsDir, file);
83
+ const { dir, name } = path.parse(relativePath);
84
+ const segments = dir ? dir.split(path.sep) : [];
85
+ if (name !== "index") {
86
+ segments.push(name);
87
+ }
88
+ return segments.map(
89
+ (s) => s.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase())
90
+ ).join("") || "Index";
48
91
  }
49
- function normalizePath(p) {
50
- return p.split(path.sep).join("/");
92
+ function normalizePath(file, viewsDir) {
93
+ const relativePath = path.relative(path.resolve(viewsDir), file);
94
+ const combinedPath = path.join(viewsDir, relativePath);
95
+ return combinedPath.split(path.sep).join("/");
51
96
  }
52
97
 
53
98
  // src/virtual-module.ts
@@ -61,24 +106,60 @@ export const viewNameMap = ${JSON.stringify(map, null, 2)};
61
106
  function viewNameMapPlugin(options = {}) {
62
107
  const virtualId = "virtual:view-name-map";
63
108
  const resolvedVirtualId = "\0" + virtualId;
64
- let code = "";
109
+ const viewsDir = options.viewsDir ?? "src/views";
110
+ const autoInjectName = options.autoInjectName ?? true;
111
+ const autoGenerateName = autoInjectName ? true : options.autoGenerateName ?? true;
112
+ const root = normalizePath2(process.cwd());
113
+ const absoluteViewsDir = normalizePath2(path2.resolve(root, viewsDir));
114
+ let virtualCode = "";
115
+ let nameMap = {};
116
+ let injectedCount = 0;
65
117
  return {
66
118
  name: "vite-plugin-view-name-map",
67
119
  enforce: "pre",
68
120
  async buildStart() {
69
- const map = await generateViewNameMap({
70
- viewsDir: options.viewsDir,
71
- autoGenerateName: options.autoGenerateName ?? true
121
+ nameMap = await generateViewNameMap({
122
+ viewsDir,
123
+ autoGenerateName
72
124
  });
73
- code = createVirtualModule(map);
125
+ virtualCode = createVirtualModule(nameMap);
126
+ const count = Object.keys(nameMap).length;
127
+ if (count > 0) {
128
+ const status = autoInjectName ? "\u5DF2\u5F00\u542F\u81EA\u52A8\u6CE8\u5165" : "\u4EC5\u751F\u6210\u6620\u5C04\u8868";
129
+ console.log(`\x1B[32m[view-name-map] ${status}\uFF1A\u626B\u63CF\u5230 ${count} \u4E2A\u89C6\u56FE\u7EC4\u4EF6\u3002\x1B[0m`);
130
+ }
74
131
  },
75
132
  resolveId(id) {
76
133
  if (id === virtualId) return resolvedVirtualId;
77
134
  return void 0;
78
135
  },
79
136
  load(id) {
80
- if (id === resolvedVirtualId) return code;
137
+ if (id === resolvedVirtualId) return virtualCode;
81
138
  return void 0;
139
+ },
140
+ async transform(code, id) {
141
+ if (!autoInjectName) return null;
142
+ const cleanId = normalizePath2(id.split("?")[0]);
143
+ if (!cleanId.endsWith(".vue") || !cleanId.startsWith(absoluteViewsDir)) {
144
+ return null;
145
+ }
146
+ const componentName = generateNameFromFilePath(cleanId, absoluteViewsDir);
147
+ try {
148
+ const result = injectDefineOptions(code, cleanId, componentName);
149
+ if (result) {
150
+ injectedCount++;
151
+ }
152
+ return result;
153
+ } catch (err) {
154
+ console.error(`\x1B[31m[view-name-map] \u6CE8\u5165\u5F02\u5E38: ${cleanId}\x1B[0m`, err);
155
+ return null;
156
+ }
157
+ },
158
+ // 在构建结束时打印统计信息
159
+ buildEnd() {
160
+ if (autoInjectName && injectedCount > 0) {
161
+ console.log(`\x1B[36m[view-name-map] \u672C\u6B21\u8FD0\u884C\u5DF2\u4E3A ${injectedCount} \u4E2A\u7EC4\u4EF6\u6CE8\u5165\u4E86 defineOptions({ name: "..." })\u3002\x1B[0m`);
162
+ }
82
163
  }
83
164
  };
84
165
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/generate.ts","../src/virtual-module.ts","../src/index.ts"],"sourcesContent":["import path from 'path';\r\nimport fs from 'fs/promises';\r\nimport fg from 'fast-glob';\r\nimport { Project, SourceFile, SyntaxKind, ObjectLiteralExpression, CallExpression } 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 if (name) map[normalizePath(file)] = name;\r\n }\r\n\r\n return map;\r\n}\r\n\r\n/**\r\n * 从 defineOptions({ name }) 中提取组件 name\r\n */\r\nfunction extractNameFromDefineOptions(sourceFile: SourceFile): string | undefined {\r\n const calls: CallExpression[] = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);\r\n\r\n for (const call of calls) {\r\n const expression = call.getExpression();\r\n if (!expression || expression.getText() !== 'defineOptions') continue;\r\n\r\n const arg = call.getArguments()[0];\r\n if (!arg || !arg.asKind) continue;\r\n\r\n const obj: ObjectLiteralExpression | undefined = arg.asKind(SyntaxKind.ObjectLiteralExpression);\r\n if (!obj) continue;\r\n\r\n const nameProp = obj.getProperty('name');\r\n if (!nameProp || !('getInitializer' in nameProp)) continue;\r\n\r\n const initializer = nameProp.getInitializer?.();\r\n if (!initializer || !initializer.isKind) continue;\r\n\r\n if (initializer.isKind(SyntaxKind.StringLiteral)) {\r\n return initializer.getLiteralText();\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * 根据文件路径生成组件 name\r\n *\r\n * 规则:\r\n * - 文件名为 index 时取父目录名\r\n * - 首字母大写,支持驼峰\r\n */\r\nfunction generateNameFromFilePath(file: string, viewsDir: string): string {\r\n const relative: string = path.relative(viewsDir, file);\r\n const parsed = path.parse(relative);\r\n\r\n let name: string = parsed.name === 'index' ? parsed.dir.split(path.sep).pop() ?? 'Unknown' : parsed.name;\r\n // 驼峰化处理\r\n name = name.replace(/(^|[-_\\/\\\\])([a-z])/g, (_, __, c) => c.toUpperCase());\r\n\r\n return name;\r\n}\r\n\r\n/** 路径统一分隔符 */\r\nfunction normalizePath(p: string): string {\r\n return p.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","import type { Plugin } from 'vite';\r\nimport { generateViewNameMap, 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}\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 let code = '';\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 const map: ViewNameMap = await generateViewNameMap({\r\n viewsDir: options.viewsDir,\r\n autoGenerateName: options.autoGenerateName ?? true\r\n });\r\n code = createVirtualModule(map);\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 code;\r\n return undefined;\r\n }\r\n };\r\n}\r\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,SAAS,SAAqB,kBAA2D;AAczF,eAAsB,oBAClB,UAA2B,CAAC,GACR;AACpB,QAAM,WAAmB,QAAQ,YAAY;AAC7C,QAAM,UAAmB,QAAQ,oBAAoB;AAErD,QAAM,MAAmB,CAAC;AAC1B,QAAM,QAAkB,MAAM,GAAG,CAAC,YAAY,UAAU,GAAG,EAAE,KAAK,UAAU,UAAU,KAAK,CAAC;AAE5F,QAAM,UAAmB,IAAI,QAAQ,EAAE,uBAAuB,MAAM,6BAA6B,KAAK,CAAC;AAEvG,aAAW,QAAQ,OAAO;AACtB,UAAM,UAAkB,MAAM,GAAG,SAAS,MAAM,OAAO;AACvD,UAAM,aAAyB,QAAQ,iBAAiB,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC;AAE1F,QAAI,OAA2B,6BAA6B,UAAU;AAEtE,QAAI,CAAC,QAAQ,SAAS;AAClB,aAAO,yBAAyB,MAAM,QAAQ;AAAA,IAClD;AAEA,QAAI,KAAM,KAAI,cAAc,IAAI,CAAC,IAAI;AAAA,EACzC;AAEA,SAAO;AACX;AAKA,SAAS,6BAA6B,YAA4C;AAC9E,QAAM,QAA0B,WAAW,qBAAqB,WAAW,cAAc;AAEzF,aAAW,QAAQ,OAAO;AACtB,UAAM,aAAa,KAAK,cAAc;AACtC,QAAI,CAAC,cAAc,WAAW,QAAQ,MAAM,gBAAiB;AAE7D,UAAM,MAAM,KAAK,aAAa,EAAE,CAAC;AACjC,QAAI,CAAC,OAAO,CAAC,IAAI,OAAQ;AAEzB,UAAM,MAA2C,IAAI,OAAO,WAAW,uBAAuB;AAC9F,QAAI,CAAC,IAAK;AAEV,UAAM,WAAW,IAAI,YAAY,MAAM;AACvC,QAAI,CAAC,YAAY,EAAE,oBAAoB,UAAW;AAElD,UAAM,cAAc,SAAS,iBAAiB;AAC9C,QAAI,CAAC,eAAe,CAAC,YAAY,OAAQ;AAEzC,QAAI,YAAY,OAAO,WAAW,aAAa,GAAG;AAC9C,aAAO,YAAY,eAAe;AAAA,IACtC;AAAA,EACJ;AAEA,SAAO;AACX;AASA,SAAS,yBAAyB,MAAc,UAA0B;AACtE,QAAM,WAAmB,KAAK,SAAS,UAAU,IAAI;AACrD,QAAM,SAAS,KAAK,MAAM,QAAQ;AAElC,MAAI,OAAe,OAAO,SAAS,UAAU,OAAO,IAAI,MAAM,KAAK,GAAG,EAAE,IAAI,KAAK,YAAY,OAAO;AAEpG,SAAO,KAAK,QAAQ,wBAAwB,CAAC,GAAG,IAAI,MAAM,EAAE,YAAY,CAAC;AAEzE,SAAO;AACX;AAGA,SAAS,cAAc,GAAmB;AACtC,SAAO,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;AACrC;;;ACtFO,SAAS,oBAAoB,KAA0B;AAC1D,SAAO;AAAA,6BACkB,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA;AAEzD;;;ACMe,SAAR,kBACH,UAAoC,CAAC,GAC/B;AACN,QAAM,YAAY;AAClB,QAAM,oBAAoB,OAAO;AACjC,MAAI,OAAO;AAEX,SAAO;AAAA,IACH,MAAM;AAAA,IACN,SAAS;AAAA,IAET,MAAM,aAA4B;AAC9B,YAAM,MAAmB,MAAM,oBAAoB;AAAA,QAC/C,UAAU,QAAQ;AAAA,QAClB,kBAAkB,QAAQ,oBAAoB;AAAA,MAClD,CAAC;AACD,aAAO,oBAAoB,GAAG;AAAA,IAClC;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,EACJ;AACJ;","names":[]}
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":";AACA,SAAQ,iBAAAA,sBAAoB;AAC5B,OAAOC,WAAU;;;ACFjB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,iBAAiB;AACxB,SAAQ,aAAY;AACpB,SAAoC,MAA+B,SAAqB,kBAAiB;AAczG,eAAsB,oBAClB,UAA2B,CAAC,GACR;AACpB,QAAM,WAAmB,QAAQ,YAAY;AAC7C,QAAM,UAAmB,QAAQ,oBAAoB;AAErD,QAAM,MAAmB,CAAC;AAC1B,QAAM,QAAkB,MAAM,GAAG,CAAC,YAAY,UAAU,GAAG,EAAC,KAAK,UAAU,UAAU,KAAI,CAAC;AAE1F,QAAM,UAAmB,IAAI,QAAQ,EAAC,uBAAuB,MAAM,6BAA6B,KAAI,CAAC;AAErG,aAAW,QAAQ,OAAO;AACtB,UAAM,UAAkB,MAAM,GAAG,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,IAAI,MAAM,IAAI;AAC/B,MAAI,CAAC,WAAW,YAAa,QAAO;AAEpC,QAAM,UAAU,WAAW,YAAY;AACvC,QAAM,cAAc,WAAW,YAAY,IAAI,MAAM;AAGrD,QAAM,UAAU,IAAI,QAAQ,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,YAAY,IAAI;AAC9B,QAAM,oBAAoB,WAAW,qBAAqB,WAAW,cAAc,EAC9E,KAAK,OAAK,EAAE,cAAc,EAAE,QAAQ,MAAM,eAAe;AAE9D,MAAI,mBAAmB;AAEnB,UAAM,MAAM,kBAAkB,aAAa,EAAE,CAAC;AAC9C,QAAI,KAAK,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,WAAW,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,WAAW,uBAAuB;AACnG,QAAI,CAAC,IAAK;AAGV,UAAM,WAAW,IAAI,YAAY,MAAM;AACvC,QAAI,YAAY,KAAK,qBAAqB,QAAQ,GAAG;AACjD,YAAM,cAAc,SAAS,eAAe;AAC5C,UAAI,eAAe,KAAK,gBAAgB,WAAW,GAAG;AAClD,eAAO,YAAY,eAAe;AAAA,MACtC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AASO,SAAS,yBAAyB,MAAc,UAA0B;AAC7E,QAAM,mBAA2B,KAAK,QAAQ,QAAQ;AACtD,QAAM,eAAuB,KAAK,SAAS,kBAAkB,IAAI;AACjE,QAAM,EAAC,KAAK,KAAI,IAAI,KAAK,MAAM,YAAY;AAG3C,QAAM,WAAqB,MAAM,IAAI,MAAM,KAAK,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,KAAK,SAAS,KAAK,QAAQ,QAAQ,GAAG,IAAI;AACvE,QAAM,eAAuB,KAAK,KAAK,UAAU,YAAY;AAE7D,SAAO,aAAa,MAAM,KAAK,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,OAAOC,eAAc,QAAQ,IAAI,CAAC;AACxC,QAAM,mBAAmBA,eAAcC,MAAK,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,UAAUD,eAAc,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":["normalizePath","path","normalizePath","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v-long/vite-plugin-view-name-map",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Vite plugin to generate view-name-map from Vue SFC defineOptions",
6
6
  "keywords": [
@@ -23,16 +23,25 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "import": "./dist/index.js",
25
25
  "require": "./dist/index.cjs"
26
+ },
27
+ "./client": {
28
+ "types": "./dist/virtual.d.ts"
26
29
  }
27
30
  },
28
31
  "peerDependencies": {
29
- "vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
32
+ "vite": "^4.0.0 || ^5.0.0 || ^6.0.0",
33
+ "@vue/compiler-sfc": "^3.0.0"
30
34
  },
31
- "devDependencies": {
35
+ "dependencies": {
32
36
  "fast-glob": "^3.3.0",
33
- "ts-morph": "^20.0.0",
34
- "typescript": "^5.0.0",
35
- "tsup": "^8.0.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"
36
45
  },
37
46
  "scripts": {
38
47
  "build": "tsup",