td-octopus 0.2.1 → 0.2.3

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": "td-octopus",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "I18N tool",
5
5
  "author": "Anthony Li",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "colors": "^1.4.0",
18
18
  "eslint": "^8.15.0",
19
19
  "eslint-plugin-prettier": "^4.2.1",
20
+ "eslint-plugin-td-rules-plugin": "^1.0.1",
20
21
  "eslint-plugin-unused-imports": "^4.3.0",
21
22
  "glob": "^8.0.3",
22
23
  "google-translate": "^3.0.0",
@@ -1,4 +1,3 @@
1
-
2
1
  const fs = require('fs');
3
2
  const path = require('path');
4
3
  const parser = require('@babel/parser');
@@ -9,36 +8,96 @@ const { ESLint } = require('eslint');
9
8
  const prettier = require('eslint-plugin-prettier');
10
9
  const unusedImports = require('eslint-plugin-unused-imports');
11
10
 
12
- // ===== 工具函数 =====
13
- const astNodeToString = (node) => {
11
+ // ===== 工具函数:判断 key 是否匹配指定名称 =====
12
+ function isKeyNamed(node, name) {
13
+ if (t.isIdentifier(node)) {
14
+ return node.name === name;
15
+ }
14
16
  if (t.isStringLiteral(node)) {
15
- return node.value;
17
+ return node.value === name;
16
18
  }
17
- if (t.isIdentifier(node)) {
18
- return node.name;
19
+ return false;
20
+ }
21
+
22
+ function extractCnKeyDirect(node) {
23
+ if (t.isStringLiteral(node)) {
24
+ return node.value;
19
25
  }
26
+
20
27
  if (t.isMemberExpression(node)) {
21
- const objectStr = astNodeToString(node.object);
22
- const propertyStr = astNodeToString(node.property);
23
- if(objectStr === 'I18N'){
24
- return propertyStr;
28
+ let parts = [];
29
+ let current = node;
30
+
31
+ while (t.isMemberExpression(current)) {
32
+ if (current.computed) return null;
33
+ const prop = current.property;
34
+ if (!t.isIdentifier(prop)) return null;
35
+ parts.unshift(prop.name);
36
+ current = current.object;
37
+ }
38
+
39
+ if (t.isIdentifier(current) && current.name === 'I18N') {
40
+ parts.unshift('I18N');
41
+ } else {
42
+ return null;
43
+ }
44
+
45
+ if (parts.length === 4) {
46
+ return parts.join('.');
25
47
  }
26
- return `${objectStr}.${propertyStr}`;
27
48
  }
28
- return generate(node, { concise: true, comments: false }).code;
49
+
50
+ return null;
29
51
  }
30
52
 
31
- function isKeyNamed(node, name) {
53
+ // ===== 提取 cn 的 key(仅支持:字符串字面量 或 I18N.a.b.c 四层)=====
54
+ function extractCnKey(node, scope) {
55
+ // 情况1: 字符串字面量
56
+ if (t.isStringLiteral(node)) {
57
+ return node.value;
58
+ }
59
+
60
+ // 情况2: Identifier → 查找其定义
32
61
  if (t.isIdentifier(node)) {
33
- return node.name === name;
62
+ const binding = scope.getBinding(node.name);
63
+
64
+ if (binding && binding.path.isVariableDeclarator()) {
65
+ const init = binding.path.node.init;
66
+
67
+ if (init) {
68
+ // 递归解析初始化表达式(不再传 scope,避免无限递归)
69
+ return extractCnKeyDirect(init);
70
+ }
71
+ }
72
+ return null;
34
73
  }
74
+
75
+ // 情况3: 检查是否为 I18N.a.b.c(必须是四层 MemberExpression,非 computed)
76
+ return extractCnKeyDirect(node);
77
+ }
78
+
79
+ function extractEnValue(node, scope) {
80
+ // 直接是字符串字面量
35
81
  if (t.isStringLiteral(node)) {
36
- return node.value === name;
82
+ return node.value;
37
83
  }
38
- return false;
84
+
85
+ // 是变量(Identifier),尝试回溯
86
+ if (t.isIdentifier(node)) {
87
+ const binding = scope.getBinding(node.name);
88
+ if (binding && binding.path.isVariableDeclarator()) {
89
+ const init = binding.path.node.init;
90
+ if (init && t.isStringLiteral(init)) {
91
+ return init.value;
92
+ }
93
+ }
94
+ }
95
+
96
+ // 其他情况(表达式、模板、多变量等)不支持
97
+ return null;
39
98
  }
40
99
 
41
- // ===== 单文件转换函数(接受 code 和 filePath)=====
100
+ // ===== 单文件转换函数 =====
42
101
  const transformToChinese = (code, globalList) => {
43
102
  let localList = {}; // 每个文件独立的 list
44
103
 
@@ -49,57 +108,64 @@ const transformToChinese = (code, globalList) => {
49
108
 
50
109
  const langVariableNames = new Set();
51
110
 
52
- // 第一阶段:找 getLanguage()
111
+ // 第一阶段:收集 getLang / getLanguage 的变量名
53
112
  traverse(ast, {
54
113
  VariableDeclarator(path) {
55
114
  const { id, init } = path.node;
56
- if (t.isIdentifier(id) && t.isCallExpression(init) && t.isIdentifier(init.callee, { name: 'getLanguage' })) {
115
+ if (
116
+ t.isIdentifier(id) &&
117
+ t.isCallExpression(init) &&
118
+ t.isIdentifier(init.callee) &&
119
+ ['getLanguage', 'getLang'].includes(init.callee.name)
120
+ ) {
57
121
  langVariableNames.add(id.name);
58
122
  }
59
123
  }
60
124
  });
61
125
 
62
- // 第二阶段:处理多语言对象
126
+ // 第二阶段:处理对象和成员表达式
63
127
  traverse(ast, {
64
128
  ObjectExpression(path) {
65
129
  const props = path.node.properties;
66
130
  if (props.length === 0) return;
67
131
 
68
132
  const cnProp = props.find(
69
- (prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'cn') && !prop.method
70
-
133
+ (prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'cn') && !prop.method
71
134
  );
72
-
73
135
  const enProp = props.find(
74
- (prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'en') && !prop.method && t.isStringLiteral(prop.value)
136
+ (prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'en') && !prop.method
75
137
  );
76
-
138
+
139
+ // 仅当 en 是字符串字面量,且 cn 符合提取规则时,才记录
77
140
  if (cnProp && enProp) {
78
- const cnText = astNodeToString(cnProp.value);
79
- localList[cnText] = enProp.value.value
80
- }
81
-
82
- if (cnProp) {
83
- path.replaceWith(cnProp.value);
141
+ const enStr = extractEnValue(enProp.value, path.scope);
142
+ const cnKey = extractCnKey(cnProp.value, path.scope);
143
+
144
+ if (cnKey !== null && enStr !== null) {
145
+ localList[cnKey] = enStr;
146
+ }
84
147
  }
85
148
  },
86
149
 
87
150
  MemberExpression(path) {
88
151
  const node = path.node;
89
- if (t.isMemberExpression(node.object) && t.isIdentifier(node.property) && langVariableNames.has(node.property.name)) {
90
- path.replaceWith(node.object);
91
- }
92
- },
152
+ if (!node.computed) return;
93
153
 
94
- Identifier(path) {
95
- if (langVariableNames.has(path.node.name)) {
96
- const parent = path.parentPath;
97
- if (parent.isExpressionStatement()) {
98
- parent.remove();
99
- }
154
+ const isLangAccess =
155
+ (t.isIdentifier(node.property) && langVariableNames.has(node.property.name)) ||
156
+ (t.isCallExpression(node.property) &&
157
+ t.isIdentifier(node.property.callee) &&
158
+ ['getLang', 'getLanguage'].includes(node.property.callee.name));
159
+
160
+ if (isLangAccess) {
161
+ // 替换为 obj['cn']
162
+ path.replaceWith(
163
+ t.memberExpression(node.object, t.stringLiteral('cn'), true)
164
+ );
100
165
  }
101
166
  },
102
167
 
168
+ // 清理无用的 lang 变量声明
103
169
  VariableDeclarator(path) {
104
170
  const { id } = path.node;
105
171
  if (t.isIdentifier(id) && langVariableNames.has(id.name)) {
@@ -112,16 +178,26 @@ const transformToChinese = (code, globalList) => {
112
178
  }
113
179
  }
114
180
  }
181
+ },
182
+
183
+ Identifier(path) {
184
+ if (langVariableNames.has(path.node.name)) {
185
+ const parent = path.parentPath;
186
+ if (parent.isExpressionStatement()) {
187
+ parent.remove();
188
+ }
189
+ }
115
190
  }
116
191
  });
117
192
 
118
- // 第三阶段:清理 getLanguage 导入
193
+ // 第三阶段:清理 getLang / getLanguage 导入
119
194
  traverse(ast, {
120
195
  ImportDeclaration(path) {
121
196
  const specifiers = path.node.specifiers;
122
197
  const newSpecifiers = specifiers.filter((spec) => {
123
198
  if (t.isImportSpecifier(spec)) {
124
- return !t.isIdentifier(spec.imported, { name: 'getLanguage' });
199
+ return !t.isIdentifier(spec.imported, { name: 'getLanguage' }) &&
200
+ !t.isIdentifier(spec.imported, { name: 'getLang' });
125
201
  }
126
202
  return true;
127
203
  });
@@ -140,19 +216,16 @@ const transformToChinese = (code, globalList) => {
140
216
  comments: false
141
217
  }).code;
142
218
 
143
- // 合并到全局 list
144
-
219
+ // 合并到全局翻译表
145
220
  Object.assign(globalList, localList);
146
221
  return transformedCode;
147
- }
222
+ };
148
223
 
149
- // ===== ESLint 实例(复用)=====
224
+ // ===== ESLint 实例(用于格式化和清理未使用导入)=====
150
225
  const eslint = new ESLint({
151
226
  fix: true,
152
227
  cwd: process.cwd(),
153
- useEslintrc: false,
154
228
  plugins: {
155
- prettier,
156
229
  'unused-imports': unusedImports
157
230
  },
158
231
  baseConfig: {
@@ -164,15 +237,8 @@ const eslint = new ESLint({
164
237
  }
165
238
  },
166
239
  overrideConfig: {
167
- env: { browser: true, es2021: true, node: true },
168
- extends: ['eslint:recommended'],
169
- parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
240
+ plugins: ["td-rules-plugin"],
170
241
  rules: {
171
- 'no-unused-vars': ['error', { vars: 'all', args: 'none', caughtErrors: 'none', ignoreRestSiblings: true }],
172
- semi: ['error', 'always'],
173
- indent: ['error', 4, { SwitchCase: 1 }],
174
- quotes: ['error', 'single'],
175
- 'comma-dangle': ['error', 'never'],
176
242
  'prettier/prettier': [
177
243
  1,
178
244
  {
@@ -182,14 +248,14 @@ const eslint = new ESLint({
182
248
  singleQuote: true,
183
249
  tabWidth: 4,
184
250
  trailingComma: 'none',
185
- jsxBracketSameLine: true
251
+ jsxBracketSameLine: true,
186
252
  }
187
253
  ]
188
254
  }
189
255
  }
190
256
  });
191
257
 
192
- // ===== 递归读取所有 .js 文件 =====
258
+ // ===== 递归读取所有 .js 文件(排除 index.js)=====
193
259
  const getAllJsFiles = (dir) => {
194
260
  let results = [];
195
261
  const files = fs.readdirSync(dir);
@@ -198,14 +264,13 @@ const getAllJsFiles = (dir) => {
198
264
  const fullPath = path.join(dir, file);
199
265
  const stat = fs.statSync(fullPath);
200
266
  if (stat.isDirectory()) {
201
- results = results.concat(getAllJsFiles(fullPath)); // 递归
202
- } else if (file.endsWith('.js') && !['index.js'].includes(file)) {
267
+ results = results.concat(getAllJsFiles(fullPath));
268
+ } else if (file.endsWith('.js') && file !== 'index.js') {
203
269
  results.push(fullPath);
204
270
  }
205
271
  }
206
-
207
272
  return results;
208
- }
273
+ };
209
274
 
210
275
  // ===== 处理单个文件 =====
211
276
  const processFile = async (filePath, globalList) => {
@@ -216,9 +281,11 @@ const processFile = async (filePath, globalList) => {
216
281
  const finalCode = results[0].output || transformedCode;
217
282
 
218
283
  fs.writeFileSync(filePath, finalCode, 'utf-8');
219
- }
284
+ };
220
285
 
286
+ // ===== 导出接口 =====
221
287
  module.exports = {
222
288
  processFile,
223
- getAllJsFiles
224
- }
289
+ getAllJsFiles,
290
+ transformToChinese // 便于单元测试
291
+ };