feima-shortcuts 0.3.0-beta.8 → 0.3.1

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/bin/feima.js CHANGED
@@ -32,7 +32,7 @@ program
32
32
  .command('check-i18n-views')
33
33
  .description('检查已使用未创建的 key')
34
34
  .action(() => {
35
- checkI18nViews.run();
35
+ checkI18nViews.run()
36
36
  })
37
37
 
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feima-shortcuts",
3
- "version": "0.3.0-beta.8",
3
+ "version": "0.3.1",
4
4
  "description": "快捷指令",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -1,171 +1,485 @@
1
+ #!/usr/bin/env node
1
2
  const fs = require('fs')
2
3
  const path = require('path')
3
4
  const glob = require('glob')
5
+ const ts = require('typescript')
4
6
  const { parse } = require('@vue/compiler-sfc')
5
- const babelParser = require('@babel/parser')
6
- const traverse = require('@babel/traverse').default
7
7
 
8
- /* ---------------- defaults ---------------- */
8
+ const ROOT = process.cwd()
9
+ const SCAN_DIRS = ['src/views'] // views 目录
10
+ const COMPONENTS_DIR = 'src/components' // components 目录
11
+ const FILE_EXT = ['vue', 'ts', 'js', 'tsx', 'jsx']
9
12
 
