@elliemae/ds-codemods 3.26.0-next.6 → 3.26.0-next.8

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.
@@ -11,6 +11,14 @@ function parseArgumentsIntoOptions(rawArgs) {
11
11
  '--projectFolderPath': String,
12
12
  '--ignorePackagesGlobPattern': String,
13
13
  '--ignoreFilesGlobPattern': String,
14
+ '--outputPath': String,
15
+ '--startingDirPath': String,
16
+ '--gitIgnorePath': String,
17
+ // CLI flag, not available as a question -> Boolean are not supported by the combo of arg and (dynamic) inquirer.
18
+ // it's impossible to determine if the user has passed the flag or not because
19
+ // for arg Boolean are either true or undefined.
20
+ '--debug': Boolean,
21
+ '--fileExtensions': String,
14
22
  },
15
23
  {
16
24
  argv: rawArgs.slice(2),
@@ -23,6 +31,11 @@ function parseArgumentsIntoOptions(rawArgs) {
23
31
  projectFolderPath: args['--projectFolderPath'],
24
32
  ignorePackagesGlobPattern: args['--ignorePackagesGlobPattern'],
25
33
  ignoreFilesGlobPattern: args['--ignoreFilesGlobPattern'],
34
+ outputPath: args['--outputPath'],
35
+ startingDirPath: args['--startingDirPath'],
36
+ debug: args['--debug'], // CLI flag, not available as a question.
37
+ fileExtensions: args['--fileExtensions'], // CLI flag, not available as a question.
38
+ gitIgnorePath: args['--gitIgnorePath'],
26
39
  script: args._[0],
27
40
  };
28
41
  }
