@atlaskit/codemod-cli 0.27.4 → 0.28.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/main.js +20 -15
  3. package/dist/cjs/presets/remove-token-fallbacks/remove-token-fallbacks.js +350 -43
  4. package/dist/cjs/presets/remove-token-fallbacks/utils/chunk.js +14 -0
  5. package/dist/cjs/presets/remove-token-fallbacks/utils/normalize-values.js +15 -9
  6. package/dist/cjs/presets/remove-token-fallbacks/utils/remove-unused-imports.js +2 -2
  7. package/dist/cjs/presets/remove-token-fallbacks/utils/reporter.js +28 -21
  8. package/dist/cjs/presets/remove-token-fallbacks/utils/token-processor.js +201 -71
  9. package/dist/es2019/main.js +7 -1
  10. package/dist/es2019/presets/remove-token-fallbacks/remove-token-fallbacks.js +164 -17
  11. package/dist/es2019/presets/remove-token-fallbacks/utils/chunk.js +8 -0
  12. package/dist/es2019/presets/remove-token-fallbacks/utils/normalize-values.js +15 -9
  13. package/dist/es2019/presets/remove-token-fallbacks/utils/remove-unused-imports.js +3 -3
  14. package/dist/es2019/presets/remove-token-fallbacks/utils/reporter.js +6 -1
  15. package/dist/es2019/presets/remove-token-fallbacks/utils/token-processor.js +121 -17
  16. package/dist/esm/main.js +20 -15
  17. package/dist/esm/presets/remove-token-fallbacks/remove-token-fallbacks.js +350 -41
  18. package/dist/esm/presets/remove-token-fallbacks/utils/chunk.js +8 -0
  19. package/dist/esm/presets/remove-token-fallbacks/utils/normalize-values.js +15 -9
  20. package/dist/esm/presets/remove-token-fallbacks/utils/remove-unused-imports.js +2 -2
  21. package/dist/esm/presets/remove-token-fallbacks/utils/reporter.js +28 -21
  22. package/dist/esm/presets/remove-token-fallbacks/utils/token-processor.js +201 -71
  23. package/dist/types/presets/remove-token-fallbacks/remove-token-fallbacks.d.ts +9 -1
  24. package/dist/types/presets/remove-token-fallbacks/types.d.ts +8 -0
  25. package/dist/types/presets/remove-token-fallbacks/utils/chunk.d.ts +1 -0
  26. package/dist/types/presets/remove-token-fallbacks/utils/normalize-values.d.ts +2 -1
  27. package/dist/types/presets/remove-token-fallbacks/utils/token-processor.d.ts +6 -0
  28. package/dist/types-ts4.5/presets/remove-token-fallbacks/remove-token-fallbacks.d.ts +9 -1
  29. package/dist/types-ts4.5/presets/remove-token-fallbacks/types.d.ts +8 -0
  30. package/dist/types-ts4.5/presets/remove-token-fallbacks/utils/chunk.d.ts +1 -0
  31. package/dist/types-ts4.5/presets/remove-token-fallbacks/utils/normalize-values.d.ts +2 -1
  32. package/dist/types-ts4.5/presets/remove-token-fallbacks/utils/token-processor.d.ts +6 -0
  33. package/package.json +4 -4
@@ -7,6 +7,7 @@ import { hasImportDeclaration } from '@hypermod/utils';
7
7
  import { findRoot } from '@manypkg/find-root';
8
8
  import chalk from 'chalk';
9
9
  import { getTokenMap } from './utils/all-tokens';
10
+ import { chunkArray } from './utils/chunk';
10
11
  import { getTeamInfo } from './utils/get-team-info';
11
12
  import { removeUnusedImports } from './utils/remove-unused-imports';
12
13
  import { removeUnusedVariables } from './utils/remove-unused-variables';
@@ -27,6 +28,14 @@ const execAsync = promisify(exec);
27
28
  * @param {boolean} [options.useLegacyColorTheme] - If true, uses the legacy theme for color token mapping.
28
29
  * @param {string} [options.reportFolder] - Directory path to output transformation reports. Reports will be generated only if this option is provided.
29
30
  * @param {boolean} [options.dry] - If true, performs a dry run without modifying the files.
31
+ * @param {string} [options.skipTokens] - A comma-separated list of token prefixes to exempt from automatic fallback removal. By default, 'border' tokens are always included in this list. Whether fallbacks for these tokens are removed when they exactly match depends on the preserveSkippedFallbacks option.
32
+ * @param {boolean} [options.preserveSkippedFallbacks] - If true, fallbacks for skipped tokens will never be removed, even if they exactly match the token value. If false (default), fallbacks for skipped tokens will be removed if they exactly match.
33
+ * @param {boolean} [options.skipEslint] - If true, skips running ESLint on modified files after transformation.
34
+ * @param {boolean} [options.skipPrettier] - If true, skips running Prettier on modified files after transformation.
35
+ * @param {number} [options.colorDifferenceThreshold] - The maximum allowed difference for color tokens to be considered acceptable for removal. Default is 15.
36
+ * @param {number} [options.spaceDifferenceThreshold] - The maximum allowed percentage difference for space tokens to be considered acceptable for removal. Default is 0.
37
+ * @param {number} [options.numericDifferenceThreshold] - The maximum allowed percentage difference for numeric tokens to be considered acceptable for removal. Default is 0.
38
+ * @param {number} [options.borderDifferenceThreshold] - The maximum allowed percentage difference for border tokens to be considered acceptable for removal. Default is 0.
30
39
  *
