ai-localize-codemods 2.0.0 → 2.0.1

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.
@@ -22,14 +22,26 @@ export interface CodemodRunSummary {
22
22
 
23
23
  /**
24
24
  * Orchestrates codemods across all detected files.
25
- * Groups detected texts by file and applies the appropriate codemod
26
- * based on the project framework.
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"`
27
37
  */
28
38
  export class CodemodRunner {
29
39
  private config: LocalizationConfig;
40
+ private cwd: string;
30
41
 
31
- constructor(config: LocalizationConfig) {
42
+ constructor(config: LocalizationConfig, cwd = process.cwd()) {
32
43
  this.config = config;
44
+ this.cwd = cwd;
33
45
  }
34
46
 
35
47
  async run(
@@ -37,43 +49,43 @@ export class CodemodRunner {
37
49
  options: CodemodRunOptions = {}
38
50
  ): Promise<CodemodRunSummary> {
39
51
  const startTime = Date.now();
40
- const { dryRun = false, onProgress } = options;
52
+ const { dryRun = false, onProgress } = options;
41
53
 
42
54
  // Group texts by file
43
55
  const fileTextMap = new Map<string, DetectedText[]>();
44
56
  for (const dt of detectedTexts) {
45
57
  const existing = fileTextMap.get(dt.filePath) || [];
46
- existing.push(dt);
47
- fileTextMap.set(dt.filePath, existing);
48
- }
58
+ existing.push(dt);
59
+ fileTextMap.set(dt.filePath, existing);
60
+ }
49
61
 
50
62
  const results: CodemodResult[] = [];
51
63
  let totalReplacements = 0;
52
64
  let importInjections = 0;
53
65
  let hookInjections = 0;
54
66
 
55
- for (const [filePath, texts] of fileTextMap) {
67
+ for (const [filePath, texts] of fileTextMap) {
56
68
  let result: CodemodResult;
57
- const framework = this.config.framework;
69
+ const framework = this.config.framework;
58
70
 
59
71
  try {
60
72
  result = this.applyCodemod(filePath, texts, framework, dryRun);
61
73
  } catch (err) {
62
74
  result = {
63
- filePath,
64
- originalContent: '',
65
- transformedContent: '',
75
+ filePath,
76
+ originalContent: '',
77
+ transformedContent: '',
66
78
  changed: false,
67
- injectedImport: false,
79
+ injectedImport: false,
68
80
  injectedHook: false,
69
- replacedTexts: 0,
70
- };
71
- }
81
+ replacedTexts: 0,
82
+ };
83
+ }
72
84
 
73
85
  results.push(result);
74
- totalReplacements += result.replacedTexts;
75
- if (result.injectedImport) importInjections++;
76
- if (result.injectedHook) hookInjections++;
86
+ totalReplacements += result.replacedTexts;
87
+ if (result.injectedImport) importInjections++;
88
+ if (result.injectedHook) hookInjections++;
77
89
 
78
90
  onProgress?.(filePath, result);
79
91
  }
@@ -82,10 +94,10 @@ totalReplacements += result.replacedTexts;
82
94
  totalFiles: fileTextMap.size,
83
95
  changedFiles: results.filter((r) => r.changed).length,
84
96
  totalReplacements,
85
- importInjections,
86
- hookInjections,
87
- results,
88
- duration: Date.now() - startTime,
97
+ importInjections,
98
+ hookInjections,
99
+ results,
100
+ duration: Date.now() - startTime,
89
101
  };
90
102
  }
91
103
 
@@ -96,22 +108,25 @@ totalReplacements += result.replacedTexts;
96
108
  dryRun: boolean
97
109
  ): CodemodResult {
98
110
  const ext = path.extname(filePath).toLowerCase();
111
+ const codemodConfig = this.config.codemods;
99
112
 
100
113
  // Vue SFCs
101
114
  if (ext === '.vue') {
102
- return applyVueCodemod(filePath, texts, dryRun);
115
+ return applyVueCodemod(filePath, texts, dryRun, codemodConfig);
103
116
  }
104
117
 
105
118
  // Angular templates or TS
106
- if (
119
+ if (
107
120
  framework === 'angular' ||
108
121
  framework === 'angular-ngx' ||
109
122
  framework === 'angular-i18n'
110
- ) {
111
- return applyAngularCodemod(filePath, texts, dryRun);
123
+ ) {
124
+ return applyAngularCodemod(filePath, texts, dryRun, codemodConfig);
112
125
  }
113
126
 
114
127
  // React (default for .tsx, .ts, .jsx, .js)
115
- return applyReactCodemod(filePath, texts, dryRun);
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);
116
131
  }