@@ -0,0 +1,31 @@
1
+ export const getDeprecatedUsageReportQuestions = (originalOptions) => {
2
+ const extraOptions = [];
3
+ const { outputPath, startingDirPath, gitIgnorePath } = originalOptions;
4
+ if (!outputPath && outputPath !== '') {
5
+ extraOptions.push({
6
+ type: 'input',
7
+ name: 'outputPath',
8
+ message: 'path to the .csv file to write the report to',
9
+ default: './dimsum-deprecated-components-usage-report.csv',
10
+ });
11
+ }
12
+ if (!startingDirPath && startingDirPath !== '') {
13
+ extraOptions.push({
14
+ type: 'input',
15
+ name: 'startingDirPath',
16
+ message: 'Please provide the project root directory (where node_modules folder lives)',
17
+ default: './',
18
+ });
19
+ }
20
+ if (!gitIgnorePath && gitIgnorePath !== '') {
21
+ extraOptions.push({
22
+ type: 'input',
23
+ name: 'gitIgnorePath',
24
+ message: 'Please provide the path to the .gitignore file',
25
+ default: './.gitignore',
26
+ });
27
+ }
28
+ // --debug is a CLI flag, not available as a question.
29
+
30
+ return extraOptions;
31
+ };
@@ -4,7 +4,7 @@ export const getDeprecatedPackagesQuestions = (originalOptions) => {
4
4
  if (!cwd && cwd !== '') {
5
5
  extraOptions.push({
6
6
  type: 'input',
7
- name: 'globPatternIgnore',
7
+ name: 'cwd',
8
8
  message: 'Please provide the project root directory (where node_modules folder lives)',
9
9
  default: './',
10
10
  });
@@ -6,7 +6,7 @@ import {
6
6
  brightCyanStr,
7
7
  whiteStr,
8
8
  } from '../../../utils/CLI_COLORS.mjs';
9
- import { DEPRECATED_PACKAGES } from './constants.mjs';
9
+ import { DEPRECATED_PACKAGES } from '../../../constants/deprecatedPackages.mjs';
10
10
 
11
11
  const logDeprecatedComponent = ({ name, solution, severity, confluence }) => {
12
12
  const [sevDesc, sevLevel] = severity;
@@ -0,0 +1,36 @@
1
+ import fs from 'fs';
2
+ import { filePathsWithGitIgnore } from '../../../utils/filepathsMatchers.mjs';
3
+ import { generateCsvRows, checkFileForDeprecatedComponents } from './utils.mjs';
4
+
5
+ /**
6
+ * generates a report of deprecated components usage
7
+ * @param {object} options - options *
8
+ * @param {string} options.outputPath - path to the .csv file to write the report to
9
+ * @param {string} options.startingDirPath - path to the project root directory (where node_modules folder lives)
10
+ * @param {string} options.gitIgnorePath - path to the .gitignore file
11
+ * @param {boolean} options.debug - debug flag
12
+ *
13
+ * @returns {void}
14
+ */
15
+ export const deprecatedComponentsUsageReport = (options) => {
16
+ const { outputPath } = options;
17
+
18
+ const filesToParse = filePathsWithGitIgnore(options);
19
+ // we have a lot more info than the one we use in the csv,
20
+ // in case we want to expand it in the future we can store it in the map...
21
+ // const problematicFiles = new Map();
22
+
23
+ const csvRows = ['Legacy Component,Legacy Package,Filename,File Path'];
24
+ filesToParse.forEach((filePath) => {
25
+ const fileReport = checkFileForDeprecatedComponents(filePath);
26
+ if (fileReport.isFileProblematic) {
27
+ // we have a lot more info than the one we use in the csv,
28
+ // in case we want to expand it in the future we can store it in the map...
29
+ // problematicFiles.set(filePath, fileReport);
30
+ csvRows.push(...generateCsvRows(fileReport, filePath));
31
+ }
32
+ });
33
+ fs.writeFileSync(outputPath, csvRows.join('\n'));
34
+ };
35
+
36
+ export default deprecatedComponentsUsageReport;
@@ -0,0 +1,55 @@
1
+ // eslint-disable-next-line max-len, max-len
2
+ // /^import\s*((?:[\S]*(?:\s*as\s*[\S]*)?(?:\s*,\s*(?:(?:[\S]*\s*as\s*[\S]*)|(?:\{(?:[\s\S](?!{))*?\})))?)|(?:\{(?:[\s\S](?!{))*?\}))\s*from\s*['"](@elliemae\/ds-\S+)['"]/gms
3
+ /*
4
+ reg-exp divided into units for readability:
5
+ ^import\s* - matches "import" followed by any number of spaces,
6
+ constraints the match to the beginning of the line
7
+ ( - start of the first capturing group
8
+ // this one captures:
9
+ - the default import (e.g. import Button from 'wherever')
10
+ - the named import (e.g. import Button as Btn from 'wherever')
11
+ - the default import followed by a comma (e.g. import Button, { Input } from 'wherever')
12
+ (?:
13
+ [\S]* - matches any non-whitespace character any number of times
14
+ this is meant to catch the default import (e.g. import Button from 'wherever')
15
+ (?:\s*as\s*[\S]*)? - optionally matches the named import (e.g. import Button as Btn from 'wherever')
16
+
17
+ (?: - optionally matches imports with comma after them
18
+ (e.g. import Button, { Input } from 'wherever')
19
+ \s*,\s* - matches a comma, with any number of spaces before and after it
20
+ - in ESM after a comma you have a named default import or a destructured import...
21
+ (?:
22
+ (?:[\S]*\s*as\s*[\S]*) - the catching of "default" imports as above,
23
+ no optional named import because after comma you can't repeat the default import
24
+ |
25
+ (?:\{ - between curly braces START
26
+ (?:[\s\S] - anything (including new lines)
27
+ (?!{) - as long as it's not followed by another curly brace
28
+ (avoid matching import from other packages followed by import from @elliemae/ds-* )
29
+ )*?
30
+ \}) - between curly braces END
31
+ )
32
+ )?
33
+ )
34
+ | - OR
35
+ // this one captures:
36
+ - the destructured import (e.g. import { Button } from 'wherever')
37
+ (?:\{ - between curly braces START
38
+ (?:[\s\S] - anything (including new lines)
39
+ (?!{) - as long as it's not followed by another curly brace
40
+ (avoid matching import from other packages followed by import from @elliemae/ds-* )
41
+ )*?
42
+ \}) - between curly braces END
43
+
44
+ )
45
+ \s*from\s* - matches "from" with any number of spaces before and after it
46
+ - matches a string between single or double quotes that starts with "@elliemae/ds-*"
47
+ ['"]
48
+ (@elliemae\/ds-\S+)
49
+ ['"]
50
+
51
+ https://regex101.com/r/7BsJPF/1
52
+ */
53
+ export const dimsumImportStatementsRegExp =
54
+ // eslint-disable-next-line max-len
55
+ /^import\s*((?:[\S]*(?:\s*as\s*[\S]*)?(?:\s*,\s*(?:(?:[\S]*\s*as\s*[\S]*)|(?:\{(?:[\s\S](?!{))*?\})))?)|(?:\{(?:[\s\S](?!{))*?\}))\s*from\s*['"](@elliemae\/ds-\S+)['"]/gms;
@@ -0,0 +1,256 @@
1
+ /* eslint-disable max-lines */
2
+ /* eslint-disable max-statements */
3
+ /* eslint-disable max-len */
4
+ import fs from 'fs';
5
+ import { dimsumImportStatementsRegExp } from './regexp.mjs';
6
+ import {
7
+ LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT,
8
+ LEGACY_WITH_NEW_MEDIUM_MIGRATION_EFFORT,
9
+ LEGACY_WITH_NEW_LARGE_MIGRATION_EFFORT,
10
+ DISMISSED_WITH_EXAMPLE,
11
+ } from '../../../constants/index.mjs';
12
+
13
+ /**
14
+ * @typedef {typeof LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT
15
+ * | typeof LEGACY_WITH_NEW_MEDIUM_MIGRATION_EFFORT
16
+ * | typeof LEGACY_WITH_NEW_LARGE_MIGRATION_EFFORT} deprecatedEffortMap
17
+ * @typedef {deprecatedEffortMap[number]} deprecatedEffortMapEntry
18
+ * @typedef {typeof DISMISSED_WITH_EXAMPLE[number]} dismissedWithExampleEntry
19
+ *
20
+ * @typedef { object } regexpInfo
21
+ * @property { string } regexpInfo.fullMatch
22
+ * @property { string } regexpInfo.importedComponent
23
+ * @property { string } regexpInfo.packageName
24
+ *
25
+ * @typedef {object} matchSharedPropieties
26
+ * @property {regexpInfo} matchSharedPropieties.regexpInfo
27
+ *
28
+ * @typedef { object } matchWithEffortEntryPropeties
29
+ * @property { deprecatedEffortMapEntry } matchWithEffortEntryPropeties.matchingEffortMetainfo
30
+ *
31
+ * @typedef { object } matchDeprecatedEntryPropeties
32
+ * @property { dismissedWithExampleEntry } matchDeprecatedEntryPropeties.matchingEffortMetainfo
33
+ *
34
+ * @typedef {matchSharedPropieties & matchWithEffortEntryPropeties} matchWithEffortMetaInfo
35
+ * @typedef {matchSharedPropieties & matchDeprecatedEntryPropeties} matchDeprecatedWithExample
36
+ *
37
+ * @typedef {matchWithEffortMetaInfo | matchDeprecatedWithExample} matchedEffortMapValue
38
+ *
39
+ * @typedef {Map<string, matchedEffortMapValue>} matchedEffortMap
40
+ *
41
+ * @typedef {object} fileReport
42
+ * @property {boolean} fileReport.isFileProblematic
43
+ * @property {matchWithEffortMetaInfo} fileReport.deprecatedSmallEffort
44
+ * @property {matchWithEffortMetaInfo} fileReport.deprecatedMediumEffort
45
+ * @property {matchWithEffortMetaInfo} fileReport.deprecatedLargeEffort
46
+ * @property {matchDeprecatedWithExample} fileReport.deprecatedDismissed
47
+ *
48
+ */
49
+
50
+ // we generate some supporting data structures to make the matching more efficient from the existing constants...
51
+ // we do this runtime because we want to avoid having to maintain this manually and this is a one time cost runtime wise with the cache...
52
+ const runtimeDeprecatedDSReportCache = new Map();
53
+ const getSupportingDataStructures = (deprecatedEffortMap, effortCacheUID) => {
54
+ if (runtimeDeprecatedDSReportCache.has(effortCacheUID)) {
55
+ return runtimeDeprecatedDSReportCache.get(effortCacheUID);
56
+ }
57
+ // we generate some supporting data structures to make the matching more efficient from the existing constants...
58
+ const effortsSamePackageList = [];
59
+ const effortsSamePackageOldComponentsList = [];
60
+ const effortsDifferentPackageList = [];
61
+
62
+ deprecatedEffortMap.forEach((DeprecatedPackageMeta) => {
63
+ const { oldPackage, component, newPackage } = DeprecatedPackageMeta;
64
+ if (oldPackage === newPackage) {
65
+ if (!effortsSamePackageList.includes(oldPackage)) effortsSamePackageList.push(oldPackage);
66
+ if (!effortsSamePackageOldComponentsList.includes(component)) effortsSamePackageOldComponentsList.push(component);
67
+ } else {
68
+ if (!effortsDifferentPackageList.includes(oldPackage)) effortsDifferentPackageList.push(oldPackage);
69
+ }
70
+ });
71
+
72
+ runtimeDeprecatedDSReportCache.set(effortCacheUID, {
73
+ effortsSamePackageList,
74
+ effortsSamePackageOldComponentsList,
75
+ effortsDifferentPackageList,
76
+ });
77
+
78
+ return runtimeDeprecatedDSReportCache.get(effortCacheUID);
79
+ };
80
+
81
+ /**
82
+ * given a list of import statements, it checks if any of them is deprecated based on the constants provided
83
+ * @param {string[][]} importStatements - list of import statements to check generated by the regex
84
+ * @param {LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT} deprecatedEffortMap - list of constants to check against
85
+ * @param {string} effortCacheUID - a string to identify the effort level (small, medium, large) in the cache if existing
86
+ * @returns {Map<string, matchWithEffortMetaInfo>} - a map of matched statements
87
+ */
88
+ const checkDeprecatedEffort = (importStatements, deprecatedEffortMap, effortCacheUID = '') => {
89
+ const { effortsSamePackageList, effortsSamePackageOldComponentsList, effortsDifferentPackageList } =
90
+ getSupportingDataStructures(deprecatedEffortMap, effortCacheUID);
91
+
92
+ const matchedEfforts = new Map();
93
+
94
+ importStatements.forEach((importStatement) => {
95
+ const [fullMatch, importedComponent, packageName] = importStatement;
96
+ // first we check if we need to care about this package at all
97
+ // we do not care if the package is not in the list of packages that have small migration effort
98
+ // when the deprecation is in another package we know for sure that the statement is deprecated
99
+ const statementIsDeprecatedForSure =
100
+ effortsDifferentPackageList.findIndex((pck) => pck.includes(packageName)) !== -1;
101
+ // when the deprecation is in the same package we need to check if the component itself is deprecated
102
+ const statementIsPotentiallyDeprecated =
103
+ effortsSamePackageList.findIndex((pck) => pck.includes(packageName)) !== -1;
104
+ let isStatementActuallyDeprecated = false;
105
+ if (!statementIsDeprecatedForSure && statementIsPotentiallyDeprecated) {
106
+ effortsSamePackageOldComponentsList.forEach((deprecatedComponent) => {
107
+ if (importedComponent.includes(deprecatedComponent)) {
108
+ isStatementActuallyDeprecated = true;
109
+ }
110
+ });
111
+ }
112
+ const statementIsDeprecated = statementIsDeprecatedForSure || isStatementActuallyDeprecated;
113
+ if (statementIsDeprecated) {
114
+ // we iterate the deprecatedEffortMap again now that we know the statement is deprecated,
115
+ // we do this so we can reach the constant meta-informations (like the new package name, severityTags, etc...)
116
+ // technically this is O(n * m) where n is ~<10 and m is ~<50 so even the worst case is not that bad...
117
+ const matchingEffortMetainfoIndx = deprecatedEffortMap.findIndex((effortMetainfo) => {
118
+ if (statementIsDeprecatedForSure) {
119
+ return effortMetainfo.oldPackage === packageName;
120
+ }
121
+ return importedComponent.includes(effortMetainfo.component);
122
+ });
123
+ matchedEfforts.set(fullMatch, {
124
+ regexpInfo: { fullMatch, importedComponent, packageName },
125
+ matchingEffortMetainfo: deprecatedEffortMap[matchingEffortMetainfoIndx],
126
+ });
127
+ }
128
+ });
129
+ return matchedEfforts;
130
+ };
131
+ /**
132
+ * given a list of import statements, it checks if any of them is a low effort deprecation
133
+ * @param {string[][]} importStatements - list of import statements to check generated by the regex
134
+ * @returns {Map<string, matchWithEffortMetaInfo>} - a map of matched statements
135
+ */
136
+ export const checkDeprecatedSmallEffort = (importStatements) =>
137
+ checkDeprecatedEffort(importStatements, LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT, 'small');
138
+
139
+ /**
140
+ * given a list of import statements, it checks if any of them is a medium effort deprecation
141
+ * @param {string[][]} importStatements - list of import statements to check generated by the regex
142
+ * @returns {Map<string, matchWithEffortMetaInfo>} - a map of matched statements
143
+ */
144
+ export const checkDeprecatedMediumEffort = (importStatements) =>
145
+ checkDeprecatedEffort(importStatements, LEGACY_WITH_NEW_MEDIUM_MIGRATION_EFFORT, 'medium');
146
+
147
+ /**
148
+ * given a list of import statements, it checks if any of them is a large effort deprecation
149
+ * @param {string[][]} importStatements - list of import statements to check generated by the regex
150
+ * @returns {Map<string, matchWithEffortMetaInfo>} - a map of matched statements
151
+ */
152
+ export const checkDeprecatedLargeEffort = (importStatements) =>
153
+ checkDeprecatedEffort(importStatements, LEGACY_WITH_NEW_LARGE_MIGRATION_EFFORT, 'large');
154
+
155
+ /**
156
+ * given a list of import statements, it checks if any of them is a large effort deprecation
157
+ * @param {string[][]} importStatements - list of import statements to check generated by the regex
158
+ * @returns {Map<string, matchDeprecatedWithExample>} - a map of matched statements
159
+ */
160
+ export const checkDeprecatedDismissed = (importStatements) => {
161
+ const matchedEfforts = new Map();
162
+ importStatements.forEach((importStatement) => {
163
+ const [fullMatch, importedComponent, packageName] = importStatement;
164
+ const matchingEffortMetainfoIndx = DISMISSED_WITH_EXAMPLE.findIndex((effortMetainfo) =>
165
+ importedComponent.includes(effortMetainfo.component),
166
+ );
167
+ if (matchingEffortMetainfoIndx === -1) return;
168
+ matchedEfforts.set(fullMatch, {
169
+ regexpInfo: { fullMatch, importedComponent, packageName },
170
+ matchingEffortMetainfo: DISMISSED_WITH_EXAMPLE[matchingEffortMetainfoIndx],
171
+ });
172
+ });
173
+ return matchedEfforts;
174
+ };
175
+
176
+ /**
177
+ * given a file path, it checks if the file contains any deprecated components
178
+ * @param {string} filePath - path to the file to check
179
+ * @returns {fileReport} - a report of the file
180
+ */
181
+ export const checkFileForDeprecatedComponents = (filePath) => {
182
+ const fileAsString = fs.readFileSync(filePath, 'utf8');
183
+ const matches = fileAsString.matchAll(dimsumImportStatementsRegExp);
184
+ const importStatements = [...matches]; // convert iterator to array - matchAll returns an iterator...
185
+ const deprecatedSmallEffort = checkDeprecatedSmallEffort(importStatements);
186
+ const deprecatedMediumEffort = checkDeprecatedMediumEffort(importStatements);
187
+ const deprecatedLargeEffort = checkDeprecatedLargeEffort(importStatements);
188
+ const deprecatedDismissed = checkDeprecatedDismissed(importStatements);
189
+
190
+ const isFileProblematic =
191
+ deprecatedSmallEffort.size > 0 ||
192
+ deprecatedMediumEffort.size > 0 ||
193
+ deprecatedLargeEffort.size > 0 ||
194
+ deprecatedDismissed.size > 0;
195
+
196
+ return {
197
+ isFileProblematic,
198
+ deprecatedSmallEffort,
199
+ deprecatedMediumEffort,
200
+ deprecatedLargeEffort,
201
+ deprecatedDismissed,
202
+ };
203
+ };
204
+
205
+ /**
206
+ * Helper function to generate a csv row from a "matched effort" object
207
+ * @param {matchedEffortMapValue} matchedEffort
208
+ * @param {string} filePath
209
+ * @returns {string} - csv row
210
+ */
211
+ const generateCsvRowFromMatchedEffort = (matchedEffort, filePath) => {
212
+ const { matchingEffortMetainfo } = matchedEffort;
213
+ // matchingEffortMetainfo type is deprecatedEffortMapEntry | dismissedWithExampleEntry
214
+ // they share 'component' and 'oldPackage' properties and for the current use case we only care about those
215
+ // if we want to expand the csv in the future we have to take care of this difference in the types
216
+ const { component, oldPackage } = matchingEffortMetainfo;
217
+ const filename = filePath.split('/').pop();
218
+ const cwdFolders = process.cwd().split('/');
219
+ const pathFolders = filePath.split('/');
220
+ const latestMatchingFolderIndx = pathFolders.findIndex((folder, indx) => folder !== cwdFolders[indx]);
221
+ const filePathFolders = pathFolders.slice(latestMatchingFolderIndx);
222
+ const reducedFilePath = filePathFolders.join('/');
223
+ return `${component},${oldPackage},${filename},${reducedFilePath}`;
224
+ };
225
+ /**
226
+ * Helper function to generate a list of csv rows from a given file report
227
+ * @param {fileReport} fileReport
228
+ * @param {string} filePath
229
+ * @returns {string[]} - list of csv rows
230
+ */
231
+ export const generateCsvRows = (fileReport, filePath) => {
232
+ const { deprecatedSmallEffort, deprecatedMediumEffort, deprecatedLargeEffort, deprecatedDismissed } = fileReport;
233
+ // csv structure = 'Legacy Component,Legacy Package,Filename,File Path'
234
+ // Legacy Component -> matchingEffortMetainfo.component
235
+ // Legacy Package -> matchingEffortMetainfo.oldPackage
236
+ // Filename -> filePath last part
237
+ // File Path -> filePath starting from latest matching folder from cwd
238
+ // (if cwd is /a/b/c/d/ and file path /a/b/1/2/3/file.tsx -> b/1/2/3/file.tsx)
239
+ // this is not a perfect "detect the root" solution, but should work in our CI/CD pipeline
240
+ const csvRows = [];
241
+ deprecatedSmallEffort.forEach((effort) => {
242
+ csvRows.push(generateCsvRowFromMatchedEffort(effort, filePath));
243
+ });
244
+ deprecatedMediumEffort.forEach((effort) => {
245
+ csvRows.push(generateCsvRowFromMatchedEffort(effort, filePath));
246
+ });
247
+ deprecatedLargeEffort.forEach((effort) => {
248
+ csvRows.push(generateCsvRowFromMatchedEffort(effort, filePath));
249
+ });
250
+
251
+ deprecatedDismissed.forEach((effort) => {
252
+ csvRows.push(generateCsvRowFromMatchedEffort(effort, filePath));
253
+ });
254
+
255
+ return csvRows;
256
+ };
@@ -3,6 +3,7 @@ import { checkPackagesInconsistencies } from './check-packages-inconsistencies/i
3
3
  import { checkMissingPackages } from './check-missing-packages/index.mjs';
4
4
  import { checkDeprecatedPackages } from './check-deprecated-packages/index.mjs';
5
5
  import { helpMigrateToV3 } from './help-migrate-to-v3/index.mjs';
6
+ import { deprecatedComponentsUsageReport } from './deprecated-components-usage-report/index.mjs';
6
7
  import { COMMANDS } from '../commands.mjs';
7
8
 
8
9
  export async function executeCommandsMap(args, options) {
@@ -25,6 +26,9 @@ export async function executeCommandsMap(args, options) {
25
26
  case COMMANDS.HELP_MIGRATE_TO_V3:
26
27
  helpMigrateToV3(options);
27
28
  break;
29
+ case COMMANDS.DEPRECATED_PACKAGES_USAGE_REPORT:
30
+ deprecatedComponentsUsageReport(options);
31
+ break;
28
32
  default:
29
33
  break;
30
34
  }
@@ -5,6 +5,7 @@ export const COMMANDS = {
5
5
  CHECK_PACKAGES_INCONSISTENCIES: 'check-packages-inconsistencies',
6
6
  CHECK_MISSING_PACKAGES: 'check-missing-packages',
7
7
  HELP_MIGRATE_TO_V3: 'help-migrate-to-v3',
8
+ DEPRECATED_PACKAGES_USAGE_REPORT: 'deprecated-components-usage-report',
8
9
  EXIT: 'exit',
9
10
  };
10
11
 
@@ -5,6 +5,7 @@ import { getPackagesInconsistenciesQuestions } from './command-arguments/promptC
5
5
  import { getDeprecatedPackagesQuestions } from './command-arguments/promptDeprecatedPackages.mjs';
6
6
  import { getHelpMigrateToV3Questions } from './command-arguments/promptHelpMigrateToV3.mjs';
7
7
  import { getMissingPackagesQuestions } from './command-arguments/promptMissingPackages.mjs';
8
+ import { getDeprecatedUsageReportQuestions } from './command-arguments/getDeprecatedUsageReportQuestions.mjs';
8
9
 
9
10
  async function promptForMissingScriptSpecificOptions({ originalOptions, promptOptions }) {
10
11
  const script = originalOptions.script || promptOptions.script;
@@ -26,6 +27,9 @@ async function promptForMissingScriptSpecificOptions({ originalOptions, promptOp
26
27
  case COMMANDS.HELP_MIGRATE_TO_V3:
27
28
  scriptQuestions.push(...getHelpMigrateToV3Questions(originalOptions));
28
29
  break;
30
+ case COMMANDS.DEPRECATED_PACKAGES_USAGE_REPORT:
31
+ scriptQuestions.push(...getDeprecatedUsageReportQuestions(originalOptions));
32
+ break;
29
33
  default:
30
34
  break;
31
35
  }
@@ -0,0 +1,332 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @typedef {object} SeverityTag
4
+ * @property {string} SeverityTag.name - the name of the tag
5
+ * @property {string} SeverityTag.severity - the severity of the tag
6
+ * @property {number} SeverityTag.severityInteger - the severity of the tag as an integer
7
+ * @property {string} SeverityTag.description - the description of the tag
8
+ *
9
+ * @typedef {object} DeprecatedPackageMatch
10
+ * @property {string} DeprecatedPackageMatch.component - the name of the LEGACY component
11
+ * @property {string} DeprecatedPackageMatch.oldPackage - the name of the LEGACY package
12
+ *
13
+ * @typedef {object} DeprecatedPackageWithEffortProperties
14
+ * @property {string} DeprecatedPackageWithEffortProperties.newComponent - the name of the NEW component
15
+ * @property {string} DeprecatedPackageWithEffortProperties.newPackage - the name of the NEW package
16
+ * @property {SeverityTag[]} DeprecatedPackageWithEffortProperties.tags - the tags associated with the component
17
+ *
18
+ * @typedef {DeprecatedPackageMatch & DeprecatedPackageWithEffortProperties} DeprecatedPackage
19
+ */
20
+
21
+ const SEVERITY = {
22
+ NON_STARTER: ['Failing to upgrade will make the project impossible to start/use', 1],
23
+ KNOWN_ISSUES: [
24
+ 'The component has unfixable known issues. Failing to upgrade means APP will have to live with those',
25
+ 2,
26
+ ],
27
+ FRAGILE_AND_UNMANTAINED: [
28
+ 'Failing to upgrade will make the project fragile and any error will have to be addressed by the APP team',
29
+ 3,
30
+ ],
31
+ UNMANTAINED: ['Failing to upgrade will mean that any error will have to be addressed by the APP team', 4],
32
+ };
33
+
34
+ const SOLUTIONS = {
35
+ REMOVED: 'the component has been regarded as supreflous and has hencheforth been removed in favor of examples',
36
+ NEW_VERSION_BREAKING: 'the component impedes further enhancments/bugfixes, a new version ex-novo has been created.',
37
+ };
38
+
39
+ export const DEPRECATED_PACKAGES = {
40
+ '@elliemae/ds-datagrids': {
41
+ name: 'data-grid',
42
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
43
+ severity: SEVERITY.KNOWN_ISSUES,
44
+ // TODO create relative confluence
45
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
46
+ },
47
+ '@elliemae/ds-date-picker': {
48
+ name: 'date-picker',
49
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
50
+ severity: SEVERITY.KNOWN_ISSUES,
51
+ // TODO create relative confluence
52
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
53
+ },
54
+ '@elliemae/ds-date-range-picker': {
55
+ name: 'date-range-picker',
56
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
57
+ severity: SEVERITY.KNOWN_ISSUES,
58
+ // TODO create relative confluence
59
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
60
+ },
61
+ '@elliemae/ds-date-time-picker': {
62
+ name: 'date-time-picker',
63
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
64
+ severity: SEVERITY.KNOWN_ISSUES,
65
+ // TODO create relative confluence
66
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
67
+ },
68
+ '@elliemae/ds-time-picker': {
69
+ name: 'time-picker',
70
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
71
+ severity: SEVERITY.KNOWN_ISSUES,
72
+ // TODO create relative confluence
73
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
74
+ },
75
+ '@elliemae/ds-button-group': {
76
+ name: 'button-group',
77
+ solution: SOLUTIONS.REMOVED,
78
+ severity: SEVERITY.NON_STARTER,
79
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Button+Group+-+Dimsum+2.x',
80
+ },
81
+ '@elliemae/ds-card-array': {
82
+ name: 'card-array',
83
+ solution: SOLUTIONS.REMOVED,
84
+ severity: SEVERITY.NON_STARTER,
85
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Card+Array+-+Dimsum+2.x',
86
+ },
87
+ '@elliemae/ds-form': {
88
+ subComponents: [
89
+ {
90
+ name: 'combobox',
91
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
92
+ severity: SEVERITY.FRAGILE_AND_UNMANTAINED,
93
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Combo-box+-+Dimsum+2.x',
94
+ },
95
+ ],
96
+ },
97
+ '@elliemae/ds-combobox-v1': {
98
+ subComponents: [
99
+ {
100
+ name: 'combobox',
101
+ solution: SOLUTIONS.NEW_VERSION_BREAKING,
102
+ severity: SEVERITY.FRAGILE_AND_UNMANTAINED,
103
+ confluence: 'https://confluence.elliemae.io/display/FEAE/Combo-box+-+Dimsum+2.x',
104
+ },
105
+ ],
106
+ },
107
+ };
108
+
109
+ /*********************************************************************************************
110
+ Those constant would be extremely hard to read if we "prettify" them with project rules...
111
+ I'm making a conscious decision to disable prettier and auto-formatting for this section of the code.
112
+ **********************************************************************************************/
113
+ /* eslint-disable max-len, prettier/prettier */
114
+
115
+ /**
116
+ * @type {object<string, SeverityTag>}
117
+ * @property {SeverityTag} SEVERITY_TAGS.A11Y_LIMITATIONS
118
+ * @property {SeverityTag} SEVERITY_TAGS.TOOLING_BLOCKER
119
+ * @property {SeverityTag} SEVERITY_TAGS.UNMANTAINED
120
+ * @property {SeverityTag} SEVERITY_TAGS.FRAGILE
121
+ * @property {SeverityTag} SEVERITY_TAGS.KNOWN_ISSUES
122
+ */
123
+ /* prettier-ignore */
124
+ const SEVERITY_TAGS = {
125
+ // we don't expect to have any NON_STARTER, if we eventually do, we have to evaluate them manually
126
+ // { name: 'NON_STARTER', severity: "P1" },
127
+ A11Y_LIMITATIONS:{
128
+ name: 'A11Y_LIMITATIONS', severity: 'P2', severityInteger: 2,
129
+ description: 'The accessibility requirements are only available to the alternative solution' },
130
+
131
+ TOOLING_BLOCKER:{
132
+ name: 'TOOLING_BLOCKER', severity: 'P2', severityInteger: 2,
133
+ description:
134
+ 'The component is based in third-party-packages that makes changing tools like jest, styled-compoents, etc... impossible' },
135
+
136
+ UNMANTAINED:{ name: 'UNMANTAINED', severity: 'P3', severityInteger: 3,
137
+ description:
138
+ 'The component company-wise is low priority and has not received any meaningful attention in more than 8 months' },
139
+
140
+ FRAGILE:{ name: 'FRAGILE', severity: 'P3', severityInteger: 3,
141
+ description: 'The component is extremely fragile and not extensible' },
142
+
143
+ KNOWN_ISSUES:{ name: 'KNOWN_ISSUES', severity: 'P3', severityInteger: 3,
144
+ description: 'The component has known issues that cannot be fixed' },
145
+ };
146
+
147
+ /* prettier-ignore */
148
+ /**
149
+ * @type {DeprecatedPackage[]}
150
+ */
151
+ export const LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT = [
152
+ { component: 'DSModal', oldPackage: '@elliemae/ds-modal',
153
+ newComponent: 'DSDialog', newPackage: '@elliemae/ds-dialog',
154
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
155
+ },
156
+
157
+ { component: 'DSButton', oldPackage: '@elliemae/ds-button',
158
+ newComponent: 'DSButtonV3', newPackage: '@elliemae/ds-button',
159
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
160
+ },
161
+
162
+ { component: 'DSCircularProgressIndicator', oldPackage: '@elliemae/ds-circular-progress-indicator',
163
+ newComponent: 'DSCircularIndeterminateIndicator', newPackage: '@elliemae/ds-circular-progress-indicator',
164
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
165
+ },
166
+
167
+ { component: 'DSButtonV2', oldPackage: '@elliemae/ds-button',
168
+ newComponent: 'DSButtonV3', newPackage: '@elliemae/ds-button',
169
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
170
+ },
171
+
172
+ { component: 'DSTooltipV2', oldPackage: '@elliemae/ds-tooltip',
173
+ newComponent: 'DSTooltipV3', newPackage: '@elliemae/ds-tooltip',
174
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
175
+ },
176
+
177
+ { component: 'DSSeparator', oldPackage: '@elliemae/ds-separator',
178
+ newComponent: 'DSSeparatorV2', newPackage: '@elliemae/ds-separator',
179
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
180
+ },
181
+
182
+ { component: 'DSFormItemLayout', oldPackage: '@elliemae/ds-form',
183
+ newComponent: 'DSFormLayoutBlockItem', newPackage: '@elliemae/ds-form-layout-blocks',
184
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
185
+ },
186
+
187
+ { component: 'DSTooltip', oldPackage: '@elliemae/ds-tooltip',
188
+ newComponent: 'DSTooltipV3', newPackage: '@elliemae/ds-tooltip',
189
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
190
+ },
191
+
192
+ { component: 'DSToggle', oldPackage: '@elliemae/ds-toggle',
193
+ newComponent: 'DSControlledToggle', newPackage: '@elliemae/ds-controlled-form',
194
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
195
+ },
196
+
197
+ { component: 'DSDateInput', oldPackage: '@elliemae/ds-form',
198
+ newComponent: 'DSControlledDateTimePicker', newPackage: '@elliemae/ds-controlled-form',
199
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
200
+ },
201
+
202
+ { component: 'DSDateInputV2', oldPackage: '@elliemae/ds-form',
203
+ newComponent: 'DSControlledDateTimePicker', newPackage: '@elliemae/ds-controlled-form',
204
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
205
+ },
206
+
207
+ { component: 'DSComboBox2', oldPackage: '@elliemae/ds-form',
208
+ newComponent: 'DSComboBoxV3', newPackage: '@elliemae/ds-controlled-form',
209
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
210
+ },
211
+
212
+ { component: 'DSComboBoxV1', oldPackage: '@elliemae/ds-form',
213
+ newComponent: 'DSComboBoxV3', newPackage: '@elliemae/ds-controlled-form',
214
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
215
+ },
216
+
217
+ { component: 'DSComboBox', oldPackage: '@elliemae/ds-form',
218
+ newComponent: 'DSComboBoxV3', newPackage: '@elliemae/ds-controlled-form',
219
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
220
+ },
221
+
222
+ { component: 'DSPill', oldPackage: '@elliemae/ds-pills',
223
+ newComponent: 'DSPillV2', newPackage: '@elliemae/ds-pills-v2',
224
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
225
+ },
226
+
227
+ { component: 'DSCheckbox', oldPackage: '@elliemae/ds-form',
228
+ newComponent: 'DSControlledCheckbox', newPackage: '@elliemae/ds-controlled-form',
229
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
230
+ },
231
+
232
+ { component: 'DSInputGroup', oldPackage: '@elliemae/ds-form',
233
+ newComponent: 'DSInputGroup', newPackage: '@elliemae/ds-controlled-form',
234
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
235
+ },
236
+
237
+ { component: 'DSRadio', oldPackage: '@elliemae/ds-form',
238
+ newComponent: 'DSControlledRadio', newPackage: '@elliemae/ds-controlled-form',
239
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
240
+ },
241
+
242
+ { component: 'DSTextBox', oldPackage: '@elliemae/ds-form',
243
+ newComponent: 'DSInputText', newPackage: '@elliemae/ds-controlled-form',
244
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
245
+ },
246
+
247
+ { component: 'DSLargeInputText', oldPackage: '@elliemae/ds-form',
248
+ newComponent: 'DSControlledLargeTextInput', newPackage: '@elliemae/ds-controlled-form',
249
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
250
+ },
251
+ ];
252
+ /**
253
+ * @type {DeprecatedPackage[]}
254
+ */
255
+ /* prettier-ignore */
256
+ export const LEGACY_WITH_NEW_MEDIUM_MIGRATION_EFFORT = [
257
+ { component: 'DSDropdownMenu', oldPackage: '@elliemae/ds-dropdownmenu',
258
+ newComponent: 'DSDropdownMenuV2', newPackage: '@elliemae/ds-dropdown-menu-v2',
259
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
260
+ },
261
+
262
+ { component: 'Minitoolbar', oldPackage: '@elliemae/ds-mini-toolbar',
263
+ newComponent: 'DSToolbarV2', newPackage: '@elliemae/ds-toolbar-v2',
264
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
265
+ },
266
+
267
+ { component: 'DSToolbar', oldPackage: '@elliemae/ds-toolbar',
268
+ newComponent: 'DSToolbarV2', newPackage: '@elliemae/ds-toolbar',
269
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
270
+ },
271
+
272
+ { component: 'DSDatePicker', oldPackage: '@elliemae/ds-date-picker',
273
+ newComponent: 'DSControlledDateTimePicker', newPackage: '@elliemae/ds-controlled-form',
274
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
275
+ },
276
+
277
+ { component: 'DSDateRangePicker', oldPackage: '@elliemae/ds-date-range-picker',
278
+ newComponent: 'DSControlledDateRangePicker', newPackage: '@elliemae/ds-controlled-form',
279
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
280
+ },
281
+
282
+ { component: 'DSTimeInput', oldPackage: '@elliemae/ds-form',
283
+ newComponent: 'DSControlledDateTimePicker', newPackage: '@elliemae/ds-controlled-form',
284
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
285
+ },
286
+
287
+ { component: 'DSTimePicker', oldPackage: '@elliemae/ds-time-picker',
288
+ newComponent: 'DSControlledDateTimePicker', newPackage: '@elliemae/ds-controlled-form',
289
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
290
+ },
291
+
292
+ { component: 'DSInputMask', oldPackage: '@elliemae/ds-form',
293
+ newComponent: 'DSInputText', newPackage: '@elliemae/ds-controlled-form',
294
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
295
+ },
296
+ ]
297
+
298
+ /* prettier-ignore */
299
+ export const LEGACY_WITH_NEW_LARGE_MIGRATION_EFFORT = [
300
+ { component: 'DSShuttle', oldPackage: '@elliemae/ds-shuttle',
301
+ newComponent: 'DSShuttleV2', newPackage: '@elliemae/ds-shuttle-v2',
302
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
303
+ },
304
+
305
+ { component: 'DataGrid', oldPackage: '@elliemae/ds-data-grid',
306
+ newComponent: 'DataTable', newPackage: '@elliemae/ds-data-table',
307
+ tags:[ SEVERITY_TAGS.A11Y_LIMITATIONS ],
308
+ },
309
+ ];
310
+
311
+ // all of those are considered LOW effort
312
+ /**
313
+ * @type {DeprecatedPackageMatch[]}
314
+ */
315
+ /* prettier-ignore */
316
+ export const DISMISSED_WITH_EXAMPLE = [
317
+ { component: 'DSButtonGroup', oldPackage: 'ds-button-group' },
318
+ { component: 'DSCardArray', oldPackage: 'ds-card-array' },
319
+ { component: 'DSLabelValue', oldPackage: 'ds-label-value' },
320
+ { component: 'DSDateRangeSelector', oldPackage: 'ds-date-range-selector' },
321
+ { component: 'DSInputProtected', oldPackage: 'ds-form' },
322
+ { component: 'DSGroupBox', oldPackage: 'ds-group-box' },
323
+ { component: 'DSFilterBar', oldPackage: 'ds-filterbar' },
324
+ { component: 'DSDateTimeRecurrenceSelector', oldPackage: 'ds-date-time-recurrence-picker' },
325
+ { component: 'DSNumberRangeField', oldPackage: 'ds-number-range-field' },
326
+ { component: 'DSPageNumber', oldPackage: 'ds-page-number' },
327
+ { component: 'DSSearchField', oldPackage: 'ds-search-field' },
328
+ ];
329
+ /* eslint-enable max-len, prettier/prettier */
330
+ /*********************************************************************************************
331
+ Formatting is back on :)
332
+ **********************************************************************************************/
@@ -0,0 +1,7 @@
1
+ export {
2
+ DEPRECATED_PACKAGES,
3
+ LEGACY_WITH_NEW_SMALL_MIGRATION_EFFORT,
4
+ LEGACY_WITH_NEW_MEDIUM_MIGRATION_EFFORT,
5
+ LEGACY_WITH_NEW_LARGE_MIGRATION_EFFORT,
6
+ DISMISSED_WITH_EXAMPLE,
7
+ } from './deprecatedPackages.mjs';
@@ -0,0 +1,83 @@
1
+ # Regarding file matching
2
+
3
+ ## Support for Gitignore
4
+
5
+ ### pre-analysis
6
+
7
+ based on CLI `man(ual)` git ignore : `man 5 gitignore`
8
+
9
+ ```
10
+ GITIGNORE(5) Git Manual GITIGNORE(5)
11
+
12
+ NAME
13
+ gitignore - Specifies intentionally untracked files to ignore
14
+ ```
15
+
16
+ specifically:
17
+
18
+ ```
19
+
20
+ DESCRIPTION
21
+ [...]
22
+ Each line in a gitignore file specifies a pattern.
23
+ [...]
24
+
25
+ PATTERN FORMAT
26
+ [...]
27
+ See fnmatch(3) and the FNM_PATHNAME flag for a more detailed description.
28
+ ```
29
+
30
+ the algorithm to support `.gitignore` syntaxes for filtering we need requires the following charateristics:
31
+
32
+ - multiple patterns match
33
+ - `fnmatch` patterns support
34
+
35
+ ### Gathering the list of files
36
+
37
+ Ideally speaking we want to find a solution that would support the generation of the filepaths list based on the "gitignore" in one go.
38
+
39
+ The standard filesystem API from nodejs 20 is not able to support glob patterns as of today, it's merely able to generate a list of every single file-path recursively.
40
+
41
+ #### How does other do it?
42
+
43
+ ##### Glob - how does it generate the file list?
44
+
45
+ based on my understanding, the creator of `Glob` has also developed a dedicated file-system optimization library he uses in the Glob package, which is [path-scurry](https://www.npmjs.com/package/path-scurry#:~:text=For%20resolving,resolution).
46
+
47
+ > For resolving string paths, PathScurry ranges from 5-50 times faster than path.resolve on repeated resolutions, but around 100 to 1000 times slower on the first resolution.
48
+ > [...]
49
+ > With default settings on a folder tree of 100,000 items, consisting of around a 10-to-1 ratio of normal files to directories, PathScurry performs comparably to @nodelib/fs.walk, which is the fastest and most reliable file system walker I could find.
50
+ > As far as I can tell, it's almost impossible to go much faster in a Node.js program, just based on how fast you can push syscalls out to the fs thread pool.
51
+
52
+ I understand that under the hood `Glob` is NOT applying the matching pattern at the "matching" step itself, it's instead generating a list of all the files and later on filtering it down with `regexp` string matching.
53
+
54
+ I also understand that the optimization path-scurry are providing to the file-list generation are not necessarly beneficial to our scenario and are making a trade-off of memory in favory of execution time, which in CI & CD environments may not be desiderable due to the reduced RAM of the environments, we usually prefeer to have the pipeline run 2x longer but requiring 1/2 the RAM resources rather then taking 1/2 the time but reserving 2x the RAM.
55
+
56
+ [from scurry docs](https://www.npmjs.com/package/path-scurry#:~:text=PathScurry%20makes,can)
57
+
58
+ > PathScurry makes heavy use of LRUCache to efficiently cache whatever it can, and Path objects remain in the graph for the lifetime of the walker
59
+
60
+ ##### [Glob](https://www.npmjs.com/package/glob#:~:text=testing,characters) - gitignore support
61
+
62
+ based on my current understanding, the node package `glob` is not specifically about supporting all the possible syntaxes that are meant to be supported by the gitignore patterns:
63
+
64
+ > testing, fast-glob is around 10-20% faster than this module when walking over 200k files nested 4 directories deep 1.
65
+ > However, there are some inconsistencies with Bash matching behavior that this module does not suffer from
66
+
67
+ and it's instead trying to create a bash compliant list of files paths based on bash patterns.
68
+
69
+ for this reason to ensure 1-1 `Glob` matching would require still a further pattern matching on the generated list and I understand we would not necessarly benefit from the trades-off glob is making under the hood.
70
+
71
+ #### What I will do
72
+
73
+ ##### step 1 - generate the file-list via native fs readDir
74
+
75
+ I will use the native node implementation to ensure maximum compatibility and to not encur the risk that the glob abstraction may incurr due to the heavy use of the LRUCache, relying on node's own performance optimization. I think that if any enhancement on the performance/memory is required it would be easier to manage if we don't deal with Glob out-of-the-box optimization and start from the bottom-line
76
+
77
+ ##### step 2 - further filtering - gitignore compliant
78
+
79
+ I have found out https://github.com/micromatch/micromatch
80
+
81
+ which seems to be used by `square`, `webpack`, `babel core`, `yarn`, `jest`, `taro`, `bulma`, `browser-sync`, `documentation.js`, `stylelint`, `nyc`, `ava`
82
+
83
+ and seems to be matching our requirements.
@@ -0,0 +1,58 @@
1
+ // -----------------------------------------------------------------------------
2
+ // Debug helpers for ram issues
3
+ // -----------------------------------------------------------------------------
4
+ /**
5
+ * a helper function to format memory usage data into a human readable format
6
+ * @param {number} data - memory usage data
7
+ * @returns {string} - formatted memory usage data in MB
8
+ */
9
+ const formatMemoryUsage = (data) => `${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
10
+
11
+ /**
12
+ * logs the current memory usage
13
+ * @param {object} memoryData - memory usage data - process.memoryUsage()
14
+ * @param {string} [prefix=''] - prefix for the log
15
+ * @returns {void}
16
+ */
17
+ export const logCurrMemoryUsage = (memoryData, prefix = '') => {
18
+ const memoryUsage = {
19
+ // 'process total allocated memory': `${formatMemoryUsage(memoryData.rss)}`, // not relevant?
20
+ 'heap total size': `${formatMemoryUsage(memoryData.heapTotal)}`,
21
+ 'current heap memory used': `${formatMemoryUsage(memoryData.heapUsed)}`,
22
+ // 'v8 external memory': `${formatMemoryUsage(memoryData.external)}`, // not relevant?
23
+ };
24
+ console.log('************************************************************************');
25
+ console.log(prefix, memoryUsage);
26
+ console.log('************************************************************************');
27
+ };
28
+
29
+ /**
30
+ * logs the difference between two memory usage data objects
31
+ * @param {object} memoryDataBefore - memory usage data - process.memoryUsage()
32
+ * @param {object} memoryDataAfter - memory usage data - process.memoryUsage()
33
+ * @param {string} [prefix=''] - prefix for the log
34
+ * @returns {void}
35
+ */
36
+ export const logMemoryUsageDiff = (memoryDataBefore, memoryDataAfter, prefix = '') => {
37
+ // const diffRSS = memoryDataAfter.rss - memoryDataBefore.rss;
38
+ const diffHeapTotal = memoryDataAfter.heapTotal - memoryDataBefore.heapTotal;
39
+ const diffHeapUsed = memoryDataAfter.heapUsed - memoryDataBefore.heapUsed;
40
+ // const diffExternal = memoryDataAfter.external - memoryDataBefore.external;
41
+ const diffData = {
42
+ // 'process total allocated memory': `${formatMemoryUsage(memoryDataBefore.rss)} ${
43
+ // diffRSS > 0 ? '+' : ''
44
+ // }${formatMemoryUsage(diffRSS)}`, // not relevant?
45
+ 'heap total size': `${formatMemoryUsage(memoryDataBefore.heapTotal)} ${
46
+ diffHeapTotal > 0 ? '+' : ''
47
+ }${formatMemoryUsage(diffHeapTotal)}`,
48
+ 'current heap memory used': `${formatMemoryUsage(memoryDataBefore.heapUsed)} ${
49
+ diffHeapUsed > 0 ? '+' : ''
50
+ }${formatMemoryUsage(diffHeapUsed)}`,
51
+ // 'v8 external memory': `${formatMemoryUsage(memoryDataBefore.external)} ${
52
+ // diffExternal > 0 ? '+' : ''
53
+ // }${formatMemoryUsage(diffExternal)}`, // not relevant?
54
+ };
55
+ console.log('************************************************************************');
56
+ console.log(prefix, diffData);
57
+ console.log('************************************************************************');
58
+ };
@@ -0,0 +1,198 @@
1
+ /* eslint-disable max-lines */
2
+ /* eslint-disable indent */
3
+ import path from 'path';
4
+ import { readdirSync, readFileSync } from 'fs';
5
+ import { logCurrMemoryUsage, logMemoryUsageDiff } from './debugHelpers.mjs';
6
+ import micromatch from 'micromatch'; // used to filter out files that match the git ignore patterns
7
+
8
+ /********************************************************************
9
+ * ******* GENERIC STRING HELPERS COMMON TO PATHS & FILENAMES *******
10
+ ********************************************************************/
11
+
12
+ /**
13
+ * convert windows paths to POSIX (glob compatible) slashes ( \ -- to --> /).
14
+ *
15
+ * @param {string} pathString the path to convert
16
+ * @returns {string} path with glob compatible POSIX slashes ( \ -- to --> /).
17
+ */
18
+ const generatePosixPath = (pathString) => pathString.replace(/\\/g, '/');
19
+
20
+ /**
21
+ * prepend process.cwd() to a string and convert to POSIX (glob compatible) slashes ( \ -- to --> /).
22
+ *
23
+ * @param {string} stringToPrependTo the string to prepend the CWD and make glob compatible
24
+ * @returns {string} paths with prepent CWD and glob compatible POSIX slashes ( \ -- to --> /).
25
+ */
26
+ export const generatePathFromCurrentFolder = (...stringToPrependTo) =>
27
+ generatePosixPath(path.join(process.cwd(), ...stringToPrependTo));
28
+
29
+ /********************************************************************
30
+ *** DIRECTORY RECURSIONS HELPERS - NO IGNORING DURING ITERATION ****
31
+ ********************************************************************/
32
+ /**
33
+ * generates a list of files paths from a starting path iterating dirs recursively -
34
+ * in node 20 this is potentially really slow per se,
35
+ * we are implementing a micro-optimization by implementing it internally.
36
+ *
37
+ * This should not be required if
38
+ * - node 20 fixes the performance issue with `readdirSync` -> recursive: true.
39
+ * - we only need to support node 20+.
40
+ *
41
+ * withFileTypes: true - to avoid the extra `stat` system call.
42
+ *
43
+ * This function is also converting the paths to POSIX (glob compatible) internally, no need for windows specific code.
44
+ * @param {string} startingPath - path to start generating the list of files from
45
+ * @returns {string[]} - list of files paths
46
+ * @see https://nodejs.org/api/fs.html#fsreaddirsyncpath-options
47
+ * @see https://stackoverflow.com/a/76632197/1689370
48
+ */
49
+ const getFilesMicroOptimized = (dir) => {
50
+ let dirsToProcess = [dir];
51
+ const outputFiles = [];
52
+ while (dirsToProcess.length > 0) {
53
+ const dir = dirsToProcess.pop();
54
+
55
+ const fileList = readdirSync(dir, { withFileTypes: true });
56
+ fileList.forEach((file) => {
57
+ const filePath = generatePosixPath(`${dir}/${file.name}`);
58
+ if (file.isDirectory()) {
59
+ dirsToProcess.push(filePath);
60
+ } else {
61
+ outputFiles.push(filePath);
62
+ }
63
+ });
64
+ }
65
+ return outputFiles;
66
+ };
67
+
68
+ /**
69
+ * generates a list of files paths from a starting path recursively -
70
+ * in node 20 this is potentially really slow per se, we are implementing a micro-optimization:
71
+ * we invoke the function recursively only on directories as a replacement for the `recursive` option of `readdirSync`
72
+ * and additionally we use `withFileTypes` option to avoid the extra `stat` system call.
73
+ *
74
+ * the way this is implemented we can use it on node < 20 as well.
75
+ *
76
+ * This function is also converting the paths to POSIX (glob compatible) internally, no need for windows specific code.
77
+ * @param {string} startingPath - path to start generating the list of files from
78
+ * @returns {string[]} - list of files paths
79
+ * @see https://nodejs.org/api/fs.html#fsreaddirsyncpath-options
80
+ * @see https://stackoverflow.com/a/76632197/1689370
81
+ */
82
+ export const fullFilesPaths = (startingPath, debug = false) => {
83
+ let startMemoryUsage;
84
+ let endMemoryUsage;
85
+ let startTime;
86
+ if (debug) {
87
+ startTime = performance.now();
88
+ startMemoryUsage = process.memoryUsage();
89
+ logCurrMemoryUsage(startMemoryUsage, 'before recursion start');
90
+ }
91
+
92
+ // this is split into an helper function to make it explicit that
93
+ // we know about the `recursive` option of `readdirSync` and we are not using it on purpose.
94
+ // required if running on node < 20
95
+ // because of the lack of the `recursive` option of `readdirSync` in those versions.
96
+ const files = getFilesMicroOptimized(generatePathFromCurrentFolder(startingPath));
97
+
98
+ if (debug) {
99
+ endMemoryUsage = process.memoryUsage();
100
+ logMemoryUsageDiff(startMemoryUsage, endMemoryUsage, 'diff after recursion end');
101
+ // eslint-disable-next-line no-console
102
+ console.log('Estimated execution time: ', performance.now() - startTime);
103
+ // eslint-disable-next-line no-console
104
+ console.log('List of file entries generated:', files.length);
105
+ }
106
+ return files;
107
+ };
108
+
109
+ /********************************************************************
110
+ * DIRECTORY RECURSIONS HELPERS - IGNORING PATTERNS WHILE ITERATING *
111
+ this is not used, but keeping it around for reference,
112
+ as of 04/december/2023 this is slower than filtering at the end.
113
+ ********************************************************************/
114
+
115
+ // Required if running on node < 20 becuase of the lack of the `recursive` option of `readdirSync` in those versions.
116
+ // const getFilesMicroOptimizedWithIgnore = (startingPath, gitPatterns) => {
117
+ // let dirsToProcess = [generatePathFromCurrentFolder(startingPath)];
118
+ // const outputFiles = [];
119
+ // // the iterative approach avoids stack overflow errors
120
+ // // allowing us to invoke the filtering function while iterating the directories
121
+ // while (dirsToProcess.length > 0) {
122
+ // const dir = dirsToProcess.pop();
123
+ // // we filter out the ignored files and directories here
124
+ // const fileList = readdirSync(dir, { withFileTypes: true }).filter(
125
+ // (file) => !micromatch.isMatch(generatePosixPath(`${dir}/${file.name}`), gitPatterns, { contains: true }),
126
+ // );
127
+ // fileList.forEach((file) => {
128
+ // const filePath = generatePosixPath(`${dir}/${file.name}`);
129
+ // if (file.isDirectory()) {
130
+ // dirsToProcess.push(filePath);
131
+ // } else {
132
+ // outputFiles.push(filePath);
133
+ // }
134
+ // });
135
+ // }
136
+ // return outputFiles;
137
+ // };
138
+ // const fullFilesPathsWithIgnore = (startingPath, gitPatterns, debug = false) => {
139
+ // let startMemoryUsage;
140
+ // let endMemoryUsage;
141
+ // let startTime;
142
+ // if (debug) {
143
+ // startTime = performance.now();
144
+ // startMemoryUsage = process.memoryUsage();
145
+ // logCurrMemoryUsage(startMemoryUsage, 'before recursion start');
146
+ // }
147
+ // // this is split into an helper function to make it explicit that
148
+ // // we know about the `recursive` option of `readdirSync` and we are not using it on purpose.
149
+ // const files = getFilesMicroOptimizedWithIgnore(startingPath, gitPatterns, {
150
+ // debug,
151
+ // startMemoryUsage,
152
+ // });
153
+
154
+ // if (debug) {
155
+ // endMemoryUsage = process.memoryUsage();
156
+ // logMemoryUsageDiff(startMemoryUsage, endMemoryUsage, 'diff after recursion end');
157
+ // console.log('Estimated execution time: ', performance.now() - startTime);
158
+ // console.log('List of file entries generated:', files.length);
159
+ // }
160
+ // return files;
161
+ // };
162
+
163
+ /**
164
+ * generates a list of files paths from a starting path recursively,
165
+ * filtering out files that match the git ignore patterns
166
+ * @param {object} options - options
167
+ * @param {string} options.startingDirPath - path to start generating the list of files from
168
+ * @param {string} options.gitIgnorePath - path to the .gitignore file
169
+ * @param {boolean} options.debug - debug flag
170
+ * @param {string} options.fileExtensions - comma separated list of file extensions to filter by
171
+ * @returns {string[]} - list of files paths
172
+ *
173
+ **/
174
+ export const filePathsWithGitIgnore = (options) => {
175
+ const { startingDirPath, gitIgnorePath, debug = false, fileExtensions = '.js,.jsx,.ts,.tsx,.mjs' } = options;
176
+ // we generate a list of patterns to ignore from the .gitignore file.
177
+ const gitIgnoreFileContent = readFileSync(generatePathFromCurrentFolder(gitIgnorePath), 'utf8');
178
+ const gitIgnoreFileContentLines = gitIgnoreFileContent.split(/\r?\n/);
179
+ // remove empty and commented lines
180
+ const gitPatterns = gitIgnoreFileContentLines.filter((line) => line !== '' && !line.startsWith('#'));
181
+
182
+ // console.log('--------------- ----------------- filtering as we go:');
183
+ // const files = fullFilesPathsWithIgnore(startingDirPath, gitPatterns, debug);
184
+ // console.log(files.length);
185
+
186
+ // Based on my testing, filtering at the end results in way better performance than filtering as we go...
187
+ // console.log('--------------- ----------------- filtering at the end:');
188
+ const nonGitIgnoredFiles = fullFilesPaths(startingDirPath, debug).filter(
189
+ (file) => !micromatch.isMatch(file, gitPatterns),
190
+ );
191
+ // we only want files matching the file extensions
192
+ const files = nonGitIgnoredFiles.filter((filePath) => {
193
+ const dotlessExtension = filePath.split('.').pop();
194
+ return fileExtensions.split(',').some((fileExtension) => fileExtension === `.${dotlessExtension}`);
195
+ });
196
+
197
+ return files;
198
+ };
@@ -1,5 +1,5 @@
1
- export * from './generatePathFromCurrentFolder.mjs';
2
- export * from './globArray.mjs';
3
- export * from './replaceFromMap.mjs';
4
- export * from './matchHelpers.mjs';
5
- export * from './getLatestDimsumVersion.mjs';
1
+ export { generatePathFromCurrentFolder, fullFilesPaths } from './filepathsMatchers.mjs';
2
+ export { globArray } from './globArray.mjs';
3
+ export { replaceFromMap } from './replaceFromMap.mjs';
4
+ export { getVersions, getMatchVersions, getHighestMatchVersion } from './matchHelpers.mjs';
5
+ export { getLatestDimsumVersion } from './getLatestDimsumVersion.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elliemae/ds-codemods",
3
- "version": "3.26.0-next.6",
3
+ "version": "3.26.0-next.8",
4
4
  "license": "MIT",
5
5
  "description": "ICE MT - Dimsum - Code Mods",
6
6
  "files": [
@@ -1,87 +0,0 @@
1
- export const SEVERITY = {
2
- NON_STARTER: ['Failing to upgrade will make the project impossible to start/use', 1],
3
- KNOWN_ISSUES: [
4
- 'The component has unfixable known issues. Failing to upgrade means APP will have to live with those',
5
- 2,
6
- ],
7
- FRAGILE_AND_UNMANTAINED: [
8
- 'Failing to upgrade will make the project fragile and any error will have to be addressed by the APP team',
9
- 3,
10
- ],
11
- UNMANTAINED: ['Failing to upgrade will mean that any error will have to be addressed by the APP team', 4],
12
- };
13
-
14
- export const SOLUTIONS = {
15
- REMOVED: 'the component has been regarded as supreflous and has hencheforth been removed in favor of examples',
16
- NEW_VERSION_BREAKING: 'the component impedes further enhancments/bugfixes, a new version ex-novo has been created.',
17
- };
18
-
19
- export const DEPRECATED_PACKAGES = {
20
- '@elliemae/ds-datagrids': {
21
- name: 'data-grid',
22
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
23
- severity: SEVERITY.KNOWN_ISSUES,
24
- // TODO create relative confluence
25
- confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
26
- },
27
- '@elliemae/ds-date-picker': {
28
- name: 'date-picker',
29
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
30
- severity: SEVERITY.KNOWN_ISSUES,
31
- // TODO create relative confluence
32
- confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
33
- },
34
- '@elliemae/ds-date-range-picker': {
35
- name: 'date-range-picker',
36
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
37
- severity: SEVERITY.KNOWN_ISSUES,
38
- // TODO create relative confluence
39
- confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
40
- },
41
- '@elliemae/ds-date-time-picker': {
42
- name: 'date-time-picker',
43
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
44
- severity: SEVERITY.KNOWN_ISSUES,
45
- // TODO create relative confluence
46
- confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
47
- },
48
- '@elliemae/ds-time-picker': {
49
- name: 'time-picker',
50
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
51
- severity: SEVERITY.KNOWN_ISSUES,
52
- // TODO create relative confluence
53
- confluence: 'https://confluence.elliemae.io/display/FEAE/Breaking+changes+-+Migration+steps+and+rationale',
54
- },
55
- '@elliemae/ds-button-group': {
56
- name: 'button-group',
57
- solution: SOLUTIONS.REMOVED,
58
- severity: SEVERITY.NON_STARTER,
59
- confluence: 'https://confluence.elliemae.io/display/FEAE/Button+Group+-+Dimsum+2.x',
60
- },
61
- '@elliemae/ds-card-array': {
62
- name: 'card-array',
63
- solution: SOLUTIONS.REMOVED,
64
- severity: SEVERITY.NON_STARTER,
65
- confluence: 'https://confluence.elliemae.io/display/FEAE/Card+Array+-+Dimsum+2.x',
66
- },
67
- '@elliemae/ds-form': {
68
- subComponents: [
69
- {
70
- name: 'combobox',
71
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
72
- severity: SEVERITY.FRAGILE_AND_UNMANTAINED,
73
- confluence: 'https://confluence.elliemae.io/display/FEAE/Combo-box+-+Dimsum+2.x',
74
- },
75
- ],
76
- },
77
- '@elliemae/ds-combobox-v1': {
78
- subComponents: [
79
- {
80
- name: 'combobox',
81
- solution: SOLUTIONS.NEW_VERSION_BREAKING,
82
- severity: SEVERITY.FRAGILE_AND_UNMANTAINED,
83
- confluence: 'https://confluence.elliemae.io/display/FEAE/Combo-box+-+Dimsum+2.x',
84
- },
85
- ],
86
- },
87
- };
@@ -1,11 +0,0 @@
1
- import path from 'path';
2
- /**
3
- * prepend process.cwd() to a string and convert to POSIX (glob compatible) slashes ( \ -- to --> /).
4
- *
5
- * @param {string} stringToPrependTo the string to prepend the CWD and make glob compatible
6
- * @returns {string} paths with prepent CWD and glob compatible POSIX slashes ( \ -- to --> /).
7
- */
8
- export const generatePathFromCurrentFolder = (...stringToPrependTo) =>
9
- path.join(process.cwd(), ...stringToPrependTo).replace(/\\/g, '/');
10
-
11
- export default generatePathFromCurrentFolder;