31
40
  * @returns {Promise<string>} A promise that resolves to the transformed source code as a string.
32
41
  */
@@ -45,6 +54,32 @@ export default async function transformer(fileInfo, {
45
54
  };
46
55
  if (options.verbose) {
47
56
  console.log(chalk.yellow(`Using ${options.useLegacyColorTheme ? 'legacy light' : 'light'} theme.`));
57
+ if (options.skipTokens) {
58
+ console.log(chalk.yellow(`Auto fallback exemptions active for: ${options.skipTokens}`));
59
+ }
60
+ if (options.preserveSkippedFallbacks) {
61
+ console.log(chalk.yellow(`Preserving all fallbacks for skipped tokens, even if they match exactly.`));
62
+ }
63
+ if (options.skipEslint) {
64
+ console.log(chalk.yellow(`Skipping ESLint post-processing.`));
65
+ }
66
+ if (options.skipPrettier) {
67
+ console.log(chalk.yellow(`Skipping Prettier post-processing.`));
68
+ }
69
+
70
+ // Log threshold values if they are set
71
+ if (options.colorDifferenceThreshold !== undefined) {
72
+ console.log(chalk.yellow(`Color difference threshold set to: ${options.colorDifferenceThreshold}`));
73
+ }
74
+ if (options.spaceDifferenceThreshold !== undefined) {
75
+ console.log(chalk.yellow(`Space difference threshold set to: ${options.spaceDifferenceThreshold}%`));
76
+ }
77
+ if (options.numericDifferenceThreshold !== undefined) {
78
+ console.log(chalk.yellow(`Numeric difference threshold set to: ${options.numericDifferenceThreshold}%`));
79
+ }
80
+ if (options.borderDifferenceThreshold !== undefined) {
81
+ console.log(chalk.yellow(`Border difference threshold set to: ${options.borderDifferenceThreshold}%`));
82
+ }
48
83
  }
49
84
  const tokenMap = getTokenMap((_options$useLegacyCol = options.useLegacyColorTheme) !== null && _options$useLegacyCol !== void 0 ? _options$useLegacyCol : false);
50
85
  const teamInfo = await getTeamInfo(fileInfo.path);
@@ -59,6 +94,9 @@ export default async function transformer(fileInfo, {
59
94
  });
60
95
  const results = await Promise.all(transformPromises);
61
96
  const unusedVars = [];
97
+ if (options.reportFolder) {
98
+ await writeReports(details, options.reportFolder);
99
+ }
62
100
  if (results.some(result => result.fallbackRemoved)) {
63
101
  const allImports = results.flatMap(result => {
64
102
  var _result$resolvedImpor;
@@ -75,20 +113,22 @@ export default async function transformer(fileInfo, {
75
113
  }
76
114
  removeUnusedImports(allImports, j);
77
115
  }
78
- }
79
- removeUnusedVariables(unusedVars, j);
80
- if (options.reportFolder) {
81
- await writeReports(details, options.reportFolder);
82
- }
83
- if (options.dry) {
84
- if (options.verbose) {
85
- console.log(chalk.cyan(`${fileInfo.path}: dry run mode active. Source was not modified.`));
116
+ if (unusedVars.length) {
117
+ removeUnusedVariables(unusedVars, j);
86
118
  }
119
+ if (options.dry) {
120
+ if (options.verbose) {
121
+ console.log(chalk.cyan(`${fileInfo.path}: dry run mode active. Source was not modified.`));
122
+ }
123
+ // Return the unmodified source if dryRun is true
124
+ return fileInfo.source;
125
+ } else {
126
+ // Return the transformed source
127
+ return source.toSource();
128
+ }
129
+ } else {
87
130
  // Return the unmodified source if dryRun is true
88
131
  return fileInfo.source;
89
- } else {
90
- // Return the transformed source
91
- return source.toSource();
92
132
  }
93
133
  }
94
134
  export const parser = 'tsx';
@@ -104,7 +144,7 @@ export async function beforeAll(options) {
104
144
 
105
145
  /**
106
146
  * Function executed after all transformations to combine individual file reports into a comprehensive transformation report.
107
- * It also applies prettier to the affected files.
147
+ * It also applies prettier and eslint (to remove dangling suppressions) to the affected files.
108
148
  */
109
149
  export async function afterAll(options) {
110
150
  if (options.reportFolder) {
@@ -115,16 +155,123 @@ export async function afterAll(options) {
115
155
  const filesTxtPath = path.join(options.reportFolder, 'files.txt');
116
156
  const fileContent = await fs.readFile(filesTxtPath, 'utf-8');
117
157
  if (fileContent.length > 0) {
118
- console.log(`Running prettier on files: ${chalk.magenta(fileContent)}`);
119
- await execAsync(`yarn prettier --write ${fileContent}`);
120
- console.log(chalk.green(`Prettier was run successfully`));
158
+ const filePaths = fileContent.split(/\r?\n/).filter(Boolean);
159
+
160
+ // Get the first file path and strip any quotes
161
+ const firstFilePath = filePaths[0].replace(/^['"]|['"]$/g, '');
162
+
163
+ // Determine the root directory using findRoot
164
+ const rootDir = await findRoot(path.dirname(firstFilePath));
165
+ console.log('Root directory:', rootDir);
166
+ await gitStage(filePaths, rootDir);
167
+ if (!options.skipEslint) {
168
+ await runEslint(filePaths, rootDir);
169
+ } else if (options.verbose) {
170
+ console.log(chalk.blue('Skipping ESLint post-processing as requested.'));
171
+ }
172
+ if (!options.skipPrettier) {
173
+ await runPrettier(filePaths, rootDir);
174
+ } else if (options.verbose) {
175
+ console.log(chalk.blue('Skipping Prettier post-processing as requested.'));
176
+ }
121
177
  }
122
178
  } catch (error) {
123
179
  if (error instanceof Error) {
124
- console.error(chalk.red(`Unexpected error running Prettier: ${error.message}`));
180
+ console.error(chalk.red(`Unexpected error: ${error.message}`));
125
181
  } else {
126
- console.error(chalk.red('An unknown error occurred while running Prettier.'));
182
+ console.error(chalk.red('An unknown error occurred.'));
183
+ }
184
+ }
185
+ }
186
+ }
187
+ async function gitStage(filePaths, cwd) {
188
+ const gitAddCommand = `git add ${filePaths.join(' ')}`;
189
+ console.log(`Executing command: ${gitAddCommand}`);
190
+ const {
191
+ stdout: gitAddStdout,
192
+ stderr: gitAddStderr
193
+ } = await execAsync(gitAddCommand, {
194
+ cwd
195
+ });
196
+ if (gitAddStdout) {
197
+ console.log(chalk.blue(`Git add output:\n${gitAddStdout}`));
198
+ }
199
+ if (gitAddStderr) {
200
+ console.error(chalk.yellow(`Git add errors:\n${gitAddStderr}`));
201
+ }
202
+ console.log(chalk.green(`All changes have been staged.`));
203
+ }
204
+ async function runPrettier(filePaths, cwd) {
205
+ const prettierCommand = `yarn prettier --write ${filePaths.join(' ')}`;
206
+ console.log(`Executing command: ${prettierCommand}`);
207
+ const {
208
+ stdout: prettierStdout,
209
+ stderr: prettierStderr
210
+ } = await execAsync(prettierCommand, {
211
+ cwd
212
+ });
213
+ if (prettierStdout) {
214
+ console.log(chalk.blue(`Prettier output:\n${prettierStdout}`));
215
+ }
216
+ if (prettierStderr) {
217
+ console.error(chalk.yellow(`Prettier errors:\n${prettierStderr}`));
218
+ }
219
+ console.log(chalk.green(`Prettier was run successfully`));
220
+ }
221
+ async function runEslint(filePaths, cwd) {
222
+ const fileChunks = chunkArray(filePaths, 20);
223
+ const totalChunks = fileChunks.length;
224
+ for (const [chunkIndex, fileChunk] of fileChunks.entries()) {
225
+ const eslintCommand = `yarn eslint ${fileChunk.join(' ')} --report-unused-disable-directives --fix`;
226
+ console.log(`Executing command for chunk ${chunkIndex + 1} of ${totalChunks}: ${eslintCommand}`);
227
+ try {
228
+ const result = await execAsync(eslintCommand, {
229
+ cwd
230
+ });
231
+ const {
232
+ stdout,
233
+ stderr
234
+ } = result;
235
+ if (stdout) {
236
+ console.log(chalk.blue(`ESLint output for chunk ${chunkIndex + 1} of ${totalChunks}:\n${stdout}`));
237
+ }
238
+ if (stderr) {
239
+ console.error(chalk.yellow(`ESLint errors for chunk ${chunkIndex + 1} of ${totalChunks}:\n${stderr}`));
240
+ }
241
+ } catch (error) {
242
+ console.error(chalk.red(`Error running ESLint on chunk ${chunkIndex + 1} of ${totalChunks}: ${error}`));
243
+
244
+ // Retry each file individually
245
+ console.log(chalk.yellow(`Retrying each file in chunk ${chunkIndex + 1} of ${totalChunks} individually...`));
246
+
247
+ // Chunk the files into smaller groups of 5 for parallel retry
248
+ const smallerChunks = chunkArray(fileChunk, 5);
249
+ const totalSmallerChunks = smallerChunks.length;
250
+ for (const [smallChunkIndex, smallerChunk] of smallerChunks.entries()) {
251
+ await Promise.all(smallerChunk.map(async file => {
252
+ try {
253
+ const individualEslintCommand = `yarn eslint ${file} --report-unused-disable-directives --fix`;
254
+ console.log(`Executing command for file in small chunk ${smallChunkIndex + 1} of ${totalSmallerChunks}: ${individualEslintCommand}`);
255
+ const result = await execAsync(individualEslintCommand, {
256
+ cwd
257
+ });
258
+ const {
259
+ stdout,
260
+ stderr
261
+ } = result;
262
+ if (stdout) {
263
+ console.log(chalk.blue(`ESLint output for file ${file} in small chunk ${smallChunkIndex + 1} of ${totalSmallerChunks}:\n${stdout}`));
264
+ }
265
+ if (stderr) {
266
+ console.error(chalk.yellow(`ESLint errors for file ${file} in small chunk ${smallChunkIndex + 1} of ${totalSmallerChunks}:\n${stderr}`));
267
+ }
268
+ } catch (fileError) {
269
+ console.error(chalk.red(`Error running ESLint on file ${file} in small chunk ${smallChunkIndex + 1} of ${totalSmallerChunks}: ${fileError}`));
270
+ }
271
+ }));
127
272
  }
128
273
  }
274
+ console.log(chalk.green(`Finished running ESLint for chunk ${chunkIndex + 1} of ${totalChunks}.`));
129
275
  }
276
+ console.log(chalk.green(`ESLint was run on all files successfully`));
130
277
  }
@@ -0,0 +1,8 @@
1
+ // Utility function to split an array into chunks
2
+ export const chunkArray = (array, chunkSize) => {
3
+ const result = [];
4
+ for (let i = 0; i < array.length; i += chunkSize) {
5
+ result.push(array.slice(i, i + chunkSize));
6
+ }
7
+ return result;
8
+ };
@@ -1,11 +1,17 @@
1
1
  import chalk from 'chalk';
2
2
  import { colorToHex, compareHex, isValidColor } from './color-utils';
3
3
 
4
- // so far allowing to remove only exact matches in values. Can be increased to auto-remove fallbacks with similar values
5
- const ACCEPTABLE_COLOR_DIFFERENCE = 0;
6
- const ACCEPTABLE_SPACE_DIFFERENCE = 0;
7
- const ACCEPTABLE_NUMERIC_DIFFERENCE = 0;
8
- export function normalizeValues(tokenKey, tokenValue, fallbackValue) {
4
+ // Default threshold values
5
+ const DEFAULT_COLOR_DIFFERENCE = 15;
6
+ const DEFAULT_SPACE_DIFFERENCE = 0;
7
+ const DEFAULT_NUMERIC_DIFFERENCE = 0;
8
+ const DEFAULT_BORDER_DIFFERENCE = 0;
9
+ export function normalizeValues(tokenKey, tokenValue, fallbackValue, options) {
10
+ // Use options thresholds or defaults
11
+ const colorDifference = (options === null || options === void 0 ? void 0 : options.colorDifferenceThreshold) !== undefined ? options.colorDifferenceThreshold : DEFAULT_COLOR_DIFFERENCE;
12
+ const spaceDifference = (options === null || options === void 0 ? void 0 : options.spaceDifferenceThreshold) !== undefined ? options.spaceDifferenceThreshold : DEFAULT_SPACE_DIFFERENCE;
13
+ const numericDifference = (options === null || options === void 0 ? void 0 : options.numericDifferenceThreshold) !== undefined ? options.numericDifferenceThreshold : DEFAULT_NUMERIC_DIFFERENCE;
14
+ const borderDifference = (options === null || options === void 0 ? void 0 : options.borderDifferenceThreshold) !== undefined ? options.borderDifferenceThreshold : DEFAULT_BORDER_DIFFERENCE;
9
15
  let tokenLogValue;
10
16
  let fallbackLogValue;
11
17
  let normalizedTokenValue = tokenValue;
@@ -26,15 +32,15 @@ export function normalizeValues(tokenKey, tokenValue, fallbackValue) {
26
32
  }
27
33
  if (normalizedTokenValue && normalizedFallbackValue) {
28
34
  difference = compareHex(normalizedTokenValue, normalizedFallbackValue);
29
- isAcceptableDifference = difference <= ACCEPTABLE_COLOR_DIFFERENCE;
35
+ isAcceptableDifference = difference <= colorDifference;
30
36
  }
31
- } else if (lowerCaseTokenKey.startsWith('space')) {
37
+ } else if (lowerCaseTokenKey.startsWith('space') || lowerCaseTokenKey.startsWith('border')) {
32
38
  const tokenValueInPx = tokenValue ? convertToPx(tokenValue) : undefined;
33
39
  const fallbackValueInPx = fallbackValue ? convertToPx(fallbackValue) : undefined;
34
40
  if (tokenValueInPx !== undefined && fallbackValueInPx !== undefined) {
35
41
  const maxVal = Math.max(tokenValueInPx, fallbackValueInPx);
36
42
  difference = Math.abs(tokenValueInPx - fallbackValueInPx) / maxVal * 100;
37
- isAcceptableDifference = difference <= ACCEPTABLE_SPACE_DIFFERENCE;
43
+ isAcceptableDifference = difference <= (lowerCaseTokenKey.startsWith('space') ? spaceDifference : borderDifference);
38
44
  }
39
45
  // Log the normalized values
40
46
  normalizedTokenValue = tokenValue;
@@ -48,7 +54,7 @@ export function normalizeValues(tokenKey, tokenValue, fallbackValue) {
48
54
  if (!isNaN(tokenValueNumber) && !isNaN(fallbackValueNumber)) {
49
55
  const maxVal = Math.max(tokenValueNumber, fallbackValueNumber);
50
56
  difference = Math.abs(tokenValueNumber - fallbackValueNumber) / maxVal * 100;
51
- isAcceptableDifference = difference <= ACCEPTABLE_NUMERIC_DIFFERENCE;
57
+ isAcceptableDifference = difference <= numericDifference;
52
58
  }
53
59
  // Log the normalized values
54
60
  normalizedTokenValue = tokenValue;
@@ -35,13 +35,13 @@ export function removeUnusedImports(importDeclarations, j) {
35
35
  return j(importDeclaration).find(j.ImportSpecifier).filter(s => removeIfUnused(s, importDeclaration)).size() > 0;
36
36
  };
37
37
  const processImportDeclaration = importDeclaration => {
38
- var _importDeclaration$va, _importDeclaration$va2;
39
- if (((_importDeclaration$va = importDeclaration.value.specifiers) === null || _importDeclaration$va === void 0 ? void 0 : _importDeclaration$va.length) === 0) {
38
+ var _importDeclaration$va, _importDeclaration$va2, _importDeclaration$va3, _importDeclaration$va4;
39
+ if (((_importDeclaration$va = importDeclaration.value) === null || _importDeclaration$va === void 0 ? void 0 : (_importDeclaration$va2 = _importDeclaration$va.specifiers) === null || _importDeclaration$va2 === void 0 ? void 0 : _importDeclaration$va2.length) === 0) {
40
40
  return false;
41
41
  }
42
42
  const hadUnusedDefaultImport = removeUnusedDefaultImport(importDeclaration);
43
43
  const hadUnusedNonDefaultImports = removeUnusedNonDefaultImports(importDeclaration);
44
- if (((_importDeclaration$va2 = importDeclaration.value.specifiers) === null || _importDeclaration$va2 === void 0 ? void 0 : _importDeclaration$va2.length) === 0) {
44
+ if (((_importDeclaration$va3 = importDeclaration.value) === null || _importDeclaration$va3 === void 0 ? void 0 : (_importDeclaration$va4 = _importDeclaration$va3.specifiers) === null || _importDeclaration$va4 === void 0 ? void 0 : _importDeclaration$va4.length) === 0) {
45
45
  j(importDeclaration).remove();
46
46
  return true;
47
47
  }
@@ -25,6 +25,10 @@ function escapeCsvValue(value) {
25
25
  }
26
26
  export async function clearFolder(reportFolder) {
27
27
  console.log('Clearing report folder:', reportFolder);
28
+ // Create the folder if it doesn't exist
29
+ await fs.mkdir(reportFolder, {
30
+ recursive: true
31
+ });
28
32
  const filesToDelete = await fs.readdir(reportFolder);
29
33
  for (const file of filesToDelete) {
30
34
  const filePath = path.join(reportFolder, file);
@@ -33,7 +37,8 @@ export async function clearFolder(reportFolder) {
33
37
  }
34
38
  async function saveFilePaths(reportFolder, files) {
35
39
  const filesTxtPath = path.join(reportFolder, 'files.txt');
36
- await fs.writeFile(filesTxtPath, Array.from(files).map(filePath => `"${filePath}"`).join(' '), 'utf-8');
40
+ const sortedFiles = Array.from(files).sort(); // Sort the file paths alphabetically
41
+ await fs.writeFile(filesTxtPath, sortedFiles.map(filePath => `"${filePath}"`).join('\n'), 'utf-8');
37
42
  }
38
43
  export async function combineReports(reportFolder) {
39
44
  console.log('Combining reports in folder:', reportFolder);
@@ -45,6 +45,29 @@ export class TokenProcessor {
45
45
  logError(message) {
46
46
  this.log(chalk.red(message));
47
47
  }
48
+
49
+ /**
50
+ * Checks if a token should be exempted from automatic fallback removal
51
+ * @param tokenKey The token key to check
52
+ * @returns An object containing whether the token should be exempted and related information
53
+ */
54
+ checkTokenExemption(tokenKey) {
55
+ // Create exemption list from user-provided skipTokens, and always include 'border'
56
+ const userExemptions = this.options.skipTokens ? this.options.skipTokens.split(',').map(item => item.trim()) : [];
57
+
58
+ // Always include 'border' in the exemption list
59
+ const exemptionList = [...userExemptions];
60
+ if (!exemptionList.includes('border')) {
61
+ exemptionList.push('border');
62
+ }
63
+ const isExemptedToken = exemptionList.some(prefix => tokenKey.startsWith(prefix));
64
+ const exemptedPrefix = isExemptedToken ? exemptionList.find(prefix => tokenKey.startsWith(prefix)) || null : null;
65
+ return {
66
+ shouldBeExempted: isExemptedToken,
67
+ exemptedPrefix,
68
+ exemptionList
69
+ };
70
+ }
48
71
  async processSingleToken(callPath) {
49
72
  var _callPath$node$loc2;
50
73
  const args = callPath.node.arguments;
@@ -67,20 +90,14 @@ export class TokenProcessor {
67
90
  resolvedLocalVarDeclaration: undefined
68
91
  };
69
92
  }
70
- const isSkipped = tokenKey.startsWith('elevation.shadow') || tokenKey.startsWith('font.body') || tokenKey.startsWith('font.heading');
71
- const tokenValue = isSkipped ? '' : this.tokenMap[tokenKey];
93
+ const tokenValue = this.tokenMap[tokenKey];
72
94
  this.logVerbose(`Token value from tokenMap: ${chalk.magenta(tokenValue)} for key: ${chalk.yellow(tokenKey)}`);
73
95
  const {
74
96
  rawFallbackValue,
75
97
  fallbackValue,
76
98
  resolvedImportDeclaration,
77
99
  resolvedLocalVarDeclaration
78
- } = isSkipped ? {
79
- rawFallbackValue: 'N/A',
80
- fallbackValue: undefined,
81
- resolvedImportDeclaration: undefined,
82
- resolvedLocalVarDeclaration: undefined
83
- } : await this.getFallbackValue(args[1]);
100
+ } = await this.getFallbackValue(args[1]);
84
101
  const {
85
102
  difference,
86
103
  isAcceptableDifference,
@@ -88,7 +105,7 @@ export class TokenProcessor {
88
105
  fallbackLogValue,
89
106
  normalizedTokenValue,
90
107
  normalizedFallbackValue
91
- } = normalizeValues(tokenKey, tokenValue, fallbackValue);
108
+ } = normalizeValues(tokenKey, tokenValue, fallbackValue, this.options);
92
109
  const areEqual = normalizedTokenValue === normalizedFallbackValue;
93
110
  const logData = {
94
111
  teamInfo: this.teamInfo,
@@ -103,7 +120,22 @@ export class TokenProcessor {
103
120
  let fallbackRemoved = false;
104
121
  let importDeclaration;
105
122
  let localVarDeclaration;
106
- if (areEqual || isAcceptableDifference || this.options.forceUpdate) {
123
+
124
+ // Check if token should be exempted
125
+ const {
126
+ shouldBeExempted,
127
+ exemptedPrefix
128
+ } = this.checkTokenExemption(tokenKey);
129
+
130
+ // Determine if we should modify this token based on the exemption status and settings
131
+ const shouldModifyToken =
132
+ // Always modify if not exempted and values match
133
+ areEqual && !shouldBeExempted ||
134
+ // Or if values don't exactly match but are acceptable to modify and not exempted
135
+ (isAcceptableDifference || this.options.forceUpdate) && !shouldBeExempted ||
136
+ // Or if exempted but values match exactly and we're not preserving skipped fallbacks
137
+ areEqual && shouldBeExempted && !this.options.preserveSkippedFallbacks;
138
+ if (shouldModifyToken) {
107
139
  this.log(chalk.green(areEqual ? 'Token value and fallback value are equal, removing fallback' : 'Token value and fallback value are within acceptable difference threshold, removing fallback'));
108
140
  args.pop();
109
141
  this.details.replaced.push(logData);
@@ -111,7 +143,7 @@ export class TokenProcessor {
111
143
  importDeclaration = resolvedImportDeclaration;
112
144
  localVarDeclaration = resolvedLocalVarDeclaration;
113
145
  } else {
114
- const message = normalizedFallbackValue === undefined ? `Fallback value could not be resolved` : `Values mismatched significantly`;
146
+ const message = shouldBeExempted ? this.options.preserveSkippedFallbacks && areEqual ? `Preserving fallback for exempted token '${tokenKey}' (matches exemption '${exemptedPrefix}')` : `Skip modifying exempted token '${tokenKey}' (matches exemption '${exemptedPrefix}')` : normalizedFallbackValue === undefined ? `Fallback value could not be resolved` : `Values mismatched significantly`;
115
147
  this.logError(message);
116
148
  if (this.options.addEslintComments) {
117
149
  addOrUpdateEslintIgnoreComment(this.j, tokenValue, fallbackValue, callPath);
@@ -211,6 +243,56 @@ export class TokenProcessor {
211
243
  let objectName;
212
244
  let propertyName;
213
245
  let fallbackValue;
246
+ let resolvedImportDeclaration;
247
+ let resolvedLocalVarDeclaration;
248
+
249
+ // Function to get full member expression path as string
250
+ const getFullMemberPath = node => {
251
+ if (node.type === 'Identifier') {
252
+ return node.name;
253
+ } else if (node.type === 'MemberExpression') {
254
+ return `${getFullMemberPath(node.object)}.${node.property.type === 'Identifier' ? node.property.name : ''}`;
255
+ }
256
+ return '';
257
+ };
258
+ const fullMemberPath = getFullMemberPath(fallbackValueNode);
259
+
260
+ // Detect long member expression paths
261
+ const pathSegments = fullMemberPath.split('.');
262
+ if (pathSegments.length > 2) {
263
+ this.logVerbose(`Detected long member expression: ${chalk.yellow(fullMemberPath)}. Just resolving import or local variable declaration.`);
264
+
265
+ // Find the import statement or local variable for the top-level object
266
+ objectName = pathSegments[0];
267
+
268
+ // Check if it's a local variable
269
+ const localVarDeclaration = this.source.find(this.j.VariableDeclarator, {
270
+ id: {
271
+ name: objectName
272
+ }
273
+ }).at(0);
274
+ if (localVarDeclaration.size()) {
275
+ resolvedLocalVarDeclaration = localVarDeclaration.paths()[0];
276
+ this.logVerbose(`Resolved local variable declaration for: ${chalk.yellow(objectName)}`);
277
+ } else {
278
+ // Search for import declaration
279
+ const importDeclaration = this.source.find(this.j.ImportDeclaration).filter(this.createImportFilter(objectName)).at(0);
280
+ if (importDeclaration.size()) {
281
+ resolvedImportDeclaration = importDeclaration.paths()[0];
282
+ this.logVerbose(`Resolved import declaration for: ${chalk.yellow(objectName)}`);
283
+ } else {
284
+ this.logError(`Could not resolve import or local variable for: ${chalk.yellow(objectName)}`);
285
+ }
286
+ }
287
+ return {
288
+ rawFallbackValue: fullMemberPath,
289
+ fallbackValue: undefined,
290
+ resolvedImportDeclaration,
291
+ resolvedLocalVarDeclaration
292
+ };
293
+ }
294
+
295
+ // Existing logic for member expressions with shorter paths
214
296
  if (fallbackValueNode.object.type === 'Identifier') {
215
297
  objectName = fallbackValueNode.object.name;
216
298
  }
@@ -230,7 +312,6 @@ export class TokenProcessor {
230
312
  }
231
313
  const rawFallbackValue = `${objectName}.${propertyName}`;
232
314
  this.logVerbose(`Fallback is a member expression: ${chalk.yellow(rawFallbackValue)}, attempting to resolve...`);
233
- let resolvedImportDeclaration;
234
315
 
235
316
  // Find the import statement for the object
236
317
  const importDeclaration = this.source.find(this.j.ImportDeclaration).filter(this.createImportFilter(objectName)).at(0);
@@ -253,22 +334,45 @@ export class TokenProcessor {
253
334
  }
254
335
  async processFallbackAsTemplateLiteral(fallbackValueNode) {
255
336
  const expressions = fallbackValueNode.expressions;
337
+ const quasis = fallbackValueNode.quasis;
338
+ let resolvedImportDeclaration;
339
+ let resolvedLocalVarDeclaration;
256
340
  let rawFallbackValue = '';
257
341
  let fallbackValue;
258
- const quasis = fallbackValueNode.quasis;
259
342
  if (expressions.length !== 1 || quasis.length !== 2) {
260
343
  this.logError(`Unsupported template literal structure`);
344
+
345
+ // Attempt to resolve any imports or local variables used in expressions
346
+ for (const expression of expressions) {
347
+ if (expression.type === 'Identifier') {
348
+ const result = await this.processFallbackAsIdentifier(expression);
349
+ if (result.resolvedImportDeclaration) {
350
+ resolvedImportDeclaration = result.resolvedImportDeclaration;
351
+ }
352
+ if (result.resolvedLocalVarDeclaration) {
353
+ resolvedLocalVarDeclaration = result.resolvedLocalVarDeclaration;
354
+ }
355
+ } else if (expression.type === 'MemberExpression') {
356
+ const result = await this.processFallbackAsMemberExpression(expression);
357
+ if (result.resolvedImportDeclaration) {
358
+ resolvedImportDeclaration = result.resolvedImportDeclaration;
359
+ }
360
+ if (result.resolvedLocalVarDeclaration) {
361
+ resolvedLocalVarDeclaration = result.resolvedLocalVarDeclaration;
362
+ }
363
+ }
364
+ }
261
365
  return {
262
366
  rawFallbackValue,
263
367
  fallbackValue,
264
- resolvedImportDeclaration: undefined,
265
- resolvedLocalVarDeclaration: undefined
368
+ resolvedImportDeclaration,
369
+ resolvedLocalVarDeclaration
266
370
  };
267
371
  }
372
+
373
+ // Handle supported template literal structures as before
268
374
  let exprValue;
269
375
  const expression = expressions[0];
270
- let resolvedImportDeclaration;
271
- let resolvedLocalVarDeclaration;
272
376
  if (expression.type === 'Identifier') {
273
377
  const result = await this.processFallbackAsIdentifier(expression);
274
378
  exprValue = result.fallbackValue;
package/dist/esm/main.js CHANGED
@@ -128,7 +128,7 @@ var resolveTransform = /*#__PURE__*/function () {
128
128
  }();
129
129
  var runTransform = /*#__PURE__*/function () {
130
130
  var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(filePaths, transform, flags) {
131
- var logger, codemodDirs, transformPath, args, jscodeshiftContent, jscodeshiftContentNew, transformModule;
131
+ var logger, codemodDirs, transformPath, ignorePatterns, ignoreArgs, args, jscodeshiftContent, jscodeshiftContentNew, transformModule;
132
132
  return _regeneratorRuntime.wrap(function _callee3$(_context3) {
133
133
  while (1) switch (_context3.prev = _context3.next) {
134
134
  case 0:
@@ -153,15 +153,20 @@ var runTransform = /*#__PURE__*/function () {
153
153
  }
154
154
  case 9:
155
155
  logger.log(chalk.green("Transforming files matching these extensions '".concat(chalk.bold(flags.extensions), "'...")));
156
- transformPath = getTransformPath(transform);
156
+ transformPath = getTransformPath(transform); // Split the ignorePattern by '|' and add each as a separate --ignore-pattern
157
+ ignorePatterns = flags.ignorePattern.split('|').filter(Boolean);
158
+ ignoreArgs = ignorePatterns.map(function (pattern) {
159
+ return "--ignore-pattern=".concat(pattern);
160
+ });
157
161
  args = Object.keys(flags).reduce(function (acc, key) {
158
162
  if (!['transform', 'parser', 'extensions', 'ignorePattern', 'logger', 'packages', 'sinceRef', 'preset', 'failOnError'].includes(key)) {
159
163
  acc.unshift("--".concat(key, "=").concat(flags[key]));
160
164
  }
161
165
  return acc;
162
- }, ["--transform=".concat(transformPath), "--ignore-pattern=".concat(flags.ignorePattern), "--parser=".concat(flags.parser), "--extensions=".concat(flags.extensions),
166
+ }, ["--transform=".concat(transformPath)].concat(_toConsumableArray(ignoreArgs), [// Spread the ignoreArgs array here
167
+ "--parser=".concat(flags.parser), "--extensions=".concat(flags.extensions),
163
168
  // Limit CPUs to 8 to prevent issues when running on CI with a large amount of cpus
164
- '--cpus=8'].concat(_toConsumableArray(codemodDirs)));
169
+ '--cpus=8'], _toConsumableArray(codemodDirs)));
165
170
  if (flags.failOnError) {
166
171
  args.unshift('--fail-on-error');
167
172
  }
@@ -179,24 +184,24 @@ var runTransform = /*#__PURE__*/function () {
179
184
  console.warn("Error loading transform module: ".concat(transformPath, ". Skipping lifecycle hooks."));
180
185
  }
181
186
  if (!transformModule) {
182
- _context3.next = 20;
187
+ _context3.next = 22;
183
188
  break;
184
189
  }
185
- _context3.next = 20;
186
- return processLifecycleHook('beforeAll', transformModule, logger, transform, flags);
187
- case 20:
188
190
  _context3.next = 22;
191
+ return processLifecycleHook('beforeAll', transformModule, logger, transform, flags);
192
+ case 22:
193
+ _context3.next = 24;
189
194
  return spawn(jscodeshift, args, {
190
195
  stdio: 'inherit'
191
196
  });
192
- case 22:
197
+ case 24:
193
198
  if (!transformModule) {
194
- _context3.next = 25;
199
+ _context3.next = 27;
195
200
  break;
196
201
  }
197
- _context3.next = 25;
202
+ _context3.next = 27;
198
203
  return processLifecycleHook('afterAll', transformModule, logger, transform, flags);
199
- case 25:
204
+ case 27:
200
205
  case "end":
201
206
  return _context3.stop();
202
207
  }
@@ -302,7 +307,7 @@ var defaultFlags = {
302
307
  ignorePattern: 'node_modules',
303
308
  logger: console
304
309
  };
305
- function processLifecycleHook(_x9, _x10, _x11, _x12, _x13) {
310
+ function processLifecycleHook(_x9, _x0, _x1, _x10, _x11) {
306
311
  return _processLifecycleHook.apply(this, arguments);
307
312
  }
308
313
  function _processLifecycleHook() {
@@ -334,7 +339,7 @@ function _processLifecycleHook() {
334
339
  }));
335
340
  return _processLifecycleHook.apply(this, arguments);
336
341
  }
337
- export default function main(_x14, _x15) {
342
+ export default function main(_x12, _x13) {
338
343
  return _main.apply(this, arguments);
339
344
  }
340
345
  function _main() {
@@ -350,7 +355,7 @@ function _main() {
350
355
  case 4:
351
356
  _yield$parseArgs = _context6.sent;
352
357
  packages = _yield$parseArgs.packages;
353
- _process$env$_PACKAGE = "0.27.4", _PACKAGE_VERSION_ = _process$env$_PACKAGE === void 0 ? '0.0.0-dev' : _process$env$_PACKAGE;
358
+ _process$env$_PACKAGE = "0.28.1", _PACKAGE_VERSION_ = _process$env$_PACKAGE === void 0 ? '0.0.0-dev' : _process$env$_PACKAGE;
354
359
  logger.log(chalk.bgBlue(chalk.black("\uD83D\uDCDA Atlassian-Frontend codemod library @ ".concat(_PACKAGE_VERSION_, " \uD83D\uDCDA"))));
355
360
  if (packages && packages.length > 0) {
356
361
  logger.log(chalk.gray("Searching for codemods for newer versions of the following packages: ".concat(packages.map(function (pkg) {