117
132
  }
@@ -1,10 +1,11 @@
1
1
  import * as fs from 'fs';
2
+ import * as nodePath from 'path';
2
3
  import * as parser from '@babel/parser';
3
4
  import traverse from '@babel/traverse';
4
5
  import generate from '@babel/generator';
5
6
  import * as t from '@babel/types';
6
7
 
7
- import type { DetectedText } from 'ai-localize-shared';
8
+ import type { DetectedText, CodemodConfig } from 'ai-localize-shared';
8
9
 
9
10
  export interface CodemodResult {
10
11
  filePath: string;
@@ -16,60 +17,96 @@ export interface CodemodResult {
16
17
  replacedTexts: number;
17
18
  }
18
19
 
19
- const USE_TRANSLATION_IMPORT = 'react-i18next';
20
- const USE_TRANSLATION_HOOK = 'useTranslation';
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';
21
24
 
22
25
  /**
23
- * React i18n codemod: wraps hardcoded JSX text / string literals with t() calls.
26
+ * React i18n codemod: wraps hardcoded JSX text / string literals with t() or t[''] calls.
24
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']
25
42
  */
26
43
  export class ReactCodemod {
27
44
  private filePath: string;
28
45
  private content: string;
29
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';
30
53
 
31
- constructor(filePath: string, content: string, texts: DetectedText[]) {
54
+ constructor(
55
+ filePath: string,
56
+ content: string,
57
+ texts: DetectedText[],
58
+ codemodConfig?: CodemodConfig,
59
+ cwd = process.cwd()
60
+ ) {
32
61
  this.filePath = filePath;
33
62
  this.content = content;
34
- this.texts = texts;
35
- }
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
+ }
36
73
 
37
74
  transform(): CodemodResult {
38
75
  if (this.texts.length === 0) {
39
76
  return {
40
- filePath: this.filePath,
77
+ filePath: this.filePath,
41
78
  originalContent: this.content,
42
79
  transformedContent: this.content,
43
- changed: false,
44
- injectedImport: false,
80
+ changed: false,
81
+ injectedImport: false,
45
82
  injectedHook: false,
46
- replacedTexts: 0,
83
+ replacedTexts: 0,
47
84
  };
48
- }
85
+ }
49
86
 
50
87
  let ast: t.File;
51
88
  try {
52
- ast = parser.parse(this.content, {
89
+ ast = parser.parse(this.content, {
53
90
  sourceType: 'module',
54
91
  plugins: [
55
- 'jsx',
56
- 'typescript',
92
+ 'jsx',
93
+ 'typescript',
57
94
  'decorators-legacy',
58
- 'classProperties',
59
- 'optionalChaining',
95
+ 'classProperties',
96
+ 'optionalChaining',
60
97
  'nullishCoalescingOperator',
61
- ],
98
+ ],
62
99
  errorRecovery: true,
63
100
  });
64
101
  } catch {
65
- return {
66
- filePath: this.filePath,
102
+ return {
103
+ filePath: this.filePath,
67
104
  originalContent: this.content,
68
- transformedContent: this.content,
105
+ transformedContent: this.content,
69
106
  changed: false,
70
107
  injectedImport: false,
71
- injectedHook: false,
72
- replacedTexts: 0,
108
+ injectedHook: false,
109
+ replacedTexts: 0,
73
110
  };
74
111
  }
75
112
 
@@ -80,144 +117,153 @@ originalContent: this.content,
80
117
  }
81
118
 
82
119
  let replacedTexts = 0;
83
- let hasUseTranslationImport = false;
84
- let hasUseTranslationHook = false;
120
+ let hasImport = false;
121
+ let hasHook = false;
85
122
  let injectedImport = false;
86
123
  let injectedHook = false;
87
124
 
88
- // Check existing imports
89
- traverse(ast, {
90
- ImportDeclaration(nodePath) {
91
- if (nodePath.node.source.value === USE_TRANSLATION_IMPORT) {
92
- hasUseTranslationImport = true;
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
+ );
93
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
+ }
94
152
  },
95
- // Check if useTranslation hook already destructured
96
153
  VariableDeclarator(nodePath) {
97
- const id = nodePath.node.id;
98
- if (
99
- t.isObjectPattern(id) &&
100
- id.properties.some(
101
- (p) =>
102
- t.isObjectProperty(p) &&
103
- t.isIdentifier(p.key) &&
104
- p.key.name === 't'
105
- )
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
+ )
106
163
  ) {
107
- hasUseTranslationHook = true;
108
- }
164
+ hasHook = true;
165
+ }
109
166
  },
110
167
  });
