feima-shortcuts 0.3.0-beta.1 → 0.3.0-beta.11
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
|
@@ -3,7 +3,9 @@ const inquirer = require("inquirer");
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const feima = require("../src/generate");
|
|
5
5
|
|
|
6
|
-
const checkLocale
|
|
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')
|
|
7
9
|
const { version } = require("../package.json"); // 读取 package.json 中的版本号
|
|
8
10
|
|
|
9
11
|
const program = new Command()
|
|
@@ -19,6 +21,20 @@ program
|
|
|
19
21
|
checkLocale.run();
|
|
20
22
|
})
|
|
21
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()
|
|
36
|
+
})
|
|
37
|
+
|
|
22
38
|
|
|
23
39
|
const run = () => {
|
|
24
40
|
console.log(`🚀 当前版本:v${version}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feima-shortcuts",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.11",
|
|
4
4
|
"description": "快捷指令",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -15,6 +15,9 @@
|
|
|
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",
|
|
@@ -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,271 @@
|
|
|
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
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
cwd: process.cwd(),
|
|
10
|
+
scanDirs: ['src/views', 'src/components'],
|
|
11
|
+
silent: false,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function flattenLocale(obj, prefix = '') {
|
|
15
|
+
let res = []
|
|
16
|
+
for (const k in obj) {
|
|
17
|
+
const key = prefix ? `${prefix}.${k}` : k
|
|
18
|
+
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
19
|
+
res = res.concat(flattenLocale(obj[k], key))
|
|
20
|
+
} else {
|
|
21
|
+
res.push(key)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return res
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadLocaleKeys(localePath) {
|
|
28
|
+
if (!fs.existsSync(localePath)) return null
|
|
29
|
+
const mod = require(localePath)
|
|
30
|
+
const localeObj = mod.default || mod
|
|
31
|
+
return flattenLocale(localeObj)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCode(code) {
|
|
35
|
+
return babelParser.parse(code, {
|
|
36
|
+
sourceType: 'module',
|
|
37
|
+
plugins: ['typescript', 'jsx'],
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* components 目录专用:
|
|
43
|
+
* 向上查找最近含 locale.ts 的目录(包含自身目录)
|
|
44
|
+
* 到达 componentsRoot 停止
|
|
45
|
+
*/
|
|
46
|
+
function findNearestLocale(fileDir, componentsRoot) {
|
|
47
|
+
let current = fileDir
|
|
48
|
+
while (current.startsWith(componentsRoot)) {
|
|
49
|
+
const localePath = path.join(current, 'locale.ts')
|
|
50
|
+
if (fs.existsSync(localePath)) {
|
|
51
|
+
return localePath
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(current)
|
|
54
|
+
if (parent === current) break
|
|
55
|
+
current = parent
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* views 目录专用:
|
|
62
|
+
* 只查找当前文件同目录的 locale.ts
|
|
63
|
+
*/
|
|
64
|
+
function findViewsLocale(fileDir) {
|
|
65
|
+
const localePath = path.join(fileDir, 'locale.ts')
|
|
66
|
+
if (fs.existsSync(localePath)) return localePath
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function run(userOptions = {}) {
|
|
71
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions }
|
|
72
|
+
const { cwd, scanDirs, silent } = options
|
|
73
|
+
|
|
74
|
+
let files = []
|
|
75
|
+
|
|
76
|
+
for (const dir of scanDirs) {
|
|
77
|
+
const absDir = path.join(cwd, dir)
|
|
78
|
+
if (!fs.existsSync(absDir)) {
|
|
79
|
+
if (!silent) console.warn(`⚠️ 目录不存在:${dir}`)
|
|
80
|
+
continue
|
|
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 // 只对 views 生效
|
|
120
|
+
|
|
121
|
+
traverse(ast, {
|
|
122
|
+
ImportDeclaration(p) {
|
|
123
|
+
if (p.node.source.value === './locale') {
|
|
124
|
+
importedLocale = true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
CallExpression(p) {
|
|
128
|
+
const callee = p.node.callee
|
|
129
|
+
|
|
130
|
+
if (file.startsWith(viewsRoot)) {
|
|
131
|
+
// src/views 目录:只检查 t()
|
|
132
|
+
if (
|
|
133
|
+
callee.type === 'Identifier' &&
|
|
134
|
+
callee.name === 't'
|
|
135
|
+
) {
|
|
136
|
+
const keyArg = p.node.arguments[0]
|
|
137
|
+
const defaultArg = p.node.arguments[1]
|
|
138
|
+
if (keyArg && keyArg.type === 'StringLiteral') {
|
|
139
|
+
usedKeys.push({
|
|
140
|
+
key: keyArg.value,
|
|
141
|
+
defaultValue:
|
|
142
|
+
defaultArg && defaultArg.type === 'StringLiteral'
|
|
143
|
+
? defaultArg.value
|
|
144
|
+
: null,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if (file.startsWith(componentsRoot)) {
|
|
149
|
+
// src/components 目录:只检查 $t()
|
|
150
|
+
if (
|
|
151
|
+
callee.type === 'Identifier' &&
|
|
152
|
+
callee.name === '$t'
|
|
153
|
+
) {
|
|
154
|
+
const keyArg = p.node.arguments[0]
|
|
155
|
+
const defaultArg = p.node.arguments[1]
|
|
156
|
+
if (keyArg && keyArg.type === 'StringLiteral') {
|
|
157
|
+
usedKeys.push({
|
|
158
|
+
key: keyArg.value,
|
|
159
|
+
defaultValue:
|
|
160
|
+
defaultArg && defaultArg.type === 'StringLiteral'
|
|
161
|
+
? defaultArg.value
|
|
162
|
+
: null,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (!usedKeys.length) continue
|
|
171
|
+
|
|
172
|
+
const fileDir = path.dirname(file)
|
|
173
|
+
|
|
174
|
+
if (file.startsWith(viewsRoot)) {
|
|
175
|
+
// views 逻辑:只查同目录 locale.ts,且 vue 文件必须导入 locale
|
|
176
|
+
const localePath = findViewsLocale(fileDir)
|
|
177
|
+
const localeKeys = localePath && loadLocaleKeys(localePath)
|
|
178
|
+
|
|
179
|
+
if (file.endsWith('.vue') && !importedLocale) {
|
|
180
|
+
noLocaleUsage.push({
|
|
181
|
+
file: path.relative(cwd, file),
|
|
182
|
+
keys: usedKeys.map(i => i.key),
|
|
183
|
+
reason: 'vue 文件未 import locale',
|
|
184
|
+
})
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!localeKeys) {
|
|
189
|
+
noLocaleUsage.push({
|
|
190
|
+
file: path.relative(cwd, file),
|
|
191
|
+
keys: usedKeys.map(i => i.key),
|
|
192
|
+
reason: '同目录无 locale.ts',
|
|
193
|
+
})
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const { key, defaultValue } of usedKeys) {
|
|
198
|
+
if (!localeKeys.includes(key)) {
|
|
199
|
+
missingKeys.push({
|
|
200
|
+
file: path.relative(cwd, file),
|
|
201
|
+
key,
|
|
202
|
+
defaultValue,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (file.startsWith(componentsRoot)) {
|
|
210
|
+
// components 逻辑:向上找最近 locale.ts
|
|
211
|
+
const localePath = findNearestLocale(fileDir, componentsRoot)
|
|
212
|
+
const localeKeys = localePath && loadLocaleKeys(localePath)
|
|
213
|
+
|
|
214
|
+
if (!localeKeys) {
|
|
215
|
+
noLocaleUsage.push({
|
|
216
|
+
file: path.relative(cwd, file),
|
|
217
|
+
keys: usedKeys.map(i => i.key),
|
|
218
|
+
reason: '未找到可用 locale.ts',
|
|
219
|
+
})
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const { key, defaultValue } of usedKeys) {
|
|
224
|
+
if (!localeKeys.includes(key)) {
|
|
225
|
+
missingKeys.push({
|
|
226
|
+
file: path.relative(cwd, file),
|
|
227
|
+
key,
|
|
228
|
+
defaultValue,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!silent) {
|
|
237
|
+
if (missingKeys.length) {
|
|
238
|
+
console.error('\n❌ 缺失的国际化 key:')
|
|
239
|
+
missingKeys.forEach(i =>
|
|
240
|
+
console.error(
|
|
241
|
+
` ${i.file} → ${i.key}${
|
|
242
|
+
i.defaultValue ? ` (默认值: ${i.defaultValue})` : ''
|
|
243
|
+
}`
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (noLocaleUsage.length) {
|
|
249
|
+
console.warn('\n⚠️ 以下文件使用了 $t / t,但未正确使用 locale.ts:')
|
|
250
|
+
noLocaleUsage.forEach(i =>
|
|
251
|
+
console.warn(
|
|
252
|
+
` ${i.file} → 原因: ${i.reason},涉及 keys: ${i.keys.join(', ')}`
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!missingKeys.length && !noLocaleUsage.length) {
|
|
258
|
+
console.log('✅ 国际化检查通过(views + components)')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
success: missingKeys.length === 0,
|
|
264
|
+
missingKeys,
|
|
265
|
+
noLocaleUsage,
|
|
266
|
+
scannedFiles: files.length,
|
|
267
|
+
options,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
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
|
|
49
|
+
const reg = /t\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,|\))/g
|
|
43
50
|
const set = new Set()
|
|
44
51
|
let m
|
|
45
|
-
while ((m = reg.exec(code)))
|
|
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')
|