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

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
@@ -2,6 +2,10 @@
2
2
  const inquirer = require("inquirer");
3
3
  const { Command } = require('commander')
4
4
  const feima = require("../src/generate");
5
+
6
+ const checkLocale = require('../src/scripts/check-locale')
7
+ const checkI18nDefault = require('../src/scripts/check-i18n-default')
8
+ const checkI18nViews = require('../src/scripts/check-i18n-views')
5
9
  const { version } = require("../package.json"); // 读取 package.json 中的版本号
6
10
 
7
11
  const program = new Command()
@@ -14,7 +18,21 @@ program
14
18
  .command('check-locale')
15
19
  .description('检查未使用的 locale')
16
20
  .action(() => {
17
- require('../src/scripts/check-locale')
21
+ checkLocale.run();
22
+ })
23
+
24
+ program
25
+ .command('check-i18n-default')
26
+ .description('检查未设置默认值的 t')
27
+ .action(() => {
28
+ checkI18nDefault.run();
29
+ })
30
+
31
+ program
32
+ .command('check-i18n-views')
33
+ .description('检查已使用未创建的 key')
34
+ .action(() => {
35
+ checkI18nViews.run()
18
36
  })
19
37
 
20
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feima-shortcuts",
3
- "version": "0.3.0-beta.0",
3
+ "version": "0.3.0-beta.10",
4
4
  "description": "快捷指令",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -15,11 +15,16 @@
15
15
  "author": "1198810568@qq.com",
16
16
  "license": "ISC",
17
17
  "dependencies": {
18
+ "@babel/parser": "^7.28.5",
19
+ "@babel/traverse": "^7.28.5",
20
+ "@vue/compiler-sfc": "^3.5.26",
18
21
  "chalk": "^4.1.0",
19
22
  "commander": "^5.1.0",
20
23
  "download-git-repo": "^3.0.2",
21
24
  "fs-extra": "^9.0.1",
25
+ "glob": "^13.0.0",
22
26
  "inquirer": "^7.3.0",
27
+ "typescript": "^5.9.3",
23
28
  "uuid": "^9.0.0"
24
29
  },
25
30
  "packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * i18n 默认值检查脚本
