feima-shortcuts 0.3.0-beta.10 → 0.3.0-beta.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feima-shortcuts",
3
- "version": "0.3.0-beta.10",
3
+ "version": "0.3.0-beta.12",
4
4
  "description": "快捷指令",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -1,238 +1,331 @@
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'] // components 先预留
10
+ const FILE_EXT = ['vue', 'ts', 'js', 'tsx', 'jsx']
9
11
 
10
- const DEFAULT_OPTIONS = {
11
- cwd: process.cwd(),
12
- scanDirs: ['src/views', 'src/components'],
13
- silent: false,
14
- }
15
-
16
- /* ---------------- utils ---------------- */
12
+ // 匹配 t() 调用:t('key') 或 t('key', 'default')
13
+ const T_FUNCTION_REG = /\bt\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`][^'"`]*['"`])?\s*\)/g
17
14
 
15
+ /**
16
+ * 扁平化 locale 对象,返回所有 key 的集合
17
+ */
18
18
  function flattenLocale(obj, prefix = '') {
19
- let res = []
19
+ const keys = new Set()
20
20
  for (const k in obj) {
21
21
  const key = prefix ? `${prefix}.${k}` : k
22
- if (typeof obj[k] === 'object' && obj[k] !== null) {
23
- res = res.concat(flattenLocale(obj[k], key))
22
+ if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
23
+ const nestedKeys = flattenLocale(obj[k], key)
24
+ nestedKeys.forEach(k => keys.add(k))
24
25
  } else {
25
- res.push(key)
26
+ keys.add(key)
26
27
  }
27
28
  }
28
- return res
29
+ return keys
29
30
  }
30
31
 
32
+ /**
33
+ * 加载 locale.ts 文件并提取所有定义的 key
34
+ */
31
35
  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)
36
+ if (!fs.existsSync(localePath)) {
37
+ return null
38
+ }
39
+
40
+ try {
41
+ const code = fs.readFileSync(localePath, 'utf-8')
42
+ const sf = ts.createSourceFile(localePath, code, ts.ScriptTarget.Latest, true)
43
+
44
+ let obj = null
45
+
46
+ function visit(node) {
47
+ if (
48
+ ts.isExportAssignment(node) &&
49
+ ts.isObjectLiteralExpression(node.expression)
50
+ ) {
51
+ try {
52
+ // 使用 eval 来解析对象字面量
53
+ obj = eval(`(${node.expression.getText()})`)
54
+ } catch (e) {
55
+ // 如果解析失败,尝试其他方式
56
+ console.warn(`⚠️ 无法解析 locale 文件: ${localePath}`)
57
+ }
58
+ }
59
+ ts.forEachChild(node, visit)
60
+ }
61
+
62
+ visit(sf)
63
+ return obj ? flattenLocale(obj) : new Set()
64
+ } catch (error) {
65
+ console.warn(`⚠️ 读取 locale 文件失败: ${localePath}`, error.message)
66
+ return null
67
+ }
36
68
  }
37
69
 
