ai-localize-codemods 2.0.1 → 2.0.4
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/CHANGELOG.md +20 -0
- package/package.json +24 -2
- package/src/angular-codemod.ts +0 -112
- package/src/cdn-replacer.ts +0 -102
- package/src/codemod-runner.ts +0 -132
- package/src/index.ts +0 -5
- package/src/react-codemod.ts +0 -416
- package/src/vue-codemod.ts +0 -106
- package/tsconfig.json +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# ai-localize-codemods
|
|
2
2
|
|
|
3
|
+
## 2.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- ai-localize-shared@2.0.3
|
|
9
|
+
- ai-localize-config@2.0.3
|
|
10
|
+
|
|
11
|
+
## 2.0.2
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- **Bracket-notation accessor support** — `codemods.accessorStyle: "function" | "bracket"` (default `"function"`) controls whether the codemod generates `t('key')` or `t['key']` for React codemods
|
|
16
|
+
- **CodemodConfig wired through** — all three `applyReactCodemod()`, `applyVueCodemod()`, `applyAngularCodemod()` functions now accept and apply `CodemodConfig`; the `codemods` block in `ai-localize.config.json` is no longer silently ignored
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- ai-localize-shared@2.0.2
|
|
22
|
+
|
|
3
23
|
## 2.0.1
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-codemods",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "AST-based codemods to inject i18n wrappers into frontend source code",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
8
13
|
"exports": {
|
|
9
14
|
".": {
|
|
10
15
|
"types": "./dist/index.d.ts",
|
|
@@ -12,6 +17,23 @@
|
|
|
12
17
|
"require": "./dist/index.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"i18n",
|
|
22
|
+
"localization",
|
|
23
|
+
"l10n",
|
|
24
|
+
"internationalization",
|
|
25
|
+
"ai-localize",
|
|
26
|
+
"codemod",
|
|
27
|
+
"ast",
|
|
28
|
+
"babel",
|
|
29
|
+
"jscodeshift",
|
|
30
|
+
"react",
|
|
31
|
+
"vue",
|
|
32
|
+
"angular"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
15
37
|
"dependencies": {
|
|
16
38
|
"@babel/parser": "^7.23.9",
|
|
17
39
|
"@babel/traverse": "^7.23.9",
|
|
@@ -19,7 +41,7 @@
|
|
|
19
41
|
"@babel/generator": "^7.23.9",
|
|
20
42
|
"recast": "^0.23.4",
|
|
21
43
|
"jscodeshift": "^0.15.2",
|
|
22
|
-
"ai-localize-shared": "2.0.
|
|
44
|
+
"ai-localize-shared": "2.0.4"
|
|
23
45
|
},
|
|
24
46
|
"devDependencies": {
|
|
25
47
|
"@types/babel__generator": "^7.6.8",
|
package/src/angular-codemod.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import type { DetectedText, CodemodConfig } from 'ai-localize-shared';
|
|
4
|
-
import type { CodemodResult } from './react-codemod.js';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_TS_SERVICE = 'this.translateService.instant';
|
|
7
|
-
const DEFAULT_TEMPLATE_PIPE = 'translate';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Angular ngx-translate codemod:
|
|
11
|
-
* - In templates: wraps text with {{ 'key' | translate }}
|
|
12
|
-
* - In TS: replaces string literals with this.translateService.instant('key')
|
|
13
|
-
*
|
|
14
|
-
* The pipe name used in templates can be overridden via
|
|
15
|
-
* `codemods.translationFunction` (default: "translate").
|
|
16
|
-
* The TS service call can be overridden via `codemods.hookName`
|
|
17
|
-
* (default: "this.translateService.instant").
|
|
18
|
-
*/
|
|
19
|
-
export class AngularCodemod {
|
|
20
|
-
private filePath: string;
|
|
21
|
-
private content: string;
|
|
22
|
-
private texts: DetectedText[];
|
|
23
|
-
private templatePipe: string;
|
|
24
|
-
private tsServiceCall: string;
|
|
25
|
-
|
|
26
|
-
constructor(filePath: string, content: string, texts: DetectedText[], codemodConfig?: CodemodConfig) {
|
|
27
|
-
this.filePath = filePath;
|
|
28
|
-
this.content = content;
|
|
29
|
-
this.texts = texts;
|
|
30
|
-
// translationFunction maps to the Angular template pipe name
|
|
31
|
-
this.templatePipe = codemodConfig?.translationFunction ?? DEFAULT_TEMPLATE_PIPE;
|
|
32
|
-
// hookName maps to the TS service call expression
|
|
33
|
-
this.tsServiceCall = codemodConfig?.hookName ?? DEFAULT_TS_SERVICE;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
transform(): CodemodResult {
|
|
37
|
-
if (this.texts.length === 0) return this.unchanged();
|
|
38
|
-
|
|
39
|
-
const textKeyMap = new Map<string, string>();
|
|
40
|
-
for (const dt of this.texts) {
|
|
41
|
-
textKeyMap.set(dt.text, dt.suggestedKey);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let transformedContent = this.content;
|
|
45
|
-
let replacedTexts = 0;
|
|
46
|
-
const isTemplate = this.filePath.endsWith('.html');
|
|
47
|
-
const pipe = this.templatePipe;
|
|
48
|
-
const svc = this.tsServiceCall;
|
|
49
|
-
|
|
50
|
-
for (const [text, key] of textKeyMap) {
|
|
51
|
-
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
-
if (isTemplate) {
|
|
53
|
-
// Template: >Some Text< => >{{ 'key' | translate }}<
|
|
54
|
-
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, 'g');
|
|
55
|
-
transformedContent = transformedContent.replace(re, `$1{{ '${key}' | ${pipe} }}$2`);
|
|
56
|
-
// attribute: [attr]="'Some Text'" => [attr]="'key' | translate"
|
|
57
|
-
const attrRe = new RegExp(`\\[(?:placeholder|label|title|alt)\\]="'${escaped}'"`, 'g');
|
|
58
|
-
transformedContent = transformedContent.replace(
|
|
59
|
-
attrRe,
|
|
60
|
-
`[placeholder]="'${key}' | ${pipe}"`
|
|
61
|
-
);
|
|
62
|
-
} else {
|
|
63
|
-
// TS file: 'Some Text' => this.translateService.instant('key')
|
|
64
|
-
const tsRe = new RegExp(`'${escaped}'`, 'g');
|
|
65
|
-
transformedContent = transformedContent.replace(
|
|
66
|
-
tsRe,
|
|
67
|
-
`${svc}('${key}')`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
replacedTexts++;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (replacedTexts === 0) return this.unchanged();
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
filePath: this.filePath,
|
|
77
|
-
originalContent: this.content,
|
|
78
|
-
transformedContent,
|
|
79
|
-
changed: true,
|
|
80
|
-
injectedImport: false,
|
|
81
|
-
injectedHook: false,
|
|
82
|
-
replacedTexts,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private unchanged(): CodemodResult {
|
|
87
|
-
return {
|
|
88
|
-
filePath: this.filePath,
|
|
89
|
-
originalContent: this.content,
|
|
90
|
-
transformedContent: this.content,
|
|
91
|
-
changed: false,
|
|
92
|
-
injectedImport: false,
|
|
93
|
-
injectedHook: false,
|
|
94
|
-
replacedTexts: 0,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function applyAngularCodemod(
|
|
100
|
-
filePath: string,
|
|
101
|
-
texts: DetectedText[],
|
|
102
|
-
dryRun = false,
|
|
103
|
-
codemodConfig?: CodemodConfig
|
|
104
|
-
): CodemodResult {
|
|
105
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
106
|
-
const codemod = new AngularCodemod(filePath, content, texts, codemodConfig);
|
|
107
|
-
const result = codemod.transform();
|
|
108
|
-
if (result.changed && !dryRun) {
|
|
109
|
-
fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
package/src/cdn-replacer.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
import type { LegacyCdnUrl, CloudFrontAsset } from 'ai-localize-shared';
|
|
5
|
-
|
|
6
|
-
export interface CdnReplacementResult {
|
|
7
|
-
filePath: string;
|
|
8
|
-
replacedCount: number;
|
|
9
|
-
changed: boolean;
|
|
10
|
-
originalContent: string;
|
|
11
|
-
transformedContent: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Replaces legacy CDN URLs with CloudFront URLs in source files.
|
|
16
|
-
* Supports: TS/JS/JSX/TSX/CSS/HTML/Vue
|
|
17
|
-
*/
|
|
18
|
-
export class CdnReplacer {
|
|
19
|
-
private assetMap: Map<string, CloudFrontAsset>;
|
|
20
|
-
|
|
21
|
-
constructor(assets: CloudFrontAsset[]) {
|
|
22
|
-
this.assetMap = new Map();
|
|
23
|
-
for (const asset of assets) {
|
|
24
|
-
this.assetMap.set(asset.localPath, asset);
|
|
25
|
-
// Also index by asset path without leading slash
|
|
26
|
-
this.assetMap.set(asset.s3Key, asset);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
replaceInFile(
|
|
31
|
-
filePath: string,
|
|
32
|
-
legacyUrls: LegacyCdnUrl[],
|
|
33
|
-
dryRun = false
|
|
34
|
-
): CdnReplacementResult {
|
|
35
|
-
let content: string;
|
|
36
|
-
try {
|
|
37
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
-
} catch {
|
|
39
|
-
return { filePath, replacedCount: 0, changed: false, originalContent: '', transformedContent: '' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let transformedContent = content;
|
|
43
|
-
let replacedCount = 0;
|
|
44
|
-
|
|
45
|
-
for (const legacyUrl of legacyUrls) {
|
|
46
|
-
// Find the corresponding CloudFront asset
|
|
47
|
-
const asset = this.findAsset(legacyUrl.assetPath);
|
|
48
|
-
if (!asset) continue;
|
|
49
|
-
|
|
50
|
-
const escapedUrl = legacyUrl.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
-
const re = new RegExp(escapedUrl, 'g');
|
|
52
|
-
const newCount = (transformedContent.match(re) || []).length;
|
|
53
|
-
transformedContent = transformedContent.replace(re, asset.cloudfrontUrl);
|
|
54
|
-
replacedCount += newCount;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (replacedCount > 0 && !dryRun) {
|
|
58
|
-
fs.writeFileSync(filePath, transformedContent, 'utf-8');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
filePath,
|
|
63
|
-
replacedCount,
|
|
64
|
-
changed: replacedCount > 0,
|
|
65
|
-
originalContent: content,
|
|
66
|
-
transformedContent,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private findAsset(assetPath: string): CloudFrontAsset | undefined {
|
|
71
|
-
// Normalize path
|
|
72
|
-
const normalized = assetPath.replace(/^\//, '');
|
|
73
|
-
return (
|
|
74
|
-
this.assetMap.get(assetPath) ||
|
|
75
|
-
this.assetMap.get(normalized) ||
|
|
76
|
-
this.assetMap.get('/' + normalized) ||
|
|
77
|
-
// fuzzy match by filename
|
|
78
|
-
Array.from(this.assetMap.values()).find((a) =>
|
|
79
|
-
path.basename(a.localPath) === path.basename(assetPath)
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Batch replace CDN URLs across multiple files.
|
|
87
|
-
*/
|
|
88
|
-
export async function batchReplaceCdnUrls(
|
|
89
|
-
fileUrlMap: Map<string, LegacyCdnUrl[]>,
|
|
90
|
-
assets: CloudFrontAsset[],
|
|
91
|
-
dryRun = false
|
|
92
|
-
): Promise<CdnReplacementResult[]> {
|
|
93
|
-
const replacer = new CdnReplacer(assets);
|
|
94
|
-
const results: CdnReplacementResult[] = [];
|
|
95
|
-
|
|
96
|
-
for (const [filePath, legacyUrls] of fileUrlMap) {
|
|
97
|
-
const result = replacer.replaceInFile(filePath, legacyUrls, dryRun);
|
|
98
|
-
results.push(result);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return results;
|
|
102
|
-
}
|
package/src/codemod-runner.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import type { Framework, DetectedText, LocalizationConfig } from 'ai-localize-shared';
|
|
3
|
-
import type { CodemodResult } from './react-codemod.js';
|
|
4
|
-
import { applyReactCodemod } from './react-codemod.js';
|
|
5
|
-
import { applyVueCodemod } from './vue-codemod.js';
|
|
6
|
-
import { applyAngularCodemod } from './angular-codemod.js';
|
|
7
|
-
|
|
8
|
-
export interface CodemodRunOptions {
|
|
9
|
-
dryRun?: boolean;
|
|
10
|
-
onProgress?: (filePath: string, result: CodemodResult) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface CodemodRunSummary {
|
|
14
|
-
totalFiles: number;
|
|
15
|
-
changedFiles: number;
|
|
16
|
-
totalReplacements: number;
|
|
17
|
-
importInjections: number;
|
|
18
|
-
hookInjections: number;
|
|
19
|
-
results: CodemodResult[];
|
|
20
|
-
duration: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Orchestrates codemods across all detected files.
|
|
25
|
-
*
|
|
26
|
-
* Groups detected texts by file and applies the appropriate codemod based on
|
|
27
|
-
* the project framework. Codemod behaviour (import package, hook name,
|
|
28
|
-
* translation function, namespace, accessor style) is driven by `config.codemods`
|
|
29
|
-
* so the user can override defaults in their ai-localize.config.json.
|
|
30
|
-
*
|
|
31
|
-
* ### Local hook import path resolution
|
|
32
|
-
* When `config.codemods.importPackage` is a project-relative path (e.g.
|
|
33
|
-
* `"src/Locales/translate"`), `cwd` is forwarded to `applyReactCodemod` so it
|
|
34
|
-
* can compute the correct relative import path for each individual file:
|
|
35
|
-
* src/components/Button.tsx → `"../../Locales/translate"`
|
|
36
|
-
* src/pages/dashboard/Header.tsx → `"../../../Locales/translate"`
|
|
37
|
-
*/
|
|
38
|
-
export class CodemodRunner {
|
|
39
|
-
private config: LocalizationConfig;
|
|
40
|
-
private cwd: string;
|
|
41
|
-
|
|
42
|
-
constructor(config: LocalizationConfig, cwd = process.cwd()) {
|
|
43
|
-
this.config = config;
|
|
44
|
-
this.cwd = cwd;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async run(
|
|
48
|
-
detectedTexts: DetectedText[],
|
|
49
|
-
options: CodemodRunOptions = {}
|
|
50
|
-
): Promise<CodemodRunSummary> {
|
|
51
|
-
const startTime = Date.now();
|
|
52
|
-
const { dryRun = false, onProgress } = options;
|
|
53
|
-
|
|
54
|
-
// Group texts by file
|
|
55
|
-
const fileTextMap = new Map<string, DetectedText[]>();
|
|
56
|
-
for (const dt of detectedTexts) {
|
|
57
|
-
const existing = fileTextMap.get(dt.filePath) || [];
|
|
58
|
-
existing.push(dt);
|
|
59
|
-
fileTextMap.set(dt.filePath, existing);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const results: CodemodResult[] = [];
|
|
63
|
-
let totalReplacements = 0;
|
|
64
|
-
let importInjections = 0;
|
|
65
|
-
let hookInjections = 0;
|
|
66
|
-
|
|
67
|
-
for (const [filePath, texts] of fileTextMap) {
|
|
68
|
-
let result: CodemodResult;
|
|
69
|
-
const framework = this.config.framework;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
result = this.applyCodemod(filePath, texts, framework, dryRun);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
result = {
|
|
75
|
-
filePath,
|
|
76
|
-
originalContent: '',
|
|
77
|
-
transformedContent: '',
|
|
78
|
-
changed: false,
|
|
79
|
-
injectedImport: false,
|
|
80
|
-
injectedHook: false,
|
|
81
|
-
replacedTexts: 0,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
results.push(result);
|
|
86
|
-
totalReplacements += result.replacedTexts;
|
|
87
|
-
if (result.injectedImport) importInjections++;
|
|
88
|
-
if (result.injectedHook) hookInjections++;
|
|
89
|
-
|
|
90
|
-
onProgress?.(filePath, result);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
totalFiles: fileTextMap.size,
|
|
95
|
-
changedFiles: results.filter((r) => r.changed).length,
|
|
96
|
-
totalReplacements,
|
|
97
|
-
importInjections,
|
|
98
|
-
hookInjections,
|
|
99
|
-
results,
|
|
100
|
-
duration: Date.now() - startTime,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private applyCodemod(
|
|
105
|
-
filePath: string,
|
|
106
|
-
texts: DetectedText[],
|
|
107
|
-
framework: Framework,
|
|
108
|
-
dryRun: boolean
|
|
109
|
-
): CodemodResult {
|
|
110
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
111
|
-
const codemodConfig = this.config.codemods;
|
|
112
|
-
|
|
113
|
-
// Vue SFCs
|
|
114
|
-
if (ext === '.vue') {
|
|
115
|
-
return applyVueCodemod(filePath, texts, dryRun, codemodConfig);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Angular templates or TS
|
|
119
|
-
if (
|
|
120
|
-
framework === 'angular' ||
|
|
121
|
-
framework === 'angular-ngx' ||
|
|
122
|
-
framework === 'angular-i18n'
|
|
123
|
-
) {
|
|
124
|
-
return applyAngularCodemod(filePath, texts, dryRun, codemodConfig);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// React (default for .tsx, .ts, .jsx, .js)
|
|
128
|
-
// Pass cwd so the codemod can resolve per-file relative import paths
|
|
129
|
-
// for local hook files (e.g. "src/Locales/translate").
|
|
130
|
-
return applyReactCodemod(filePath, texts, dryRun, codemodConfig, this.cwd);
|
|
131
|
-
}
|
|
132
|
-
}
|
package/src/index.ts
DELETED
package/src/react-codemod.ts
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as nodePath from 'path';
|
|
3
|
-
import * as parser from '@babel/parser';
|
|
4
|
-
import traverse from '@babel/traverse';
|
|
5
|
-
import generate from '@babel/generator';
|
|
6
|
-
import * as t from '@babel/types';
|
|
7
|
-
|
|
8
|
-
import type { DetectedText, CodemodConfig } from 'ai-localize-shared';
|
|
9
|
-
|
|
10
|
-
export interface CodemodResult {
|
|
11
|
-
filePath: string;
|
|
12
|
-
originalContent: string;
|
|
13
|
-
transformedContent: string;
|
|
14
|
-
changed: boolean;
|
|
15
|
-
injectedImport: boolean;
|
|
16
|
-
injectedHook: boolean;
|
|
17
|
-
replacedTexts: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const DEFAULT_IMPORT_PACKAGE = 'react-i18next';
|
|
21
|
-
const DEFAULT_HOOK_NAME = 'useTranslation';
|
|
22
|
-
const DEFAULT_TRANSLATION_FN = 't';
|
|
23
|
-
const DEFAULT_ACCESSOR_STYLE = 'function';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* React i18n codemod: wraps hardcoded JSX text / string literals with t() or t[''] calls.
|
|
27
|
-
* Preserves formatting, comments, and import order.
|
|
28
|
-
*
|
|
29
|
-
* ### Local hook import paths
|
|
30
|
-
*
|
|
31
|
-
* When `codemodConfig.importPackage` is a project-relative path (e.g. `"src/Locales/translate"`),
|
|
32
|
-
* the codemod computes the correct **relative import path for each file** it transforms:
|
|
33
|
-
*
|
|
34
|
-
* src/components/Button.tsx → `"../../Locales/translate"`
|
|
35
|
-
* src/pages/dashboard/Header.tsx → `"../../../Locales/translate"`
|
|
36
|
-
*
|
|
37
|
-
* npm package names (no `/` prefix, not starting with `.`) are injected verbatim.
|
|
38
|
-
*
|
|
39
|
-
* ### Accessor styles
|
|
40
|
-
* accessorStyle "function" (default) → t('key')
|
|
41
|
-
* accessorStyle "bracket" → t['key']
|
|
42
|
-
*/
|
|
43
|
-
export class ReactCodemod {
|
|
44
|
-
private filePath: string;
|
|
45
|
-
private content: string;
|
|
46
|
-
private texts: DetectedText[];
|
|
47
|
-
/** Resolved import specifier string (may differ per file for local paths) */
|
|
48
|
-
private importSpecifier: string;
|
|
49
|
-
private hookName: string;
|
|
50
|
-
private translationFn: string;
|
|
51
|
-
private namespace?: string;
|
|
52
|
-
private accessorStyle: 'function' | 'bracket';
|
|
53
|
-
|
|
54
|
-
constructor(
|
|
55
|
-
filePath: string,
|
|
56
|
-
content: string,
|
|
57
|
-
texts: DetectedText[],
|
|
58
|
-
codemodConfig?: CodemodConfig,
|
|
59
|
-
cwd = process.cwd()
|
|
60
|
-
) {
|
|
61
|
-
this.filePath = filePath;
|
|
62
|
-
this.content = content;
|
|
63
|
-
this.texts = texts;
|
|
64
|
-
this.hookName = codemodConfig?.hookName ?? DEFAULT_HOOK_NAME;
|
|
65
|
-
this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN;
|
|
66
|
-
this.namespace = codemodConfig?.namespace;
|
|
67
|
-
this.accessorStyle = codemodConfig?.accessorStyle ?? DEFAULT_ACCESSOR_STYLE;
|
|
68
|
-
|
|
69
|
-
// Resolve import specifier: npm package → verbatim, local path → per-file relative
|
|
70
|
-
const pkg = codemodConfig?.importPackage ?? DEFAULT_IMPORT_PACKAGE;
|
|
71
|
-
this.importSpecifier = resolveImportSpecifier(pkg, filePath, cwd);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
transform(): CodemodResult {
|
|
75
|
-
if (this.texts.length === 0) {
|
|
76
|
-
return {
|
|
77
|
-
filePath: this.filePath,
|
|
78
|
-
originalContent: this.content,
|
|
79
|
-
transformedContent: this.content,
|
|
80
|
-
changed: false,
|
|
81
|
-
injectedImport: false,
|
|
82
|
-
injectedHook: false,
|
|
83
|
-
replacedTexts: 0,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let ast: t.File;
|
|
88
|
-
try {
|
|
89
|
-
ast = parser.parse(this.content, {
|
|
90
|
-
sourceType: 'module',
|
|
91
|
-
plugins: [
|
|
92
|
-
'jsx',
|
|
93
|
-
'typescript',
|
|
94
|
-
'decorators-legacy',
|
|
95
|
-
'classProperties',
|
|
96
|
-
'optionalChaining',
|
|
97
|
-
'nullishCoalescingOperator',
|
|
98
|
-
],
|
|
99
|
-
errorRecovery: true,
|
|
100
|
-
});
|
|
101
|
-
} catch {
|
|
102
|
-
return {
|
|
103
|
-
filePath: this.filePath,
|
|
104
|
-
originalContent: this.content,
|
|
105
|
-
transformedContent: this.content,
|
|
106
|
-
changed: false,
|
|
107
|
-
injectedImport: false,
|
|
108
|
-
injectedHook: false,
|
|
109
|
-
replacedTexts: 0,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Build a map of text -> key for fast lookup
|
|
114
|
-
const textKeyMap = new Map<string, string>();
|
|
115
|
-
for (const dt of this.texts) {
|
|
116
|
-
textKeyMap.set(dt.text, dt.suggestedKey);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let replacedTexts = 0;
|
|
120
|
-
let hasImport = false;
|
|
121
|
-
let hasHook = false;
|
|
122
|
-
let injectedImport = false;
|
|
123
|
-
let injectedHook = false;
|
|
124
|
-
|
|
125
|
-
const translationFn = this.translationFn;
|
|
126
|
-
const hookName = this.hookName;
|
|
127
|
-
const importSpecifier = this.importSpecifier;
|
|
128
|
-
const accessorStyle = this.accessorStyle;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Builds the translation expression for a key.
|
|
132
|
-
* "function" style: t('some.key')
|
|
133
|
-
* "bracket" style: t['some.key']
|
|
134
|
-
*/
|
|
135
|
-
const buildTranslationExpr = (key: string): t.Expression => {
|
|
136
|
-
if (accessorStyle === 'bracket') {
|
|
137
|
-
return t.memberExpression(
|
|
138
|
-
t.identifier(translationFn),
|
|
139
|
-
t.stringLiteral(key),
|
|
140
|
-
true /* computed */
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return t.callExpression(t.identifier(translationFn), [t.stringLiteral(key)]);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// First pass: detect existing imports and hook usage
|
|
147
|
-
traverse(ast, {
|
|
148
|
-
ImportDeclaration(nodePath) {
|
|
149
|
-
if (nodePath.node.source.value === importSpecifier) {
|
|
150
|
-
hasImport = true;
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
VariableDeclarator(nodePath) {
|
|
154
|
-
const id = nodePath.node.id;
|
|
155
|
-
if (
|
|
156
|
-
t.isObjectPattern(id) &&
|
|
157
|
-
id.properties.some(
|
|
158
|
-
(p) =>
|
|
159
|
-
t.isObjectProperty(p) &&
|
|
160
|
-
t.isIdentifier(p.key) &&
|
|
161
|
-
p.key.name === translationFn
|
|
162
|
-
)
|
|
163
|
-
) {
|
|
164
|
-
hasHook = true;
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Second pass: replace hardcoded texts
|
|
170
|
-
traverse(ast, {
|
|
171
|
-
JSXText(nodePath) {
|
|
172
|
-
const text = nodePath.node.value.trim();
|
|
173
|
-
if (!text) return;
|
|
174
|
-
const key = textKeyMap.get(text);
|
|
175
|
-
if (!key) return;
|
|
176
|
-
nodePath.replaceWith(t.jsxExpressionContainer(buildTranslationExpr(key)));
|
|
177
|
-
replacedTexts++;
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
StringLiteral(nodePath) {
|
|
181
|
-
const text = nodePath.node.value;
|
|
182
|
-
const key = textKeyMap.get(text);
|
|
183
|
-
if (!key) return;
|
|
184
|
-
if (t.isImportDeclaration(nodePath.parent)) return;
|
|
185
|
-
if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
|
|
186
|
-
if (t.isJSXAttribute(nodePath.parent)) return;
|
|
187
|
-
nodePath.replaceWith(buildTranslationExpr(key));
|
|
188
|
-
replacedTexts++;
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
JSXAttribute(nodePath) {
|
|
192
|
-
const valueNode = nodePath.node.value;
|
|
193
|
-
if (!t.isStringLiteral(valueNode)) return;
|
|
194
|
-
const text = valueNode.value;
|
|
195
|
-
const key = textKeyMap.get(text);
|
|
196
|
-
if (!key) return;
|
|
197
|
-
nodePath.node.value = t.jsxExpressionContainer(buildTranslationExpr(key));
|
|
198
|
-
replacedTexts++;
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (replacedTexts === 0) {
|
|
203
|
-
return {
|
|
204
|
-
filePath: this.filePath,
|
|
205
|
-
originalContent: this.content,
|
|
206
|
-
transformedContent: this.content,
|
|
207
|
-
changed: false,
|
|
208
|
-
injectedImport: false,
|
|
209
|
-
injectedHook: false,
|
|
210
|
-
replacedTexts: 0,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Inject import if needed (uses the per-file resolved import specifier)
|
|
215
|
-
if (!hasImport) {
|
|
216
|
-
const importDecl = t.importDeclaration(
|
|
217
|
-
[t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
|
|
218
|
-
t.stringLiteral(importSpecifier)
|
|
219
|
-
);
|
|
220
|
-
ast.program.body.unshift(importDecl);
|
|
221
|
-
injectedImport = true;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Inject hook at the top of function components
|
|
225
|
-
if (!hasHook) {
|
|
226
|
-
this.injectHook(ast);
|
|
227
|
-
injectedHook = true;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Generate output preserving formatting
|
|
231
|
-
const output = generate(
|
|
232
|
-
ast,
|
|
233
|
-
{ retainLines: false, comments: true, compact: false },
|
|
234
|
-
this.content
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
filePath: this.filePath,
|
|
239
|
-
originalContent: this.content,
|
|
240
|
-
transformedContent: output.code,
|
|
241
|
-
changed: true,
|
|
242
|
-
injectedImport,
|
|
243
|
-
injectedHook,
|
|
244
|
-
replacedTexts,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private injectHook(ast: t.File): void {
|
|
249
|
-
const hookName = this.hookName;
|
|
250
|
-
const translationFn = this.translationFn;
|
|
251
|
-
const namespace = this.namespace;
|
|
252
|
-
|
|
253
|
-
traverse(ast, {
|
|
254
|
-
FunctionDeclaration(nodePath) {
|
|
255
|
-
if (!isReactComponent(nodePath.node.id?.name)) return;
|
|
256
|
-
injectHookIntoBody(nodePath.node.body, hookName, translationFn, namespace);
|
|
257
|
-
nodePath.stop();
|
|
258
|
-
},
|
|
259
|
-
ArrowFunctionExpression(nodePath) {
|
|
260
|
-
if (!isReactComponentParent(nodePath)) return;
|
|
261
|
-
const body = nodePath.node.body;
|
|
262
|
-
if (t.isBlockStatement(body)) {
|
|
263
|
-
injectHookIntoBody(body, hookName, translationFn, namespace);
|
|
264
|
-
nodePath.stop();
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function isReactComponent(name?: string): boolean {
|
|
272
|
-
if (!name) return false;
|
|
273
|
-
return /^[A-Z]/.test(name);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
|
-
function isReactComponentParent(nodePath: any): boolean {
|
|
278
|
-
const parent = nodePath.parent;
|
|
279
|
-
if (t.isVariableDeclarator(parent)) {
|
|
280
|
-
const id = parent.id;
|
|
281
|
-
if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
|
|
282
|
-
}
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function injectHookIntoBody(
|
|
287
|
-
body: t.BlockStatement,
|
|
288
|
-
hookName: string,
|
|
289
|
-
translationFn: string,
|
|
290
|
-
namespace?: string
|
|
291
|
-
): void {
|
|
292
|
-
const hookArgs: t.Expression[] = namespace ? [t.stringLiteral(namespace)] : [];
|
|
293
|
-
const hookDecl = t.variableDeclaration('const', [
|
|
294
|
-
t.variableDeclarator(
|
|
295
|
-
t.objectPattern([
|
|
296
|
-
t.objectProperty(t.identifier(translationFn), t.identifier(translationFn), false, true),
|
|
297
|
-
]),
|
|
298
|
-
t.callExpression(t.identifier(hookName), hookArgs)
|
|
299
|
-
),
|
|
300
|
-
]);
|
|
301
|
-
body.body.unshift(hookDecl);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Determines the import specifier string to inject for a given `importPackage`
|
|
306
|
-
* config value and the file being transformed.
|
|
307
|
-
*
|
|
308
|
-
* Rules:
|
|
309
|
-
* 1. If `importPackage` looks like an npm package name (no leading `.` or `/`
|
|
310
|
-
* and no path separators in a way that suggests a local path starting with
|
|
311
|
-
* a known directory like `src/`) → return verbatim.
|
|
312
|
-
*
|
|
313
|
-
* 2. If `importPackage` is an absolute path → compute `path.relative(fileDir, importPackage)`
|
|
314
|
-
* and ensure it starts with `./` or `../`.
|
|
315
|
-
*
|
|
316
|
-
* 3. If `importPackage` is a project-relative path (starts with `src/`, `app/`,
|
|
317
|
-
* `lib/`, `hooks/`, `utils/`, or any path without a leading `.`) that is NOT
|
|
318
|
-
* a bare npm package name → resolve it relative to `cwd` first, then compute
|
|
319
|
-
* `path.relative(fileDir, resolvedPath)`.
|
|
320
|
-
*
|
|
321
|
-
* 4. If `importPackage` starts with `./` or `../` → treat as a path relative to
|
|
322
|
-
* `cwd` (config file location), resolve it, then compute relative to each file.
|
|
323
|
-
*/
|
|
324
|
-
export function resolveImportSpecifier(
|
|
325
|
-
importPackage: string,
|
|
326
|
-
filePath: string,
|
|
327
|
-
cwd: string
|
|
328
|
-
): string {
|
|
329
|
-
// Bare npm package name — no slashes that indicate a local path, or scoped @org/pkg
|
|
330
|
-
if (isNpmPackage(importPackage)) {
|
|
331
|
-
return importPackage;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Resolve the hook file to an absolute path
|
|
335
|
-
let absoluteHookPath: string;
|
|
336
|
-
if (nodePath.isAbsolute(importPackage)) {
|
|
337
|
-
absoluteHookPath = importPackage;
|
|
338
|
-
} else {
|
|
339
|
-
// Project-relative (e.g. "src/Locales/translate") or relative (e.g. "../../hooks/t")
|
|
340
|
-
absoluteHookPath = nodePath.resolve(cwd, importPackage);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Compute relative path from the directory of the file being transformed
|
|
344
|
-
const fileDir = nodePath.dirname(filePath);
|
|
345
|
-
let rel = nodePath.relative(fileDir, absoluteHookPath).replace(/\\/g, '/');
|
|
346
|
-
|
|
347
|
-
// Ensure it starts with ./ or ../
|
|
348
|
-
if (!rel.startsWith('.')) {
|
|
349
|
-
rel = `./${rel}`;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return rel;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Returns true if the string looks like a bare npm package name rather than a
|
|
357
|
-
* local file path.
|
|
358
|
-
*
|
|
359
|
-
* npm package names:
|
|
360
|
-
* - Do not start with `.` or `/`
|
|
361
|
-
* - Scoped packages start with `@` followed by `org/pkg`
|
|
362
|
-
* - Do NOT look like project-relative source paths such as `src/...`, `app/...`
|
|
363
|
-
*
|
|
364
|
-
* We consider a string a local path if it:
|
|
365
|
-
* - Starts with `.` (relative: `./hooks/t`, `../../hooks/t`)
|
|
366
|
-
*- Starts with `/` (absolute)
|
|
367
|
-
* - Contains a `/` AND its first segment looks like a source directory
|
|
368
|
-
* (`src`, `app`, `lib`, `hooks`, `utils`, `pages`, `components`, `features`,
|
|
369
|
-
* `shared`, `common`, `locales`, `i18n`, `services`, `store`)
|
|
370
|
-
*/
|
|
371
|
-
function isNpmPackage(pkg: string): boolean {
|
|
372
|
-
if (pkg.startsWith('.') || pkg.startsWith('/')) return false;
|
|
373
|
-
|
|
374
|
-
// Scoped npm package: @scope/name — always npm
|
|
375
|
-
if (pkg.startsWith('@') && pkg.includes('/')) {
|
|
376
|
-
// @/alias paths used in bundlers (e.g. "@/hooks/useT") are NOT npm packages
|
|
377
|
-
if (pkg.startsWith('@/')) return false;
|
|
378
|
-
return true;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// If no slash at all → bare npm package name (e.g. "i18next", "react-i18next")
|
|
382
|
-
if (!pkg.includes('/')) return true;
|
|
383
|
-
|
|
384
|
-
// Has a slash — check if the first segment is a known source directory name
|
|
385
|
-
const firstSegment = pkg.split('/')[0].toLowerCase();
|
|
386
|
-
const sourceDirectories = new Set([
|
|
387
|
-
'src', 'app', 'lib', 'hooks', 'utils', 'pages', 'components',
|
|
388
|
-
'features', 'shared', 'common', 'locales', 'i18n', 'services',
|
|
389
|
-
'store', 'helpers', 'core', 'modules',
|
|
390
|
-
]);
|
|
391
|
-
if (sourceDirectories.has(firstSegment)) return false;
|
|
392
|
-
|
|
393
|
-
// Everything else (e.g. "lodash/fp", "date-fns/locale") → npm package
|
|
394
|
-
return true;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Applies the React codemod to a file in-place.
|
|
399
|
-
*/
|
|
400
|
-
export function applyReactCodemod(
|
|
401
|
-
filePath: string,
|
|
402
|
-
texts: DetectedText[],
|
|
403
|
-
dryRun = false,
|
|
404
|
-
codemodConfig?: CodemodConfig,
|
|
405
|
-
cwd = process.cwd()
|
|
406
|
-
): CodemodResult {
|
|
407
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
408
|
-
const codemod = new ReactCodemod(filePath, content, texts, codemodConfig, cwd);
|
|
409
|
-
const result = codemod.transform();
|
|
410
|
-
|
|
411
|
-
if (result.changed && !dryRun) {
|
|
412
|
-
fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return result;
|
|
416
|
-
}
|
package/src/vue-codemod.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import type { DetectedText, CodemodConfig } from 'ai-localize-shared';
|
|
4
|
-
import type { CodemodResult } from './react-codemod.js';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_TRANSLATION_FN = '$t';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Vue i18n codemod: wraps hardcoded text with $t() / t() calls in Vue SFCs.
|
|
10
|
-
* Handles both Options API (global $t) and Composition API (useI18n / useTranslation).
|
|
11
|
-
*
|
|
12
|
-
* The translation function name can be overridden via the `codemods.translationFunction`
|
|
13
|
-
* config field (default: "$t").
|
|
14
|
-
*/
|
|
15
|
-
export class VueCodemod {
|
|
16
|
-
private filePath: string;
|
|
17
|
-
private content: string;
|
|
18
|
-
private texts: DetectedText[];
|
|
19
|
-
private translationFn: string;
|
|
20
|
-
|
|
21
|
-
constructor(filePath: string, content: string, texts: DetectedText[], codemodConfig?: CodemodConfig) {
|
|
22
|
-
this.filePath = filePath;
|
|
23
|
-
this.content = content;
|
|
24
|
-
this.texts = texts;
|
|
25
|
-
this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
transform(): CodemodResult {
|
|
29
|
-
if (this.texts.length === 0) {
|
|
30
|
-
return this.unchanged();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Parse <template> and <script> sections separately
|
|
34
|
-
const templateMatch = this.content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
|
|
35
|
-
const scriptMatch = this.content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
36
|
-
|
|
37
|
-
if (!templateMatch && !scriptMatch) {
|
|
38
|
-
return this.unchanged();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const textKeyMap = new Map<string, string>();
|
|
42
|
-
for (const dt of this.texts) {
|
|
43
|
-
textKeyMap.set(dt.text, dt.suggestedKey);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let transformedContent = this.content;
|
|
47
|
-
let replacedTexts = 0;
|
|
48
|
-
const fn = this.translationFn;
|
|
49
|
-
|
|
50
|
-
// Replace in template: >text< => >{{ $t('key') }}<
|
|
51
|
-
if (templateMatch) {
|
|
52
|
-
let template = templateMatch[1];
|
|
53
|
-
for (const [text, key] of textKeyMap) {
|
|
54
|
-
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
-
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, 'g');
|
|
56
|
-
template = template.replace(re, `$1{{ ${fn}('${key}') }}$2`);
|
|
57
|
-
// Also replace in attribute values: title="text" => :title="$t('key')"
|
|
58
|
-
const attrRe = new RegExp(`((?:title|placeholder|alt|label)=)"(${escaped})"`, 'g');
|
|
59
|
-
template = template.replace(attrRe, `:$1"${fn}('${key}')"`);
|
|
60
|
-
replacedTexts++;
|
|
61
|
-
}
|
|
62
|
-
transformedContent = transformedContent.replace(templateMatch[1], template);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (replacedTexts === 0) {
|
|
66
|
-
return this.unchanged();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
filePath: this.filePath,
|
|
71
|
-
originalContent: this.content,
|
|
72
|
-
transformedContent,
|
|
73
|
-
changed: true,
|
|
74
|
-
injectedImport: false, // vue-i18n is globally injected
|
|
75
|
-
injectedHook: false,
|
|
76
|
-
replacedTexts,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private unchanged(): CodemodResult {
|
|
81
|
-
return {
|
|
82
|
-
filePath: this.filePath,
|
|
83
|
-
originalContent: this.content,
|
|
84
|
-
transformedContent: this.content,
|
|
85
|
-
changed: false,
|
|
86
|
-
injectedImport: false,
|
|
87
|
-
injectedHook: false,
|
|
88
|
-
replacedTexts: 0,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function applyVueCodemod(
|
|
94
|
-
filePath: string,
|
|
95
|
-
texts: DetectedText[],
|
|
96
|
-
dryRun = false,
|
|
97
|
-
codemodConfig?: CodemodConfig
|
|
98
|
-
): CodemodResult {
|
|
99
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
100
|
-
const codemod = new VueCodemod(filePath, content, texts, codemodConfig);
|
|
101
|
-
const result = codemod.transform();
|
|
102
|
-
if (result.changed && !dryRun) {
|
|
103
|
-
fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
106
|
-
}
|