react-i18next-scanner 0.1.1 → 0.1.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/README.md +81 -5
- package/dist/config/loadConfig.js +4 -0
- package/dist/core/actions/removeUnusedTranslations.js +2 -2
- package/dist/core/actions/syncMissingTranslations.js +2 -2
- package/dist/core/actions/writeDynamicTranslationReport.js +14 -0
- package/dist/core/analysis/detectDynamicTranslationUsages.js +180 -0
- package/dist/core/runSyncMissing.js +1 -1
- package/dist/core/scanner.js +6 -2
- package/package.json +5 -7
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ CLI tool to scan React projects for missing and unused `react-i18next` translati
|
|
|
11
11
|
- Sync missing keys into translation JSON files
|
|
12
12
|
- Remove unused keys from translation JSON files
|
|
13
13
|
- Sort translation JSON files alphabetically
|
|
14
|
-
-
|
|
14
|
+
- Detect dynamic translation usages and write them into a separate report
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -37,6 +37,8 @@ Create one of these config files in your project root:
|
|
|
37
37
|
- `i18n-scanner.config.cjs`
|
|
38
38
|
- `i18n-scanner.config.mjs`
|
|
39
39
|
|
|
40
|
+
> `.ts` config is not supported yet.
|
|
41
|
+
|
|
40
42
|
Example:
|
|
41
43
|
|
|
42
44
|
```js
|
|
@@ -112,6 +114,20 @@ If `true`, unused keys will be removed from translation JSON files.
|
|
|
112
114
|
|
|
113
115
|
## Commands
|
|
114
116
|
|
|
117
|
+
### Initialize config
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx react-i18next-scanner init
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This command creates a config file automatically and lets you choose:
|
|
124
|
+
|
|
125
|
+
- `.js`
|
|
126
|
+
- `.mjs`
|
|
127
|
+
- `.cjs`
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
115
131
|
### Scan project
|
|
116
132
|
|
|
117
133
|
```bash
|
|
@@ -130,6 +146,7 @@ What it does:
|
|
|
130
146
|
- extracts translation keys
|
|
131
147
|
- compares them with translation JSON files
|
|
132
148
|
- writes unused translation reports
|
|
149
|
+
- writes dynamic translation usage reports
|
|
133
150
|
- optionally syncs missing keys
|
|
134
151
|
- optionally removes unused keys
|
|
135
152
|
|
|
@@ -178,7 +195,40 @@ These files contain only unused translations, so you can review them safely befo
|
|
|
178
195
|
|
|
179
196
|
---
|
|
180
197
|
|
|
181
|
-
##
|
|
198
|
+
## Dynamic Translation Report
|
|
199
|
+
|
|
200
|
+
If the scanner finds dynamic translation usage such as:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
t(getDescription());
|
|
204
|
+
t(labelKey);
|
|
205
|
+
t(transaction.descriptionKey);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
it creates a report file:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
reports-dynamic/dynamic-translation-usages.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Example structure:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"src/views/Account/BalancesTransactionsPage/PaymentDescription.tsx": [
|
|
219
|
+
{
|
|
220
|
+
"namespaces": ["accountInformation"],
|
|
221
|
+
"expression": "getDescription()",
|
|
222
|
+
"reason": "Object-based non-literal translation key",
|
|
223
|
+
"suggestion": "Translation key may come from imported object(s) or helper functions. Manual or deeper analysis may be required."
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Supported Translation Patterns
|
|
182
232
|
|
|
183
233
|
The scanner currently detects translation keys used with:
|
|
184
234
|
|
|
@@ -187,6 +237,31 @@ The scanner currently detects translation keys used with:
|
|
|
187
237
|
- `<Trans i18nKey="key" />`
|
|
188
238
|
- `useTranslation("namespace")`
|
|
189
239
|
|
|
240
|
+
It can also report dynamic usages where translation keys are not passed directly as string literals.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Supported Files
|
|
245
|
+
|
|
246
|
+
### Source files
|
|
247
|
+
|
|
248
|
+
The scanner can scan:
|
|
249
|
+
|
|
250
|
+
- `.js`
|
|
251
|
+
- `.jsx`
|
|
252
|
+
- `.ts`
|
|
253
|
+
- `.tsx`
|
|
254
|
+
|
|
255
|
+
### Config files
|
|
256
|
+
|
|
257
|
+
The scanner currently supports:
|
|
258
|
+
|
|
259
|
+
- `.js`
|
|
260
|
+
- `.cjs`
|
|
261
|
+
- `.mjs`
|
|
262
|
+
|
|
263
|
+
> `.ts` config is not supported yet.
|
|
264
|
+
|
|
190
265
|
---
|
|
191
266
|
|
|
192
267
|
## Example Workflow
|
|
@@ -252,9 +327,10 @@ npx react-i18next-scanner scan
|
|
|
252
327
|
|
|
253
328
|
## Notes
|
|
254
329
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
330
|
+
- `syncMissing` does not translate values automatically. It only adds missing keys with empty values.
|
|
331
|
+
- `removeUnused` should be used carefully after reviewing unused translation reports.
|
|
332
|
+
- Dynamic translation usages are written into a separate report and are not automatically resolved.
|
|
333
|
+
- Config paths are resolved relative to the config file location.
|
|
258
334
|
|
|
259
335
|
---
|
|
260
336
|
|
|
@@ -41,6 +41,9 @@ function validateConfig(config) {
|
|
|
41
41
|
if (cfg.syncMissing !== undefined && typeof cfg.syncMissing !== "boolean") {
|
|
42
42
|
throw new Error("syncMissing must be a boolean.");
|
|
43
43
|
}
|
|
44
|
+
if (cfg.sortJson !== undefined && typeof cfg.sortJson !== "boolean") {
|
|
45
|
+
throw new Error("sortJson must be a boolean.");
|
|
46
|
+
}
|
|
44
47
|
if (cfg.outputDir !== undefined && typeof cfg.outputDir !== "string") {
|
|
45
48
|
throw new Error("outputDir must be a string.");
|
|
46
49
|
}
|
|
@@ -52,6 +55,7 @@ function normalizeConfig(config, baseDir) {
|
|
|
52
55
|
sortJsonFiles: config.sortJsonFiles?.map((p) => path.resolve(baseDir, p)),
|
|
53
56
|
removeUnused: config.removeUnused ?? false,
|
|
54
57
|
syncMissing: config.syncMissing ?? false,
|
|
58
|
+
sortJson: config.sortJson ?? false,
|
|
55
59
|
outputDir: path.resolve(baseDir, config.outputDir ?? "unused-translations"),
|
|
56
60
|
};
|
|
57
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import { removeKey } from "../utils/removeKey.js";
|
|
3
3
|
import { sortObject } from "../utils/sortObject.js";
|
|
4
|
-
export async function removeUnusedTranslations(result, jsonFiles) {
|
|
4
|
+
export async function removeUnusedTranslations(result, jsonFiles, sortJson) {
|
|
5
5
|
const { unusedKeysByFile } = result;
|
|
6
6
|
for (const file of jsonFiles) {
|
|
7
7
|
const unusedKeys = unusedKeysByFile[file.filePath] ?? [];
|
|
@@ -19,7 +19,7 @@ export async function removeUnusedTranslations(result, jsonFiles) {
|
|
|
19
19
|
if (!updated) {
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
-
const sorted = sortObject(file.data);
|
|
22
|
+
const sorted = sortJson ? sortObject(file.data) : file.data;
|
|
23
23
|
file.data = sorted;
|
|
24
24
|
await fs.writeFile(file.filePath, JSON.stringify(sorted, null, 2), "utf-8");
|
|
25
25
|
console.log(`\n✅ Unused keys removed: ${file.filePath}`);
|
|
@@ -2,7 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { addKey } from "../utils/addKey.js";
|
|
3
3
|
import { sortObject } from "../utils/sortObject.js";
|
|
4
4
|
import { isValidTranslationKey } from "../utils/isValidTranslationKey.js";
|
|
5
|
-
export async function syncMissingTranslations(result, jsonFiles) {
|
|
5
|
+
export async function syncMissingTranslations(result, jsonFiles, sortJson) {
|
|
6
6
|
const { missingKeysByFile } = result;
|
|
7
7
|
for (const file of jsonFiles) {
|
|
8
8
|
const missingKeys = missingKeysByFile[file.filePath] ?? [];
|
|
@@ -23,7 +23,7 @@ export async function syncMissingTranslations(result, jsonFiles) {
|
|
|
23
23
|
if (!updated) {
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
|
-
const sorted = sortObject(file.data);
|
|
26
|
+
const sorted = sortJson ? sortObject(file.data) : file.data;
|
|
27
27
|
file.data = sorted;
|
|
28
28
|
await fs.writeFile(file.filePath, JSON.stringify(sorted, null, 2), "utf-8");
|
|
29
29
|
console.log(`\n✅ Missing keys synced: ${file.filePath}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { sortObject } from "../utils/sortObject.js";
|
|
4
|
+
const DEFAULT_DYNAMIC_REPORT_DIR = path.resolve(process.cwd(), "reports-dynamic");
|
|
5
|
+
export async function writeDynamicTranslationReport(report, outputDir = DEFAULT_DYNAMIC_REPORT_DIR) {
|
|
6
|
+
if (Object.keys(report).length === 0) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
10
|
+
const reportPath = path.join(outputDir, "dynamic-translation-usages.json");
|
|
11
|
+
const sortedReport = sortObject(report);
|
|
12
|
+
await fs.writeFile(reportPath, `${JSON.stringify(sortedReport, null, 2)}\n`, "utf-8");
|
|
13
|
+
console.log(`📝 Dynamic translation report created: ${reportPath}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
function getScriptKind(filePath) {
|
|
5
|
+
if (filePath.endsWith(".tsx"))
|
|
6
|
+
return ts.ScriptKind.TSX;
|
|
7
|
+
if (filePath.endsWith(".ts"))
|
|
8
|
+
return ts.ScriptKind.TS;
|
|
9
|
+
if (filePath.endsWith(".jsx"))
|
|
10
|
+
return ts.ScriptKind.JSX;
|
|
11
|
+
return ts.ScriptKind.JS;
|
|
12
|
+
}
|
|
13
|
+
function createSourceFile(filePath, content) {
|
|
14
|
+
return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
15
|
+
}
|
|
16
|
+
function extractImportMap(sourceFile) {
|
|
17
|
+
const imports = new Map();
|
|
18
|
+
sourceFile.forEachChild((node) => {
|
|
19
|
+
if (!ts.isImportDeclaration(node))
|
|
20
|
+
return;
|
|
21
|
+
if (!node.importClause || !node.moduleSpecifier)
|
|
22
|
+
return;
|
|
23
|
+
if (!ts.isStringLiteral(node.moduleSpecifier))
|
|
24
|
+
return;
|
|
25
|
+
const modulePath = node.moduleSpecifier.text;
|
|
26
|
+
const importClause = node.importClause;
|
|
27
|
+
if (importClause.name) {
|
|
28
|
+
imports.set(importClause.name.text, modulePath);
|
|
29
|
+
}
|
|
30
|
+
if (importClause.namedBindings &&
|
|
31
|
+
ts.isNamedImports(importClause.namedBindings)) {
|
|
32
|
+
for (const element of importClause.namedBindings.elements) {
|
|
33
|
+
const localName = element.name.text;
|
|
34
|
+
imports.set(localName, modulePath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return imports;
|
|
39
|
+
}
|
|
40
|
+
function extractNamespaces(sourceFile) {
|
|
41
|
+
const namespaces = new Set();
|
|
42
|
+
function visit(node) {
|
|
43
|
+
if (ts.isCallExpression(node) &&
|
|
44
|
+
ts.isIdentifier(node.expression) &&
|
|
45
|
+
node.expression.text === "useTranslation") {
|
|
46
|
+
const firstArg = node.arguments[0];
|
|
47
|
+
if (!firstArg) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (ts.isStringLiteral(firstArg) ||
|
|
51
|
+
ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
52
|
+
namespaces.add(firstArg.text);
|
|
53
|
+
}
|
|
54
|
+
if (ts.isArrayLiteralExpression(firstArg)) {
|
|
55
|
+
for (const element of firstArg.elements) {
|
|
56
|
+
if (ts.isStringLiteral(element) ||
|
|
57
|
+
ts.isNoSubstitutionTemplateLiteral(element)) {
|
|
58
|
+
namespaces.add(element.text);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ts.forEachChild(node, visit);
|
|
64
|
+
}
|
|
65
|
+
visit(sourceFile);
|
|
66
|
+
return [...namespaces];
|
|
67
|
+
}
|
|
68
|
+
function isTranslationCall(node) {
|
|
69
|
+
if (ts.isIdentifier(node.expression)) {
|
|
70
|
+
return node.expression.text === "t";
|
|
71
|
+
}
|
|
72
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
73
|
+
return node.expression.name.text === "t";
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function isDirectLiteralTranslationArg(node) {
|
|
78
|
+
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
|
|
79
|
+
}
|
|
80
|
+
function getExpressionKind(node) {
|
|
81
|
+
if (ts.isTemplateExpression(node))
|
|
82
|
+
return "template-literal";
|
|
83
|
+
if (ts.isIdentifier(node))
|
|
84
|
+
return "identifier";
|
|
85
|
+
if (ts.isCallExpression(node))
|
|
86
|
+
return "function-call";
|
|
87
|
+
if (ts.isPropertyAccessExpression(node))
|
|
88
|
+
return "property-access";
|
|
89
|
+
if (ts.isElementAccessExpression(node))
|
|
90
|
+
return "element-access";
|
|
91
|
+
if (ts.isBinaryExpression(node))
|
|
92
|
+
return "binary-expression";
|
|
93
|
+
if (ts.isConditionalExpression(node))
|
|
94
|
+
return "conditional-expression";
|
|
95
|
+
return "expression";
|
|
96
|
+
}
|
|
97
|
+
function findRelatedImports(expressionText, importMap) {
|
|
98
|
+
const related = [];
|
|
99
|
+
for (const [localName, source] of importMap.entries()) {
|
|
100
|
+
const identifierRegex = new RegExp(`\\b${localName}\\b`);
|
|
101
|
+
if (identifierRegex.test(expressionText)) {
|
|
102
|
+
related.push(`${localName} -> ${source}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return related;
|
|
106
|
+
}
|
|
107
|
+
function buildFinding(namespaces, expression, kind, importHints) {
|
|
108
|
+
let reason = "Non-literal translation key";
|
|
109
|
+
let suggestion = "Translation key is not passed as a direct string literal. Manual or deeper analysis may be required.";
|
|
110
|
+
if (kind === "template-literal") {
|
|
111
|
+
reason = "Template-literal translation key";
|
|
112
|
+
suggestion =
|
|
113
|
+
"Translation key uses a template literal with dynamic interpolation. Manual or deeper analysis may be required.";
|
|
114
|
+
}
|
|
115
|
+
if (kind === "function-call") {
|
|
116
|
+
reason = "Function-based translation key";
|
|
117
|
+
suggestion =
|
|
118
|
+
"Translation key is returned from a function call. Manual or deeper analysis may be required.";
|
|
119
|
+
}
|
|
120
|
+
if (kind === "property-access" ||
|
|
121
|
+
kind === "element-access" ||
|
|
122
|
+
kind === "identifier") {
|
|
123
|
+
reason = "Object-based non-literal translation key";
|
|
124
|
+
suggestion =
|
|
125
|
+
"Translation key may come from an object, variable, or imported map. Manual or deeper analysis may be required.";
|
|
126
|
+
}
|
|
127
|
+
if (importHints.length > 0) {
|
|
128
|
+
suggestion += ` Related imports: ${importHints.join(", ")}.`;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
namespaces,
|
|
132
|
+
expression,
|
|
133
|
+
reason,
|
|
134
|
+
suggestion,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function collectDynamicFromExpression(expression, namespaces, importMap, sourceFile, bucket) {
|
|
138
|
+
if (isDirectLiteralTranslationArg(expression)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (ts.isConditionalExpression(expression)) {
|
|
142
|
+
collectDynamicFromExpression(expression.whenTrue, namespaces, importMap, sourceFile, bucket);
|
|
143
|
+
collectDynamicFromExpression(expression.whenFalse, namespaces, importMap, sourceFile, bucket);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const expressionText = expression.getText(sourceFile).trim();
|
|
147
|
+
const kind = getExpressionKind(expression);
|
|
148
|
+
const importHints = findRelatedImports(expressionText, importMap);
|
|
149
|
+
bucket.push(buildFinding(namespaces, expressionText, kind, importHints));
|
|
150
|
+
}
|
|
151
|
+
export async function detectDynamicTranslationUsages(files) {
|
|
152
|
+
const report = {};
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
const content = await fs.readFile(file, "utf-8");
|
|
155
|
+
if (!content.includes("t(")) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const sourceFile = createSourceFile(file, content);
|
|
159
|
+
const namespaces = extractNamespaces(sourceFile);
|
|
160
|
+
const importMap = extractImportMap(sourceFile);
|
|
161
|
+
const findings = [];
|
|
162
|
+
function visit(node) {
|
|
163
|
+
if (ts.isCallExpression(node) && isTranslationCall(node)) {
|
|
164
|
+
const firstArg = node.arguments[0];
|
|
165
|
+
if (firstArg) {
|
|
166
|
+
collectDynamicFromExpression(firstArg, namespaces, importMap, sourceFile, findings);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
ts.forEachChild(node, visit);
|
|
170
|
+
}
|
|
171
|
+
visit(sourceFile);
|
|
172
|
+
if (findings.length > 0) {
|
|
173
|
+
const relativePath = path
|
|
174
|
+
.relative(process.cwd(), file)
|
|
175
|
+
.replace(/\\/g, "/");
|
|
176
|
+
report[relativePath] = findings;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return report;
|
|
180
|
+
}
|
|
@@ -9,6 +9,6 @@ export async function runSyncMissing(config) {
|
|
|
9
9
|
const usedKeys = await extractKeysFromFiles(files);
|
|
10
10
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
11
11
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
12
|
-
await syncMissingTranslations(result, jsonData);
|
|
12
|
+
await syncMissingTranslations(result, jsonData, config.sortJson);
|
|
13
13
|
return result;
|
|
14
14
|
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -2,20 +2,24 @@ import { loadJsonKeys } from "./io/jsonLoader.js";
|
|
|
2
2
|
import { getFiles } from "./io/getFiles.js";
|
|
3
3
|
import { analyzeKeys } from "./analysis/analyzer.js";
|
|
4
4
|
import { extractKeysFromFiles } from "./analysis/extractKeysFromFiles.js";
|
|
5
|
+
import { detectDynamicTranslationUsages } from "./analysis/detectDynamicTranslationUsages.js";
|
|
6
|
+
import { writeDynamicTranslationReport } from "./actions/writeDynamicTranslationReport.js";
|
|
5
7
|
import { syncMissingTranslations } from "./actions/syncMissingTranslations.js";
|
|
6
8
|
import { removeUnusedTranslations } from "./actions/removeUnusedTranslations.js";
|
|
7
9
|
import { writeUnusedTranslationsReport } from "./actions/writeUnusedTranslationsReport.js";
|
|
8
10
|
export async function runScanner(config) {
|
|
9
11
|
const files = await getFiles(config.srcPaths);
|
|
12
|
+
const dynamicUsages = await detectDynamicTranslationUsages(files);
|
|
13
|
+
await writeDynamicTranslationReport(dynamicUsages);
|
|
10
14
|
const usedKeys = await extractKeysFromFiles(files);
|
|
11
15
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
12
16
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
13
17
|
await writeUnusedTranslationsReport(result, jsonData, config.outputDir);
|
|
14
18
|
if (config.syncMissing) {
|
|
15
|
-
await syncMissingTranslations(result, jsonData);
|
|
19
|
+
await syncMissingTranslations(result, jsonData, config.sortJson);
|
|
16
20
|
}
|
|
17
21
|
if (config.removeUnused) {
|
|
18
|
-
await removeUnusedTranslations(result, jsonData);
|
|
22
|
+
await removeUnusedTranslations(result, jsonData, config.sortJson);
|
|
19
23
|
}
|
|
20
24
|
return result;
|
|
21
25
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-i18next-scanner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI tool to scan React projects for missing and unused react-i18next translation keys",
|
|
5
|
-
"bin":
|
|
6
|
-
"react-i18next-scanner": "./dist/cli/cli.js"
|
|
7
|
-
},
|
|
5
|
+
"bin": "./dist/cli/cli.js",
|
|
8
6
|
"scripts": {
|
|
9
7
|
"build": "tsc",
|
|
10
8
|
"dev": "tsx src/cli/cli.ts",
|
|
@@ -26,12 +24,12 @@
|
|
|
26
24
|
],
|
|
27
25
|
"devDependencies": {
|
|
28
26
|
"@types/node": "^24.6.2",
|
|
29
|
-
"tsx": "^4.22.4"
|
|
30
|
-
"typescript": "^5.9.3"
|
|
27
|
+
"tsx": "^4.22.4"
|
|
31
28
|
},
|
|
32
29
|
"dependencies": {
|
|
33
30
|
"chalk": "^5.6.2",
|
|
34
31
|
"commander": "^15.0.0",
|
|
35
|
-
"fast-glob": "^3.3.3"
|
|
32
|
+
"fast-glob": "^3.3.3",
|
|
33
|
+
"typescript": "^6.0.3"
|
|
36
34
|
}
|
|
37
35
|
}
|