10
- const DEFAULT_OPTIONS = {
11
- cwd: process.cwd(),
12
- viewsDir: 'src/views',
13
- silent: false,
14
- }
13
+ // 匹配 t() 调用:t('key') 或 t('key', 'default')
14
+ const T_FUNCTION_REG = /\bt\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`][^'"`]*['"`])?\s*\)/g
15
15
 
16
- /* ---------------- utils ---------------- */
16
+ // 匹配 $t() 调用:$t('key') 或 $t('key', 'default')
17
+ const DOLLAR_T_FUNCTION_REG = /\$t\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`][^'"`]*['"`])?\s*\)/g
17
18
 
19
+ /**
20
+ * 扁平化 locale 对象,返回所有 key 的集合
21
+ */
18
22
  function flattenLocale(obj, prefix = '') {
19
- let res = []
23
+ const keys = new Set()
20
24
  for (const k in obj) {
21
25
  const key = prefix ? `${prefix}.${k}` : k
22
- if (typeof obj[k] === 'object') {
23
- res = res.concat(flattenLocale(obj[k], key))
26
+ if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
27
+ const nestedKeys = flattenLocale(obj[k], key)
28
+ nestedKeys.forEach(k => keys.add(k))
24
29
  } else {
25
- res.push(key)
30
+ keys.add(key)
26
31
  }
27
32
  }
28
- return res
33
+ return keys
29
34
  }
30
35
 
36
+ /**
37
+ * 加载 locale.ts 文件并提取所有定义的 key
38
+ */
31
39
  function loadLocaleKeys(localePath) {
32
- if (!fs.existsSync(localePath)) return null
33
- const mod = require(localePath)
34
- const localeObj = mod.default || mod
35
- return flattenLocale(localeObj)
40
+ if (!fs.existsSync(localePath)) {
41
+ return null
42
+ }
43
+
44
+ try {
45
+ const code = fs.readFileSync(localePath, 'utf-8')
46
+ const sf = ts.createSourceFile(localePath, code, ts.ScriptTarget.Latest, true)
47
+
48
+ let obj = null
49
+
50
+ function visit(node) {
51
+ if (
52
+ ts.isExportAssignment(node) &&
53
+ ts.isObjectLiteralExpression(node.expression)
54
+ ) {
55
+ try {
56
+ // 使用 eval 来解析对象字面量
57
+ obj = eval(`(${node.expression.getText()})`)
58
+ } catch (e) {
59
+ // 如果解析失败,尝试其他方式
60
+ console.warn(`⚠️ 无法解析 locale 文件: ${localePath}`)
61
+ }
62
+ }
63
+ ts.forEachChild(node, visit)
64
+ }
65
+
66
+ visit(sf)
67
+ return obj ? flattenLocale(obj) : new Set()
68
+ } catch (error) {
69
+ console.warn(`⚠️ 读取 locale 文件失败: ${localePath}`, error.message)
70
+ return null
71
+ }
36
72
  }
37
73
 
38
- function parseCode(code) {
39
- return babelParser.parse(code, {
40
- sourceType: 'module',
41
- plugins: ['typescript', 'jsx'],
42
- })
74
+ /**
75
+ * 从代码中提取所有 t() 调用的 key
76
+ */
77
+ function extractTKeys(code) {
78
+ const keys = new Set()
79
+ let match
80
+ while ((match = T_FUNCTION_REG.exec(code)) !== null) {
81
+ keys.add(match[1])
82
+ }
83
+ return keys
43
84
  }
44
85
 
45
- /* ---------------- core ---------------- */
86
+ /**
87
+ * 从代码中提取所有 $t() 调用的 key
88
+ */
89
+ function extractDollarTKeys(code) {
90
+ const keys = new Set()
91
+ let match
92
+ while ((match = DOLLAR_T_FUNCTION_REG.exec(code)) !== null) {
93
+ keys.add(match[1])
94
+ }
95
+ return keys
96
+ }
46
97
 
47
- function run(userOptions = {}) {
48
- // 保证 options 永远有值
49
- const options = { ...DEFAULT_OPTIONS, ...userOptions }
50
- const { cwd, viewsDir, silent } = options
98
+ /**
99
+ * 检查 locale 导入方式
100
+ * @returns {Object} { hasCorrectImport, hasOtherImport, importPaths }
101
+ */
102
+ function checkLocaleImport(code) {
103
+ const result = {
104
+ hasCorrectImport: false,
105
+ hasOtherImport: false,
106
+ importPaths: []
107
+ }
51
108
 
52
- const absViewsDir = path.join(cwd, viewsDir)
109
+ // 匹配 import locale from 'xxx'
110
+ const importReg = /import\s+locale\s+from\s+['"]([^'"]+)['"]/g
111
+ let match
53
112
 
54
- if (!fs.existsSync(absViewsDir)) {
55
- if (!silent) {
56
- console.warn(`⚠️ views 目录不存在:${viewsDir}`)
57
- }
58
- return {
59
- success: true,
60
- missingKeys: [],
61
- noLocaleUsage: [],
62
- scannedFiles: 0,
113
+ while ((match = importReg.exec(code)) !== null) {
114
+ const importPath = match[1]
115
+ result.importPaths.push(importPath)
116
+ if (importPath === './locale') {
117
+ result.hasCorrectImport = true
118
+ } else {
119
+ result.hasOtherImport = true
63
120
  }
64
121
  }
65
122
 
66
- const files = glob.sync('**/*.{vue,ts,tsx}', {
67
- cwd: absViewsDir,
68
- absolute: true,
69
- })
123
+ return result
124
+ }
70
125
 
71
- const missingKeys = []
72
- const noLocaleUsage = []
126
+ /**
127
+ * 解析 Vue 文件,提取 script 部分的代码
128
+ */
129
+ function parseVueFile(filePath) {
130
+ try {
131
+ const content = fs.readFileSync(filePath, 'utf-8')
132
+ const { descriptor } = parse(content)
133
+
134
+ // 合并所有 script 块的内容
135
+ let scriptContent = ''
136
+ if (descriptor.script) {
137
+ scriptContent += descriptor.script.content
138
+ }
139
+ if (descriptor.scriptSetup) {
140
+ scriptContent += '\n' + descriptor.scriptSetup.content
141
+ }
142
+
143
+ // 也检查 template 中的 t() 调用
144
+ if (descriptor.template) {
145
+ scriptContent += '\n' + descriptor.template.content
146
+ }
73
147
 
74
- for (const file of files) {
75
- const content = fs.readFileSync(file, 'utf-8')
148
+ return scriptContent
149
+ } catch (error) {
150
+ console.warn(`⚠️ 解析 Vue 文件失败: ${filePath}`, error.message)
151
+ return ''
152
+ }
153
+ }
76
154
 
77
- let scriptCode = content
155
+ /**
156
+ * 查找文件对应的 locale.ts 路径
157
+ */
158
+ function findLocalePath(filePath) {
159
+ const dir = path.dirname(filePath)
160
+ const localePath = path.join(dir, 'locale.ts')
161
+ return localePath
162
+ }
78
163
 
79
- if (file.endsWith('.vue')) {
80
- const { descriptor } = parse(content)
81
- scriptCode =
82
- descriptor.scriptSetup?.content ||
83
- descriptor.script?.content ||
84
- ''
164
+ /**
165
+ * 扫描单个文件
166
+ */
167
+ function scanFile(filePath, results) {
168
+ let code = ''
169
+
170
+ if (filePath.endsWith('.vue')) {
171
+ code = parseVueFile(filePath)
172
+ } else {
173
+ try {
174
+ code = fs.readFileSync(filePath, 'utf-8')
175
+ } catch (error) {
176
+ console.warn(`⚠️ 读取文件失败: ${filePath}`, error.message)
177
+ return
85
178
  }
179
+ }
86
180
 
87
- if (!scriptCode) continue
88
-
89
- const ast = parseCode(scriptCode)
181
+ if (!code) return
90
182
 
91
- let usedLocale = false
92
- const usedKeys = []
183
+ // 提取 t() 调用的 key
184
+ const usedKeys = extractTKeys(code)
185
+ if (usedKeys.size === 0) return
93
186
 
94
- traverse(ast, {
95
- ImportDeclaration(p) {
96
- if (p.node.source.value === './locale') {
97
- usedLocale = true
98
- }
99
- },
100
-
101
- CallExpression(p) {
102
- const callee = p.node.callee
103
- if (
104
- (callee.type === 'Identifier' && callee.name === 't') ||
105
- (callee.type === 'Identifier' && callee.name === '$t')
106
- ) {
107
- const arg = p.node.arguments[0]
108
- if (arg && arg.type === 'StringLiteral') {
109
- usedKeys.push(arg.value)
110
- }
111
- }
112
- },
113
- })
187
+ // 检查 locale 导入方式
188
+ const importInfo = checkLocaleImport(code)
189
+
190
+ // 查找对应的 locale.ts
191
+ const localePath = findLocalePath(filePath)
192
+ const localeKeys = loadLocaleKeys(localePath)
114
193
 
115
- if (!usedKeys.length) continue
194
+ const relativePath = path.relative(ROOT, filePath)
195
+ const relativeLocalePath = path.relative(ROOT, localePath)
116
196
 
117
- const localePath = path.join(path.dirname(file), 'locale.ts')
118
- const localeKeys = loadLocaleKeys(localePath)
197
+ // 如果使用了非标准的导入方式,记录(但不检查 key)
198
+ if (!importInfo.hasCorrectImport && importInfo.hasOtherImport) {
199
+ if (!results.invalidImports.has(relativePath)) {
200
+ results.invalidImports.set(relativePath, {
201
+ file: relativePath,
202
+ importPaths: importInfo.importPaths,
203
+ usedKeys: Array.from(usedKeys)
204
+ })
205
+ }
206
+ return // 非标准导入,不检查 key 是否缺失
207
+ }
119
208
 
120
- if (!usedLocale || !localeKeys) {
121
- noLocaleUsage.push({
122
- file: path.relative(cwd, file),
123
- keys: usedKeys,
209
+ // 如果没有导入 locale,记录(但不检查 key,可能使用全局 locale)
210
+ if (!importInfo.hasCorrectImport && !importInfo.hasOtherImport) {
211
+ if (!results.noLocaleImport.has(relativePath)) {
212
+ results.noLocaleImport.set(relativePath, {
213
+ file: relativePath,
214
+ usedKeys: Array.from(usedKeys)
124
215
  })
125
- continue
126
216
  }
217
+ return // 没有导入本地 locale,不检查 key 是否缺失
218
+ }
127
219
 
128
- for (const key of usedKeys) {
129
- if (!localeKeys.includes(key)) {
130
- missingKeys.push({
131
- file: path.relative(cwd, file),
132
- key,
220
+ // 只有使用标准导入方式(import locale from './locale')时,才检查 key
221
+ // 如果 locale.ts 不存在,记录缺失的 key
222
+ if (!localeKeys) {
223
+ usedKeys.forEach(key => {
224
+ if (!results.missingLocale.has(relativePath)) {
225
+ results.missingLocale.set(relativePath, {
226
+ file: relativePath,
227
+ localePath: relativeLocalePath,
228
+ missingKeys: new Set()
133
229
  })
134
230
  }
231
+ results.missingLocale.get(relativePath).missingKeys.add(key)
232
+ })
233
+ return
234
+ }
235
+
236
+ // 检查哪些 key 在 locale 中不存在
237
+ const missingKeys = Array.from(usedKeys).filter(key => !localeKeys.has(key))
238
+
239
+ if (missingKeys.length > 0) {
240
+ if (!results.missingKeys.has(relativePath)) {
241
+ results.missingKeys.set(relativePath, {
242
+ file: relativePath,
243
+ localePath: relativeLocalePath,
244
+ missingKeys: new Set()
245
+ })
135
246
  }
247
+ missingKeys.forEach(key => {
248
+ results.missingKeys.get(relativePath).missingKeys.add(key)
249
+ })
136
250
  }
251
+ }
137
252
 
138
- /* ---------------- output ---------------- */
253
+ /**
254
+ * 检查组件目录(使用 $t 注入方式)
255
+ */
256
+ function checkComponent(componentDir, results) {
257
+ const localePath = path.join(componentDir, 'locale.ts')
258
+ if (!fs.existsSync(localePath)) {
259
+ return // 组件没有 locale.ts,跳过
260
+ }
139
261
 
140
- if (!silent) {
141
- if (missingKeys.length) {
142
- console.error('\n❌ 缺失的国际化 key:')
143
- missingKeys.forEach(i =>
144
- console.error(` ${i.file} → ${i.key}`)
145
- )
146
- }
262
+ const localeKeys = loadLocaleKeys(localePath)
263
+ if (!localeKeys) {
264
+ console.warn(`⚠️ 无法加载组件 locale: ${path.relative(ROOT, localePath)}`)
265
+ return
266
+ }
267
+
268
+ // 递归扫描组件目录下的所有文件
269
+ const allUsedKeys = new Set()
270
+ const componentFiles = []
271
+
272
+ FILE_EXT.forEach(ext => {
273
+ const pattern = `${componentDir}/**/*.${ext}`
274
+ const files = glob.sync(pattern, { nodir: true })
275
+ files.forEach(file => {
276
+ componentFiles.push(file)
277
+ let code = ''
278
+
279
+ if (file.endsWith('.vue')) {
280
+ code = parseVueFile(file)
281
+ } else {
282
+ try {
283
+ code = fs.readFileSync(file, 'utf-8')
284
+ } catch (error) {
285
+ console.warn(`⚠️ 读取文件失败: ${file}`, error.message)
286
+ return
287
+ }
288
+ }
147
289
 
148
- if (noLocaleUsage.length) {
149
- console.warn('\n⚠️ 使用了 t/$t 但未使用 ./locale:')
150
- noLocaleUsage.forEach(i =>
151
- console.warn(` ${i.file}`)
152
- )
290
+ if (code) {
291
+ // 提取 $t() 调用的 key
292
+ const dollarTKeys = extractDollarTKeys(code)
293
+ dollarTKeys.forEach(key => allUsedKeys.add(key))
294
+ }
295
+ })
296
+ })
297
+
298
+ if (allUsedKeys.size === 0) {
299
+ return // 组件中没有使用 $t()
300
+ }
301
+
302
+ // 检查哪些 key 在 locale 中不存在
303
+ const missingKeys = Array.from(allUsedKeys).filter(key => !localeKeys.has(key))
304
+
305
+ if (missingKeys.length > 0) {
306
+ const relativeComponentDir = path.relative(ROOT, componentDir)
307
+ const relativeLocalePath = path.relative(ROOT, localePath)
308
+
309
+ if (!results.componentMissingKeys.has(relativeComponentDir)) {
310
+ results.componentMissingKeys.set(relativeComponentDir, {
311
+ componentDir: relativeComponentDir,
312
+ localePath: relativeLocalePath,
313
+ missingKeys: new Set(),
314
+ files: []
315
+ })
153
316
  }
317
+
318
+ missingKeys.forEach(key => {
319
+ results.componentMissingKeys.get(relativeComponentDir).missingKeys.add(key)
320
+ })
321
+
322
+ // 记录使用了这些 key 的文件
323
+ componentFiles.forEach(file => {
324
+ let code = ''
325
+ if (file.endsWith('.vue')) {
326
+ code = parseVueFile(file)
327
+ } else {
328
+ try {
329
+ code = fs.readFileSync(file, 'utf-8')
330
+ } catch (error) {
331
+ return
332
+ }
333
+ }
334
+
335
+ if (code) {
336
+ const dollarTKeys = extractDollarTKeys(code)
337
+ const fileMissingKeys = Array.from(dollarTKeys).filter(key => missingKeys.includes(key))
338
+ if (fileMissingKeys.length > 0) {
339
+ const relativeFile = path.relative(ROOT, file)
340
+ const info = results.componentMissingKeys.get(relativeComponentDir)
341
+ if (!info.files.find(f => f.file === relativeFile)) {
342
+ info.files.push({
343
+ file: relativeFile,
344
+ missingKeys: fileMissingKeys
345
+ })
346
+ }
347
+ }
348
+ }
349
+ })
350
+ }
351
+ }
154
352
 
155
- if (!missingKeys.length && !noLocaleUsage.length) {
156
- console.log('✅ views 国际化检查通过')
353
+ /**
354
+ * 主函数
355
+ */
356
+ function run() {
357
+ const results = {
358
+ invalidImports: new Map(), // 使用了非 './locale' 的导入
359
+ noLocaleImport: new Map(), // 没有导入 locale 但使用了 t()
360
+ missingLocale: new Map(), // locale.ts 文件不存在
361
+ missingKeys: new Map(), // locale.ts 存在但缺少某些 key
362
+ componentMissingKeys: new Map() // 组件中 $t() 的 key 在 locale.ts 中缺失
363
+ }
364
+
365
+ // 扫描 views 目录
366
+ SCAN_DIRS.forEach(dir => {
367
+ const absDir = path.join(ROOT, dir)
368
+ if (!fs.existsSync(absDir)) {
369
+ console.log(`⚠️ 目录不存在: ${dir}`)
370
+ return
157
371
  }
372
+
373
+ FILE_EXT.forEach(ext => {
374
+ const pattern = `${absDir}/**/*.${ext}`
375
+ const files = glob.sync(pattern, { nodir: true })
376
+ files.forEach(file => scanFile(file, results))
377
+ })
378
+ })
379
+
380
+ // 扫描 components 目录
381
+ const componentsDir = path.join(ROOT, COMPONENTS_DIR)
382
+ if (fs.existsSync(componentsDir)) {
383
+ // 找到所有有 locale.ts 的组件目录
384
+ const localeFiles = glob.sync(`${componentsDir}/**/locale.ts`, { nodir: true })
385
+ localeFiles.forEach(localeFile => {
386
+ const componentDir = path.dirname(localeFile)
387
+ checkComponent(componentDir, results)
388
+ })
389
+ } else {
390
+ console.log(`⚠️ 目录不存在: ${COMPONENTS_DIR}`)
391
+ }
392
+
393
+ // 输出结果
394
+ console.log('\n📋 i18n 使用检查结果\n')
395
+
396
+ let hasError = false
397
+
398
+ // 1. 输出非标准导入方式
399
+ if (results.invalidImports.size > 0) {
400
+ hasError = true
401
+ console.log('❌ 使用了非标准的 locale 导入方式(应使用 import locale from \'./locale\'):\n')
402
+ results.invalidImports.forEach((info, file) => {
403
+ console.log(` ${file}`)
404
+ info.importPaths.forEach(importPath => {
405
+ console.log(` ↳ import locale from '${importPath}'`)
406
+ })
407
+ if (info.usedKeys && info.usedKeys.length > 0) {
408
+ console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
409
+ }
410
+ })
411
+ console.log()
412
+ }
413
+
414
+ // 2. 输出没有导入 locale 的文件
415
+ if (results.noLocaleImport.size > 0) {
416
+ console.log('⚠️ 以下文件使用了 t() 但未导入本地 locale(可能使用全局 locale):\n')
417
+ results.noLocaleImport.forEach((info, file) => {
418
+ console.log(` ${file}`)
419
+ console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
420
+ })
421
+ console.log()
158
422
  }
159
423
 
160
- return {
161
- success: missingKeys.length === 0,
162
- missingKeys,
163
- noLocaleUsage,
164
- scannedFiles: files.length,
165
- options, // 🔍 调试/日志时很有用
424
+ // 3. 输出缺少 locale.ts 文件的情况
425
+ if (results.missingLocale.size > 0) {
426
+ hasError = true
427
+ console.log('❌ 以下文件使用了 t() 但缺少对应的 locale.ts 文件:\n')
428
+ results.missingLocale.forEach((info, file) => {
429
+ console.log(` ${file}`)
430
+ console.log(` 期望的 locale 文件: ${info.localePath}`)
431
+ console.log(` 缺失的 key: ${Array.from(info.missingKeys).join(', ')}`)
432
+ })
433
+ console.log()
434
+ }
435
+
436
+ // 4. 输出缺少的 key
437
+ if (results.missingKeys.size > 0) {
438
+ hasError = true
439
+ console.log('❌ 以下文件使用的 key 在 locale.ts 中未定义:\n')
440
+ results.missingKeys.forEach((info, file) => {
441
+ console.log(` ${file}`)
442
+ console.log(` locale 文件: ${info.localePath}`)
443
+ console.log(` 缺失的 key:`)
444
+ Array.from(info.missingKeys).forEach(key => {
445
+ console.log(` - ${key}`)
446
+ })
447
+ })
448
+ console.log()
449
+ }
450
+
451
+ // 5. 输出组件中缺失的 key
452
+ if (results.componentMissingKeys.size > 0) {
453
+ hasError = true
454
+ console.log('❌ 以下组件使用的 $t() key 在 locale.ts 中未定义:\n')
455
+ results.componentMissingKeys.forEach((info, componentDir) => {
456
+ console.log(` 组件: ${componentDir}`)
457
+ console.log(` locale 文件: ${info.localePath}`)
458
+ console.log(` 缺失的 key:`)
459
+ Array.from(info.missingKeys).forEach(key => {
460
+ console.log(` - ${key}`)
461
+ })
462
+ if (info.files && info.files.length > 0) {
463
+ console.log(` 使用位置:`)
464
+ info.files.forEach(fileInfo => {
465
+ console.log(` ${fileInfo.file}`)
466
+ if (fileInfo.missingKeys && fileInfo.missingKeys.length > 0) {
467
+ console.log(` 缺失的 key: ${fileInfo.missingKeys.join(', ')}`)
468
+ }
469
+ })
470
+ }
471
+ console.log()
472
+ })
166
473
  }
167
- }
168
474
 
169
- /* ---------------- exports ---------------- */
475
+ // 总结
476
+ if (hasError) {
477
+ console.log('❗ 检查未通过,请修复上述问题')
478
+ process.exitCode = 1
479
+ } else {
480
+ console.log('✅ 所有检查通过')
481
+ }
482
+ }
170
483
 
171
484
  exports.run = run
485
+
@@ -4,7 +4,7 @@ const { makeDir } = require("../../utils/makeDir");
4
4
  const fileName = "index.ts";
5
5
 
6
6
  const postTemplat = (apiPath, functionName) => {
7
- return `export const ${functionName}: any = (options: any) => {
7
+ return `export const ${functionName} = (options: any) => {
8
8
  const isFormData = options?.data instanceof FormData;
9
9
 
10
10
  const data = isFormData
@@ -37,7 +37,7 @@ function splitString(input) {
37
37
  const fileData = (answers, functionName) => {
38
38
  if (fs.existsSync(`./${fileName}`)) {
39
39
  const data = fs.readFileSync(`./${fileName}`, "utf8");
40
- if (data.includes(`export const ${functionName}: any`)) {
40
+ if (data.includes(`export const ${functionName} `)) {
41
41
  return "";
42
42
  }
43
43
  return data;