111
168
 
112
169
  // Second pass: replace hardcoded texts
113
- traverse(ast, {
170
+ traverse(ast, {
114
171
  JSXText(nodePath) {
115
172
  const text = nodePath.node.value.trim();
116
173
  if (!text) return;
117
174
  const key = textKeyMap.get(text);
118
175
  if (!key) return;
119
-
120
- // Replace with {t('key')}
121
- const tCall = t.jsxExpressionContainer(
122
- t.callExpression(t.identifier('t'), [t.stringLiteral(key)])
123
- );
124
- nodePath.replaceWith(tCall);
176
+ nodePath.replaceWith(t.jsxExpressionContainer(buildTranslationExpr(key)));
125
177
  replacedTexts++;
126
178
  },
127
179
 
128
- StringLiteral(nodePath) {
180
+ StringLiteral(nodePath) {
129
181
  const text = nodePath.node.value;
130
- const key = textKeyMap.get(text);
182
+ const key = textKeyMap.get(text);
131
183
  if (!key) return;
132
184
  if (t.isImportDeclaration(nodePath.parent)) return;
133
- if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
134
- if (t.isJSXAttribute(nodePath.parent)) return;
135
-
136
- const tCall = t.callExpression(t.identifier('t'), [t.stringLiteral(key)]);
137
- nodePath.replaceWith(tCall);
185
+ if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
186
+ if (t.isJSXAttribute(nodePath.parent)) return;
187
+ nodePath.replaceWith(buildTranslationExpr(key));
138
188
  replacedTexts++;
139
189
  },
140
190
 
141
191
  JSXAttribute(nodePath) {
142
192
  const valueNode = nodePath.node.value;
143
- if (!t.isStringLiteral(valueNode)) return;
144
- const text = valueNode.value;
193
+ if (!t.isStringLiteral(valueNode)) return;
194
+ const text = valueNode.value;
145
195
  const key = textKeyMap.get(text);
146
196
  if (!key) return;
147
-
148
- // Replace string with {t('key')} expression
149
- nodePath.node.value = t.jsxExpressionContainer(
150
- t.callExpression(t.identifier('t'), [t.stringLiteral(key)])
151
- );
152
- replacedTexts++;
197
+ nodePath.node.value = t.jsxExpressionContainer(buildTranslationExpr(key));
198
+ replacedTexts++;
153
199
  },
154
200
  });
155
201
 
156
- if (replacedTexts === 0) {
202
+ if (replacedTexts === 0) {
157
203
  return {
158
- filePath: this.filePath,
159
- originalContent: this.content,
160
- transformedContent: this.content,
161
- changed: false,
162
- injectedImport: false,
204
+ filePath: this.filePath,
205
+ originalContent: this.content,
206
+ transformedContent: this.content,
207
+ changed: false,
208
+ injectedImport: false,
163
209
  injectedHook: false,
164
- replacedTexts: 0,
210
+ replacedTexts: 0,
165
211
  };
166
212
  }
167
213
 
168
- // Inject import if needed
169
- if (!hasUseTranslationImport) {
214
+ // Inject import if needed (uses the per-file resolved import specifier)
215
+ if (!hasImport) {
170
216
  const importDecl = t.importDeclaration(
171
- [t.importSpecifier(t.identifier(USE_TRANSLATION_HOOK), t.identifier(USE_TRANSLATION_HOOK))],
172
- t.stringLiteral(USE_TRANSLATION_IMPORT)
217
+ [t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
218
+ t.stringLiteral(importSpecifier)
173
219
  );
174
- ast.program.body.unshift(importDecl);
175
- injectedImport = true;
220
+ ast.program.body.unshift(importDecl);
221
+ injectedImport = true;
176
222
  }
177
223
 
178
224
  // Inject hook at the top of function components
179
- if (!hasUseTranslationHook) {
180
- this.injectUseTranslationHook(ast);
181
- injectedHook = true;
225
+ if (!hasHook) {
226
+ this.injectHook(ast);
227
+ injectedHook = true;
182
228
  }
183
229
 
184
230
  // Generate output preserving formatting
185
- const output = generate(
231
+ const output = generate(
186
232
  ast,
187
- {
188
- retainLines: false,
189
- comments: true,
190
- compact: false,
191
- },
192
- this.content
193
- );
233
+ { retainLines: false, comments: true, compact: false },
234
+ this.content
235
+ );
194
236
 
195
237
  return {
196
238
  filePath: this.filePath,
197
239
  originalContent: this.content,
198
240
  transformedContent: output.code,
199
241
  changed: true,
200
- injectedImport,
242
+ injectedImport,
201
243
  injectedHook,
202
244
  replacedTexts,
203
245
  };
204
246
  }
205
247
 
206
- private injectUseTranslationHook(ast: t.File): void {
248
+ private injectHook(ast: t.File): void {
249
+ const hookName = this.hookName;
250
+ const translationFn = this.translationFn;
251
+ const namespace = this.namespace;
252
+
207
253
  traverse(ast, {
208
254
  FunctionDeclaration(nodePath) {
209
255
  if (!isReactComponent(nodePath.node.id?.name)) return;
210
- injectHookIntoBody(nodePath.node.body);
256
+ injectHookIntoBody(nodePath.node.body, hookName, translationFn, namespace);
211
257
  nodePath.stop();
212
258
  },
213
- ArrowFunctionExpression(nodePath) {
259
+ ArrowFunctionExpression(nodePath) {
214
260
  if (!isReactComponentParent(nodePath)) return;
215
- const body = nodePath.node.body;
261
+ const body = nodePath.node.body;
216
262
  if (t.isBlockStatement(body)) {
217
- injectHookIntoBody(body);
218
- nodePath.stop();
263
+ injectHookIntoBody(body, hookName, translationFn, namespace);
264
+ nodePath.stop();
219
265
  }
220
- },
266
+ },
221
267
  });
222
268
  }
223
269
  }
@@ -232,38 +278,138 @@ function isReactComponentParent(nodePath: any): boolean {
232
278
  const parent = nodePath.parent;
233
279
  if (t.isVariableDeclarator(parent)) {
234
280
  const id = parent.id;
235
- if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
281
+ if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
236
282
  }
237
283
  return false;
238
284
  }
239
285
 
240
- function injectHookIntoBody(body: t.BlockStatement): void {
241
- // const { t } = useTranslation();
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)] : [];
242
293
  const hookDecl = t.variableDeclaration('const', [
243
294
  t.variableDeclarator(
244
295
  t.objectPattern([
245
- t.objectProperty(t.identifier('t'), t.identifier('t'), false, true),
296
+ t.objectProperty(t.identifier(translationFn), t.identifier(translationFn), false, true),
246
297
  ]),
247
- t.callExpression(t.identifier('useTranslation'), [])
298
+ t.callExpression(t.identifier(hookName), hookArgs)
248
299
  ),
249
300
  ]);
250
301
  body.body.unshift(hookDecl);
251
302
  }
252
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
+
253
397
  /**
254
398
  * Applies the React codemod to a file in-place.
255
399
  */
256
400
  export function applyReactCodemod(
257
401
  filePath: string,
258
402
  texts: DetectedText[],
259
- dryRun = false
403
+ dryRun = false,
404
+ codemodConfig?: CodemodConfig,
405
+ cwd = process.cwd()
260
406
  ): CodemodResult {
261
407
  const content = fs.readFileSync(filePath, 'utf-8');
262
- const codemod = new ReactCodemod(filePath, content, texts);
408
+ const codemod = new ReactCodemod(filePath, content, texts, codemodConfig, cwd);
263
409
  const result = codemod.transform();
264
410
 
265
411
  if (result.changed && !dryRun) {
266
- fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
412
+ fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
267
413
  }
268
414
 
269
415
  return result;