38
- function parseCode(code) {
39
- return babelParser.parse(code, {
40
- sourceType: 'module',
41
- plugins: ['typescript', 'jsx'],
42
- })
70
+ /**
71
+ * 从代码中提取所有 t() 调用的 key
72
+ */
73
+ function extractTKeys(code) {
74
+ const keys = new Set()
75
+ let match
76
+ while ((match = T_FUNCTION_REG.exec(code)) !== null) {
77
+ keys.add(match[1])
78
+ }
79
+ return keys
43
80
  }
44
81
 
45
82
  /**
46
- * 从当前文件目录开始,向上查找最近的 locale.ts
47
- * 直到到达 componentsRoot 为止
83
+ * 检查 locale 导入方式
84
+ * @returns {Object} { hasCorrectImport, hasOtherImport, importPaths }
48
85
  */
49
- function findNearestLocale(fileDir, componentsRoot) {
50
- let current = fileDir
86
+ function checkLocaleImport(code) {
87
+ const result = {
88
+ hasCorrectImport: false,
89
+ hasOtherImport: false,
90
+ importPaths: []
91
+ }
92
+
93
+ // 匹配 import locale from 'xxx'
94
+ const importReg = /import\s+locale\s+from\s+['"]([^'"]+)['"]/g
95
+ let match
51
96
 
52
- while (current.startsWith(componentsRoot)) {
53
- const localePath = path.join(current, 'locale.ts')
54
- if (fs.existsSync(localePath)) {
55
- return localePath
97
+ while ((match = importReg.exec(code)) !== null) {
98
+ const importPath = match[1]
99
+ result.importPaths.push(importPath)
100
+ if (importPath === './locale') {
101
+ result.hasCorrectImport = true
102
+ } else {
103
+ result.hasOtherImport = true
56
104
  }
57
- const parent = path.dirname(current)
58
- if (parent === current) break
59
- current = parent
60
105
  }
61
106
 
62
- return null
107
+ return result
63
108
  }
64
109
 
65
- /* ---------------- core ---------------- */
110
+ /**
111
+ * 解析 Vue 文件,提取 script 部分的代码
112
+ */
113
+ function parseVueFile(filePath) {
114
+ try {
115
+ const content = fs.readFileSync(filePath, 'utf-8')
116
+ const { descriptor } = parse(content)
117
+
118
+ // 合并所有 script 块的内容
119
+ let scriptContent = ''
120
+ if (descriptor.script) {
121
+ scriptContent += descriptor.script.content
122
+ }
123
+ if (descriptor.scriptSetup) {
124
+ scriptContent += '\n' + descriptor.scriptSetup.content
125
+ }
126
+
127
+ // 也检查 template 中的 t() 调用
128
+ if (descriptor.template) {
129
+ scriptContent += '\n' + descriptor.template.content
130
+ }
66
131
 
67
- function run(userOptions = {}) {
68
- const options = { ...DEFAULT_OPTIONS, ...userOptions }
69
- const { cwd, scanDirs, silent } = options
132
+ return scriptContent
133
+ } catch (error) {
134
+ console.warn(`⚠️ 解析 Vue 文件失败: ${filePath}`, error.message)
135
+ return ''
136
+ }
137
+ }
70
138
 
71
- let files = []
139
+ /**
140
+ * 查找文件对应的 locale.ts 路径
141
+ */
142
+ function findLocalePath(filePath) {
143
+ const dir = path.dirname(filePath)
144
+ const localePath = path.join(dir, 'locale.ts')
145
+ return localePath
146
+ }
72
147
 
73
- for (const dir of scanDirs) {
74
- const absDir = path.join(cwd, dir)
75
- if (!fs.existsSync(absDir)) {
76
- if (!silent) {
77
- console.warn(`⚠️ 目录不存在:${dir}`)
78
- }
79
- continue
148
+ /**
149
+ * 扫描单个文件
150
+ */
151
+ function scanFile(filePath, results) {
152
+ let code = ''
153
+
154
+ if (filePath.endsWith('.vue')) {
155
+ code = parseVueFile(filePath)
156
+ } else {
157
+ try {
158
+ code = fs.readFileSync(filePath, 'utf-8')
159
+ } catch (error) {
160
+ console.warn(`⚠️ 读取文件失败: ${filePath}`, error.message)
161
+ return
80
162
  }
163
+ }
81
164
 
82
- files.push(
83
- ...glob.sync('**/*.{vue,ts,tsx}', {
84
- cwd: absDir,
85
- absolute: true,
165
+ if (!code) return
166
+
167
+ // 提取 t() 调用的 key
168
+ const usedKeys = extractTKeys(code)
169
+ if (usedKeys.size === 0) return
170
+
171
+ // 检查 locale 导入方式
172
+ const importInfo = checkLocaleImport(code)
173
+
174
+ // 查找对应的 locale.ts
175
+ const localePath = findLocalePath(filePath)
176
+ const localeKeys = loadLocaleKeys(localePath)
177
+
178
+ const relativePath = path.relative(ROOT, filePath)
179
+ const relativeLocalePath = path.relative(ROOT, localePath)
180
+
181
+ // 如果使用了非标准的导入方式,记录(但不检查 key)
182
+ if (!importInfo.hasCorrectImport && importInfo.hasOtherImport) {
183
+ if (!results.invalidImports.has(relativePath)) {
184
+ results.invalidImports.set(relativePath, {
185
+ file: relativePath,
186
+ importPaths: importInfo.importPaths,
187
+ usedKeys: Array.from(usedKeys)
86
188
  })
87
- )
189
+ }
190
+ return // 非标准导入,不检查 key 是否缺失
88
191
  }
89
192
 
90
- const missingKeys = []
91
- const noLocaleUsage = []
92
-
93
- const componentsRoot = path.join(cwd, 'src/components')
94
- const viewsRoot = path.join(cwd, 'src/views')
95
-
96
- for (const file of files) {
97
- const content = fs.readFileSync(file, 'utf-8')
98
-
99
- let scriptCode = content
100
-
101
- if (file.endsWith('.vue')) {
102
- const { descriptor } = parse(content)
103
- scriptCode =
104
- descriptor.scriptSetup?.content ||
105
- descriptor.script?.content ||
106
- ''
193
+ // 如果没有导入 locale,记录(但不检查 key,可能使用全局 locale)
194
+ if (!importInfo.hasCorrectImport && !importInfo.hasOtherImport) {
195
+ if (!results.noLocaleImport.has(relativePath)) {
196
+ results.noLocaleImport.set(relativePath, {
197
+ file: relativePath,
198
+ usedKeys: Array.from(usedKeys)
199
+ })
107
200
  }
201
+ return // 没有导入本地 locale,不检查 key 是否缺失
202
+ }
108
203
 
109
- if (!scriptCode) continue
204
+ // 只有使用标准导入方式(import locale from './locale')时,才检查 key
205
+ // 如果 locale.ts 不存在,记录缺失的 key
206
+ if (!localeKeys) {
207
+ usedKeys.forEach(key => {
208
+ if (!results.missingLocale.has(relativePath)) {
209
+ results.missingLocale.set(relativePath, {
210
+ file: relativePath,
211
+ localePath: relativeLocalePath,
212
+ missingKeys: new Set()
213
+ })
214
+ }
215
+ results.missingLocale.get(relativePath).missingKeys.add(key)
216
+ })
217
+ return
218
+ }
110
219
 
111
- let ast
112
- try {
113
- ast = parseCode(scriptCode)
114
- } catch {
115
- continue
220
+ // 检查哪些 key 在 locale 中不存在
221
+ const missingKeys = Array.from(usedKeys).filter(key => !localeKeys.has(key))
222
+
223
+ if (missingKeys.length > 0) {
224
+ if (!results.missingKeys.has(relativePath)) {
225
+ results.missingKeys.set(relativePath, {
226
+ file: relativePath,
227
+ localePath: relativeLocalePath,
228
+ missingKeys: new Set()
229
+ })
116
230
  }
117
-
118
- const usedKeys = []
119
- let importedLocale = false
120
-
121
- traverse(ast, {
122
- ImportDeclaration(p) {
123
- if (p.node.source.value === './locale') {
124
- importedLocale = true
125
- }
126
- },
127
-
128
- CallExpression(p) {
129
- const callee = p.node.callee
130
- if (
131
- (callee.type === 'Identifier' && (callee.name === 't' || callee.name === '$t'))
132
- ) {
133
- const keyArg = p.node.arguments[0]
134
- const defaultArg = p.node.arguments[1]
135
- if (keyArg && keyArg.type === 'StringLiteral') {
136
- usedKeys.push({
137
- key: keyArg.value,
138
- defaultValue:
139
- defaultArg && defaultArg.type === 'StringLiteral'
140
- ? defaultArg.value
141
- : null,
142
- })
143
- }
144
- }
145
- },
231
+ missingKeys.forEach(key => {
232
+ results.missingKeys.get(relativePath).missingKeys.add(key)
146
233
  })
234
+ }
235
+ }
147
236
 
148
- if (!usedKeys.length) continue
149
-
150
- const fileDir = path.dirname(file)
151
-
152
- /* ---------- views 逻辑 ---------- */
153
- if (file.startsWith(viewsRoot)) {
154
- const localePath = path.join(fileDir, 'locale.ts')
155
- const localeKeys = loadLocaleKeys(localePath)
156
-
157
- if (!importedLocale || !localeKeys) {
158
- noLocaleUsage.push({
159
- file: path.relative(cwd, file),
160
- keys: usedKeys.map(i => i.key),
161
- })
162
- continue
163
- }
164
-
165
- for (const { key, defaultValue } of usedKeys) {
166
- if (!localeKeys.includes(key)) {
167
- missingKeys.push({
168
- file: path.relative(cwd, file),
169
- key,
170
- defaultValue,
171
- })
172
- }
173
- }
237
+ /**
238
+ * 主函数
239
+ */
240
+ function run() {
241
+ const results = {
242
+ invalidImports: new Map(), // 使用了非 './locale' 的导入
243
+ noLocaleImport: new Map(), // 没有导入 locale 但使用了 t()
244
+ missingLocale: new Map(), // locale.ts 文件不存在
245
+ missingKeys: new Map() // locale.ts 存在但缺少某些 key
246
+ }
174
247
 
175
- continue
248
+ // 扫描所有文件
249
+ SCAN_DIRS.forEach(dir => {
250
+ const absDir = path.join(ROOT, dir)
251
+ if (!fs.existsSync(absDir)) {
252
+ console.log(`⚠️ 目录不存在: ${dir}`)
253
+ return
176
254
  }
177
255
 
178
- /* ---------- components 逻辑 ---------- */
179
- if (file.startsWith(componentsRoot)) {
180
- const localePath = findNearestLocale(fileDir, componentsRoot)
181
- const localeKeys = localePath && loadLocaleKeys(localePath)
256
+ FILE_EXT.forEach(ext => {
257
+ const pattern = `${absDir}/**/*.${ext}`
258
+ const files = glob.sync(pattern, { nodir: true })
259
+ files.forEach(file => scanFile(file, results))
260
+ })
261
+ })
182
262
 
183
- if (!localeKeys) {
184
- noLocaleUsage.push({
185
- file: path.relative(cwd, file),
186
- keys: usedKeys.map(i => i.key),
187
- })
188
- continue
189
- }
263
+ // 输出结果
264
+ console.log('\n📋 i18n 使用检查结果\n')
190
265
 
191
- for (const { key, defaultValue } of usedKeys) {
192
- if (!localeKeys.includes(key)) {
193
- missingKeys.push({
194
- file: path.relative(cwd, file),
195
- key,
196
- defaultValue,
197
- })
198
- }
266
+ let hasError = false
267
+
268
+ // 1. 输出非标准导入方式
269
+ if (results.invalidImports.size > 0) {
270
+ hasError = true
271
+ console.log('❌ 使用了非标准的 locale 导入方式(应使用 import locale from \'./locale\'):\n')
272
+ results.invalidImports.forEach((info, file) => {
273
+ console.log(` ${file}`)
274
+ info.importPaths.forEach(importPath => {
275
+ console.log(` ↳ import locale from '${importPath}'`)
276
+ })
277
+ if (info.usedKeys && info.usedKeys.length > 0) {
278
+ console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
199
279
  }
200
- }
280
+ })
281
+ console.log()
201
282
  }
202
283
 
203
- /* ---------------- output ---------------- */
204
-
205
- if (!silent) {
206
- if (missingKeys.length) {
207
- console.error('\n❌ 缺失的国际化 key:')
208
- missingKeys.forEach(i =>
209
- console.error(
210
- ` ${i.file} → ${i.key}${i.defaultValue ? ` (默认值: ${i.defaultValue})` : ''}`
211
- )
212
- )
213
- }
284
+ // 2. 输出没有导入 locale 的文件
285
+ if (results.noLocaleImport.size > 0) {
286
+ console.log('⚠️ 以下文件使用了 t() 但未导入本地 locale(可能使用全局 locale):\n')
287
+ results.noLocaleImport.forEach((info, file) => {
288
+ console.log(` ${file}`)
289
+ console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
290
+ })
291
+ console.log()
292
+ }
214
293
 
215
- if (noLocaleUsage.length) {
216
- console.warn('\n⚠️ 使用了 $t / t 但未找到 locale.ts:')
217
- noLocaleUsage.forEach(i =>
218
- console.warn(` ${i.file}`)
219
- )
220
- }
294
+ // 3. 输出缺少 locale.ts 文件的情况
295
+ if (results.missingLocale.size > 0) {
296
+ hasError = true
297
+ console.log('❌ 以下文件使用了 t() 但缺少对应的 locale.ts 文件:\n')
298
+ results.missingLocale.forEach((info, file) => {
299
+ console.log(` ${file}`)
300
+ console.log(` 期望的 locale 文件: ${info.localePath}`)
301
+ console.log(` 缺失的 key: ${Array.from(info.missingKeys).join(', ')}`)
302
+ })
303
+ console.log()
304
+ }
221
305
 
222
- if (!missingKeys.length && !noLocaleUsage.length) {
223
- console.log('✅ 国际化检查通过(views + components)')
224
- }
306
+ // 4. 输出缺少的 key
307
+ if (results.missingKeys.size > 0) {
308
+ hasError = true
309
+ console.log('❌ 以下文件使用的 key 在 locale.ts 中未定义:\n')
310
+ results.missingKeys.forEach((info, file) => {
311
+ console.log(` ${file}`)
312
+ console.log(` locale 文件: ${info.localePath}`)
313
+ console.log(` 缺失的 key:`)
314
+ Array.from(info.missingKeys).forEach(key => {
315
+ console.log(` - ${key}`)
316
+ })
317
+ })
318
+ console.log()
225
319
  }
226
320
 
227
- return {
228
- success: missingKeys.length === 0,
229
- missingKeys,
230
- noLocaleUsage,
231
- scannedFiles: files.length,
232
- options,
321
+ // 总结
322
+ if (hasError) {
323
+ console.log('❗ 检查未通过,请修复上述问题')
324
+ process.exitCode = 1
325
+ } else {
326
+ console.log('✅ 所有检查通过')
233
327
  }
234
328
  }
235
329
 
236
- /* ---------------- exports ---------------- */
237
-
238
330
  exports.run = run
331
+