5
+ *
6
+ * 规则:
7
+ * - 扫描 src/components、src/views
8
+ * - 支持 t / $t
9
+ * - 识别是否存在默认值(第二个参数)
10
+ * - 汇总缺失默认值的 key
11
+ */
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const glob = require('glob')
16
+
17
+ const ROOT = process.cwd()
18
+ const SCAN_DIRS = ['src/components', 'src/views']
19
+ const FILE_EXT = '{ts,js,tsx,jsx,vue}'
20
+
21
+ // 匹配:t('xxx') / t("xxx", "默认值")
22
+ const I18N_REG = /\b(\$?t)\s*\(\s*(['"])([^'"]+)\2\s*(?:,\s*(['"])([^'"]*)\4)?\s*\)/g
23
+
24
+ /** 结果结构 */
25
+ const resultMap = new Map()
26
+ const missingList = []
27
+
28
+ function scanFile(filePath) {
29
+ const code = fs.readFileSync(filePath, 'utf-8')
30
+ let match
31
+
32
+ while ((match = I18N_REG.exec(code))) {
33
+ const [, fn, , key, , defaultValue] = match
34
+
35
+ if (!resultMap.has(key)) {
36
+ resultMap.set(key, {
37
+ key,
38
+ defaultValue: defaultValue || '',
39
+ locations: [],
40
+ })
41
+ }
42
+
43
+ const record = resultMap.get(key)
44
+ record.locations.push({
45
+ file: path.relative(ROOT, filePath),
46
+ fn,
47
+ })
48
+
49
+ // 有一次缺失默认值就标记
50
+ if (!defaultValue) {
51
+ missingList.push({
52
+ key,
53
+ file: path.relative(ROOT, filePath),
54
+ })
55
+ }
56
+ }
57
+ }
58
+
59
+ function run() {
60
+ SCAN_DIRS.forEach((dir) => {
61
+ const absDir = path.join(ROOT, dir)
62
+ if (!fs.existsSync(absDir)) return
63
+
64
+ const files = glob.sync(`${absDir}/**/*.${FILE_EXT}`, {
65
+ nodir: true,
66
+ })
67
+
68
+ files.forEach(scanFile)
69
+ })
70
+
71
+ console.log('\n📦 i18n 使用统计\n')
72
+
73
+ console.log(`共发现 key 数量:${resultMap.size}`)
74
+ console.log(`缺失默认值使用次数:${missingList.length}\n`)
75
+
76
+ if (missingList.length) {
77
+ console.log('❌ 以下 key 缺失默认值:\n')
78
+
79
+ const uniq = new Map()
80
+ missingList.forEach((i) => {
81
+ if (!uniq.has(i.key)) uniq.set(i.key, [])
82
+ uniq.get(i.key).push(i.file)
83
+ })
84
+
85
+ uniq.forEach((files, key) => {
86
+ console.log(`- ${key}`)
87
+ files.forEach((f) => {
88
+ console.log(` ↳ ${f}`)
89
+ })
90
+ })
91
+
92
+ console.log('\n⚠️ 请为以上 key 补充默认值')
93
+ process.exitCode = 1
94
+ } else {
95
+ console.log('✅ 所有 i18n key 均设置了默认值')
96
+ }
97
+ }
98
+
99
+ exports.run = run
@@ -0,0 +1,238 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const glob = require('glob')
4
+ const { parse } = require('@vue/compiler-sfc')
5
+ const babelParser = require('@babel/parser')
6
+ const traverse = require('@babel/traverse').default
7
+
8
+ /* ---------------- defaults ---------------- */
9
+
10
+ const DEFAULT_OPTIONS = {
11
+ cwd: process.cwd(),
12
+ scanDirs: ['src/views', 'src/components'],
13
+ silent: false,
14
+ }
15
+
16
+ /* ---------------- utils ---------------- */
17
+
18
+ function flattenLocale(obj, prefix = '') {
19
+ let res = []
20
+ for (const k in obj) {
21
+ const key = prefix ? `${prefix}.${k}` : k
22
+ if (typeof obj[k] === 'object' && obj[k] !== null) {
23
+ res = res.concat(flattenLocale(obj[k], key))
24
+ } else {
25
+ res.push(key)
26
+ }
27
+ }
28
+ return res
29
+ }
30
+
31
+ 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
+ }
37
+
38
+ function parseCode(code) {
39
+ return babelParser.parse(code, {
40
+ sourceType: 'module',
41
+ plugins: ['typescript', 'jsx'],
42
+ })
43
+ }
44
+
45
+ /**
46
+ * 从当前文件目录开始,向上查找最近的 locale.ts
47
+ * 直到到达 componentsRoot 为止
48
+ */
49
+ function findNearestLocale(fileDir, componentsRoot) {
50
+ let current = fileDir
51
+
52
+ while (current.startsWith(componentsRoot)) {
53
+ const localePath = path.join(current, 'locale.ts')
54
+ if (fs.existsSync(localePath)) {
55
+ return localePath
56
+ }
57
+ const parent = path.dirname(current)
58
+ if (parent === current) break
59
+ current = parent
60
+ }
61
+
62
+ return null
63
+ }
64
+
65
+ /* ---------------- core ---------------- */
66
+
67
+ function run(userOptions = {}) {
68
+ const options = { ...DEFAULT_OPTIONS, ...userOptions }
69
+ const { cwd, scanDirs, silent } = options
70
+
71
+ let files = []
72
+
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
80
+ }
81
+
82
+ files.push(
83
+ ...glob.sync('**/*.{vue,ts,tsx}', {
84
+ cwd: absDir,
85
+ absolute: true,
86
+ })
87
+ )
88
+ }
89
+
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
+ ''
107
+ }
108
+
109
+ if (!scriptCode) continue
110
+
111
+ let ast
112
+ try {
113
+ ast = parseCode(scriptCode)
114
+ } catch {
115
+ continue
116
+ }
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
+ },
146
+ })
147
+
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
+ }
174
+
175
+ continue
176
+ }
177
+
178
+ /* ---------- components 逻辑 ---------- */
179
+ if (file.startsWith(componentsRoot)) {
180
+ const localePath = findNearestLocale(fileDir, componentsRoot)
181
+ const localeKeys = localePath && loadLocaleKeys(localePath)
182
+
183
+ if (!localeKeys) {
184
+ noLocaleUsage.push({
185
+ file: path.relative(cwd, file),
186
+ keys: usedKeys.map(i => i.key),
187
+ })
188
+ continue
189
+ }
190
+
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
+ }
199
+ }
200
+ }
201
+ }
202
+
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
+ }
214
+
215
+ if (noLocaleUsage.length) {
216
+ console.warn('\n⚠️ 使用了 $t / t 但未找到 locale.ts:')
217
+ noLocaleUsage.forEach(i =>
218
+ console.warn(` ${i.file}`)
219
+ )
220
+ }
221
+
222
+ if (!missingKeys.length && !noLocaleUsage.length) {
223
+ console.log('✅ 国际化检查通过(views + components)')
224
+ }
225
+ }
226
+
227
+ return {
228
+ success: missingKeys.length === 0,
229
+ missingKeys,
230
+ noLocaleUsage,
231
+ scannedFiles: files.length,
232
+ options,
233
+ }
234
+ }
235
+
236
+ /* ---------------- exports ---------------- */
237
+
238
+ exports.run = run
@@ -4,7 +4,8 @@ const path = require('path')
4
4
  const glob = require('glob')
5
5
  const ts = require('typescript')
6
6
 
7
- /* 工具函数,保持你原逻辑 */
7
+ /* ================= 工具函数 ================= */
8
+
8
9
  function flattenLocale(obj, prefix = '') {
9
10
  let res = []
10
11
  for (const k in obj) {
@@ -38,11 +39,19 @@ function loadLocaleKeys(localePath) {
38
39
  return obj ? flattenLocale(obj) : []
39
40
  }
40
41
 
42
+ /**
43
+ * 提取使用到的 locale key
44
+ * 兼容:
45
+ * t('a.b.c')
46
+ * t('a.b.c', 'fallback')
47
+ */
41
48
  function extractUsedKeys(code) {
42
- const reg = /t\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g
49
+ const reg = /t\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,|\))/g
43
50
  const set = new Set()
44
51
  let m
45
- while ((m = reg.exec(code))) set.add(m[1])
52
+ while ((m = reg.exec(code))) {
53
+ set.add(m[1])
54
+ }
46
55
  return set
47
56
  }
48
57
 
@@ -77,7 +86,8 @@ function detectUseLocale(code) {
77
86
  }
78
87
  }
79
88
 
80
- /* 页面 locale 检测 */
89
+ /* ================= 页面 locale 检测 ================= */
90
+
81
91
  function checkPageLocale(pageDir) {
82
92
  const localePath = path.join(pageDir, 'locale.ts')
83
93
  const indexVue = path.join(pageDir, 'index.vue')
@@ -121,7 +131,8 @@ function checkPageLocale(pageDir) {
121
131
  return false
122
132
  }
123
133
 
124
- /* 组件 locale 检测 */
134
+ /* ================= 组件 locale 检测 ================= */
135
+
125
136
  function checkComponentLocale(componentDir) {
126
137
  const localePath = path.join(componentDir, 'locale.ts')
127
138
  if (!fs.existsSync(localePath)) return false
@@ -184,7 +195,7 @@ function checkComponentLocale(componentDir) {
184
195
  return hasError
185
196
  }
186
197
 
187
- /* 主入口 */
198
+ /* ================= 主入口 ================= */
188
199
 
189
200
  function run() {
190
201
  const root = path.resolve(process.cwd(), 'src/views')
@@ -203,15 +214,11 @@ function run() {
203
214
  })
204
215
 
205
216
  if (hasError) {
206
- console.log('\n❗ locale 治理未通过')
217
+ console.log('\n❗ locale 检查未通过')
207
218
  process.exit(1)
208
219
  } else {
209
- console.log('\n✅ locale 治理通过')
220
+ console.log('\n✅ locale 检查通过')
210
221
  }
211
222
  }
212
223
 
213
- if (require.main === module) {
214
- run()
215
- }
216
-
217
- module.exports = run
224
+ exports.run = run