@elliemae/ds-monorepo-devops 3.50.1-next.8 → 3.51.0-beta.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.
package/bin/cli.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import arg from 'arg';
2
+ import { checkCommandOrInquire, executeCommandsMap } from './execute-commands-map.mjs';
3
+
4
+ function parseArgumentsIntoOptions(rawArgs) {
5
+ const args = arg(
6
+ {},
7
+ {
8
+ argv: rawArgs.slice(2),
9
+ },
10
+ );
11
+ return {
12
+ command: args._[0],
13
+ };
14
+ }
15
+
16
+ export async function cli(args) {
17
+ let options = parseArgumentsIntoOptions(args);
18
+ options = await checkCommandOrInquire(options);
19
+ executeCommandsMap(args, options);
20
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { cli } from './cli.mjs';
3
+
4
+ cli(process.argv);
@@ -0,0 +1,210 @@
1
+ /* eslint-disable max-lines */
2
+ /* eslint-disable max-len */
3
+ /* eslint-disable complexity, no-console, max-statements */
4
+ import defGlob from 'glob';
5
+ import inquirer from 'inquirer';
6
+ import { exec } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ const { glob } = defGlob;
9
+
10
+ const execSyntaxSugar = async ({ command, dryRun = false, silentFail = false }) => {
11
+ const noLFNorCRLFCmd = command.replace(/\r?\n|\r/g, '');
12
+ if (dryRun) {
13
+ console.log(noLFNorCRLFCmd);
14
+ return;
15
+ }
16
+ return new Promise((resolve, reject) => {
17
+ exec(noLFNorCRLFCmd, (error, stdout) => {
18
+ if (error && !silentFail) {
19
+ console.error(error);
20
+ reject(error);
21
+ }
22
+ resolve(stdout);
23
+ });
24
+ });
25
+ };
26
+
27
+ export async function promptForCommandOptions() {
28
+ const preAnswears = await inquirer.prompt([
29
+ {
30
+ type: 'list',
31
+ name: 'readyForProcedure',
32
+ message: 'are you currently on the branch you want to reverse merge into?',
33
+ default: 'n',
34
+ choices: ['y', 'n'],
35
+ },
36
+ {
37
+ type: 'list',
38
+ name: 'pendingChanges',
39
+ message: 'you have pending changes in your working directory?',
40
+ default: 'y',
41
+ choices: ['y', 'n'],
42
+ },
43
+ ]);
44
+ if (preAnswears.readyForProcedure === 'n') {
45
+ console.log('Please checkout the branch you want to reverse merge into and run the command again');
46
+ process.exit(0);
47
+ }
48
+ if (preAnswears.pendingChanges === 'y') {
49
+ console.log('Please commit or stash your changes and run the command again');
50
+ process.exit(0);
51
+ }
52
+ // we use git remote to get the list of remotes to choose from
53
+ const gitRemotes = await execSyntaxSugar({ command: 'git remote' });
54
+ const gitRemotesArray = gitRemotes.split('\n').filter((remote) => remote !== '');
55
+
56
+ const questions = [
57
+ {
58
+ type: 'list',
59
+ name: 'gitRemote',
60
+ message: 'which remote do you want to take the file from?',
61
+ choices: gitRemotesArray,
62
+ default: gitRemotesArray[0],
63
+ },
64
+ {
65
+ type: 'text',
66
+ name: 'gitBranch',
67
+ message: 'which branch do you want to take the files from?',
68
+ default: 'develop',
69
+ },
70
+ ];
71
+
72
+ const answers = await inquirer.prompt(questions);
73
+
74
+ return {
75
+ ...answers,
76
+ };
77
+ }
78
+
79
+ /*
80
+ * given a file name, gitRemote and gitBranch, return the command to restore the file in all subfolders, taking the file from the remote branch
81
+ * @param options
82
+ * @param {string} [options.gitRemote] - the remote to take the file from
83
+ * @param {string} [options.gitBranch] - the branch to take the file from
84
+ * @param {string} [options.fileName] - the file to restore
85
+ * @param {boolean} [options.wantSubfolder] - whether to restore the file in all subfolders or current folder only
86
+ * @returns {string} - the command to restore the file in all subfolders
87
+ *
88
+ * @example
89
+ * // => git ls-files origin/develop '** /CHANGELOG.md' | xargs git restore --source=origin/develop'
90
+ * getRestoreFileFromBranchUnixCmd({gitRemote: 'origin', gitBranch: 'develop', fileName: 'CHANGELOG.md'})
91
+ */
92
+ const getRestoreFileFromBranchUnixCmd = ({ gitRemote, gitBranch, fileName, wantSubfolder = true }) =>
93
+ wantSubfolder
94
+ ? `git ls-files ${gitRemote}/${gitBranch} '**/${fileName}' | xargs git restore --source=${gitRemote}/${gitBranch} `
95
+ : `git ls-files ${gitRemote}/${gitBranch} '${fileName}' | xargs git restore --source=${gitRemote}/${gitBranch} `;
96
+
97
+ // 0 nothing to do, 1 can't auto-solve, -1 can auto-solve
98
+ const canAutosolvePackageJsonConflict = async (packageJsonFilePath) => {
99
+ const conflictRegexp = /<<<<<<<[\s\S]*=======[\s\S]*?>>>>>>>[\s\S]*?\n/g;
100
+ const packageJsonString = fs.readFileSync(packageJsonFilePath, 'utf8');
101
+
102
+ // check if only one conflict marker exists, if multiple, this requires manual resolution
103
+ const foundConflictMarkers = [...packageJsonString.matchAll(conflictRegexp)];
104
+ //check if no conflict markers exist
105
+ if (foundConflictMarkers.length === 0) return 0;
106
+ if (foundConflictMarkers.length === 1) {
107
+ // check if the conflict is only in the version field by matching "version" and see if it's found twice
108
+ const foundVersionKeys = [...packageJsonString.matchAll(/"version"/g)];
109
+ return foundVersionKeys.length === 2 ? -1 : 1;
110
+ }
111
+ return 1;
112
+ };
113
+
114
+ /*
115
+ * reverse merge a file from a remote branch using the following strategy:
116
+ * 1 - keep current branch ./lerna.json
117
+ * 2 - keep current branch ./ci_cd folder content
118
+ * 3 - keep current branch CHANGELOG.md (they will be auto-generated, but keeping "current" makes the diff easier to read)
119
+ * 4 - keep current branch ** /CHANGELOG.md (they will be auto-generated, but keeping "current" makes the diff easier to read
120
+ * 5 - check `** /* /package.json` that the conflict is ONLY in `version` field, any other conflict is marked for manual resolution
121
+ * 6 - run 'pnpm run use-registry-version && pnpm run use-workspace-version' to update the package.json files with the correct version
122
+ * also automatically switch to the $gitBranch-$gitCurrent-mmddyyyy new branch
123
+ * @param options
124
+ * @param {string} [options.gitRemote] - the remote to take the file from
125
+ * @param {string} [options.gitBranch] - the branch to take the file from
126
+ * @returns {Promise<void>}
127
+ */
128
+ const gitReverseMerge = async (options) => {
129
+ const { gitRemote, gitBranch } = options;
130
+
131
+ const gitCurrent = await execSyntaxSugar({ command: 'git branch --show-current', dryRun: false });
132
+ const todayMMDDYYYY = new Intl.DateTimeFormat('en-US', {
133
+ month: '2-digit',
134
+ day: '2-digit',
135
+ year: 'numeric',
136
+ })
137
+ .format(new Date())
138
+ .replace(/\//g, '');
139
+ const newBranchName = `${gitBranch}-${gitCurrent}-${todayMMDDYYYY}`;
140
+
141
+ // create a new branch
142
+ await execSyntaxSugar({ command: `git checkout -b ${newBranchName}` });
143
+ // run the git merge command
144
+ await execSyntaxSugar({
145
+ command: `git merge --no-edit --no-commit --no-ff ${gitRemote}/${gitBranch}`,
146
+ silentFail: true,
147
+ });
148
+
149
+ // apply the strategy
150
+ // 1 - keep current branch ./lerna.json
151
+ const restoreLernaJsonCmd = `git restore --source=HEAD lerna.json`;
152
+ await execSyntaxSugar({ command: restoreLernaJsonCmd });
153
+ // 2 - keep current branch ./ci_cd folder content
154
+ const restoreCiCdCmd = `git restore --source=HEAD ci_cd`;
155
+ await execSyntaxSugar({ command: restoreCiCdCmd });
156
+ // 3 - keep any branch CHANGELOG.md (they will be auto-generated anyway)
157
+ const restoreChangelogCmd = getRestoreFileFromBranchUnixCmd({
158
+ gitRemote,
159
+ gitBranch: gitCurrent,
160
+ fileName: 'CHANGELOG.md',
161
+ wantSubfolder: false,
162
+ });
163
+ await execSyntaxSugar({ command: restoreChangelogCmd });
164
+ // 4 - keep any branch ** /CHANGELOG.md (they will be auto-generated anyway)
165
+ const restoreChangelogsCmd = getRestoreFileFromBranchUnixCmd({
166
+ gitRemote,
167
+ gitBranch: gitCurrent,
168
+ fileName: 'CHANGELOG.md',
169
+ wantSubfolder: true,
170
+ });
171
+ await execSyntaxSugar({ command: restoreChangelogsCmd });
172
+
173
+ // we check all package.json files for conflicts and resolve those that can be auto-resolved, listin the ones that can't
174
+ const cantAutoSolve = [];
175
+ const globPatternPackage = '**/*/package.json';
176
+ const packageJsonFilesPaths = glob(globPatternPackage, { sync: true, ignore: ['**/node_modules/**', '**/dist/**'] });
177
+
178
+ for (let i = 0; i < packageJsonFilesPaths.length; i += 1) {
179
+ const packageJsonFilePath = packageJsonFilesPaths[i];
180
+ const canAutoSolve = await canAutosolvePackageJsonConflict(packageJsonFilePath);
181
+ if (canAutoSolve === 0) continue;
182
+
183
+ if (canAutoSolve === 1) {
184
+ cantAutoSolve.push(packageJsonFilePath);
185
+ continue;
186
+ }
187
+
188
+ const restorePackageJsonCmd = getRestoreFileFromBranchUnixCmd({
189
+ gitRemote,
190
+ gitBranch,
191
+ fileName: packageJsonFilePath,
192
+ wantSubfolder: false,
193
+ });
194
+ await execSyntaxSugar({ command: restorePackageJsonCmd });
195
+ }
196
+
197
+ // 6 - run 'pnpm run use-registry-version && pnpm run use-workspace-version' to update the package.json files with the correct version
198
+ await execSyntaxSugar({ command: 'pnpm run use-registry-version && pnpm run use-workspace-version' });
199
+
200
+ // if there are conflicts that can't be auto-solved, list them
201
+ if (cantAutoSolve.length > 0) {
202
+ console.log("The following package.json files have conflicts that can't be auto-solved:");
203
+ console.log(cantAutoSolve.join('\n'));
204
+ }
205
+ };
206
+
207
+ export const execGitReverseMerge = async (options) => {
208
+ const commandOptions = await promptForCommandOptions(options);
209
+ await gitReverseMerge(commandOptions);
210
+ };
@@ -0,0 +1,104 @@
1
+ /* eslint-disable complexity, no-console, max-statements */
2
+ import inquirer from 'inquirer';
3
+ import defGlob from 'glob';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ const { glob } = defGlob;
7
+
8
+ export async function promptForCommandOptions(options) {
9
+ const questions = [];
10
+ if (!options.hello) {
11
+ questions.push({
12
+ type: 'list',
13
+ name: 'dryRun',
14
+ message: 'dry-run?',
15
+ default: 'n',
16
+ choices: ['y', 'n'],
17
+ });
18
+ }
19
+ const answers = await inquirer.prompt(questions);
20
+
21
+ return {
22
+ ...answers,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * check all subfolders under cwd + packages/ for a package.json file
28
+ * - if it exists, check for project.json file in the same folder
29
+ * - if it doesn't exist, continue to the next folder
30
+ * - if both exist, check the "tags" array in the project.json file to
31
+ * - package.json `typesafety`
32
+ * - true
33
+ * - tag ensure "strict-eslint" exists in the tags array
34
+ * - false
35
+ * - tag remove "strict-eslint" from the tags array if it exists
36
+ * - `"status:eol"`
37
+ * - yes
38
+ * - remove the taxonomy tag from the tags array
39
+ * - no
40
+ * - add the taxonomy tag to the tags array if it doesn't exist or prompt the user to add it(in dry-run mode)
41
+ * (E.G. packages/layout/... -> "layout", packages/atom/... -> "atom")
42
+ * - if it doesn't exist, continue to the next folder
43
+ * @param options
44
+ * @param {string} [options.dryRun='n'] - Indicates whether to perform a dry run ('y' or 'n').
45
+ * @returns {Promise<void>}
46
+ */
47
+ const fixMissingTags = async (options) => {
48
+ const { dryRun } = options;
49
+ const globPatternPackage = 'packages/**/*/package.json';
50
+ const packageJsonFilesPaths = glob(globPatternPackage, { sync: true, ignore: ['**/node_modules/**', '**/dist/**'] });
51
+ for (let i = 0; i < packageJsonFilesPaths.length; i += 1) {
52
+ const packageJsonFilePath = packageJsonFilesPaths[i];
53
+ const finalPath = path.resolve(process.cwd(), packageJsonFilePath);
54
+ const packageJson = JSON.parse(fs.readFileSync(finalPath, 'utf8'));
55
+ const { publishConfig: { typeSafety = false } = {} } = packageJson;
56
+ const taxonomy = finalPath.split('/').reverse()[2];
57
+
58
+ const projectJsonFilePath = path.resolve(finalPath, '../project.json');
59
+ if (!fs.existsSync(projectJsonFilePath)) continue;
60
+ const projectJson = JSON.parse(fs.readFileSync(projectJsonFilePath, 'utf8'));
61
+ const missingTags = [];
62
+ const extraTags = [];
63
+
64
+ const isMarkedAsStrict = projectJson.tags.includes('strict-eslint');
65
+ if (typeSafety && !isMarkedAsStrict) missingTags.push('strict-eslint');
66
+ if (!typeSafety && isMarkedAsStrict) extraTags.push('strict-eslint');
67
+
68
+ const isEol = projectJson.tags.includes('status:eol');
69
+ const hasTaxonomy = projectJson.tags.includes(taxonomy);
70
+ if (!isEol && !hasTaxonomy) missingTags.push(taxonomy);
71
+ if (isEol && hasTaxonomy) extraTags.push(taxonomy);
72
+
73
+ const shouldRemoveTags = extraTags.length !== 0;
74
+ const shouldAddTags = missingTags.length !== 0;
75
+ const shouldDoNothing = !shouldAddTags && !shouldRemoveTags;
76
+ if (shouldDoNothing) continue;
77
+
78
+ // mutate projectJson.tags
79
+ if (shouldAddTags) {
80
+ projectJson.tags.push(...missingTags);
81
+ }
82
+ // mutate projectJson.tags
83
+ if (shouldRemoveTags) {
84
+ projectJson.tags = projectJson.tags.filter((tag) => !extraTags.includes(tag));
85
+ }
86
+
87
+ if (dryRun === 'n') {
88
+ projectJson.tags.sort();
89
+ fs.writeFileSync(projectJsonFilePath, JSON.stringify(projectJson, null, 2));
90
+ if (shouldAddTags) console.log(`Added ${missingTags.join(', ')} tag to ${projectJsonFilePath}`);
91
+ if (shouldRemoveTags) console.log(`Removed ${taxonomy} tag from ${projectJsonFilePath}`);
92
+ continue;
93
+ }
94
+ if (dryRun === 'y') {
95
+ if (shouldAddTags) console.log(`Would have added ${missingTags.join(', ')} to ${projectJsonFilePath}`);
96
+ if (shouldRemoveTags) console.log(`Would have removed ${taxonomy} from ${projectJsonFilePath}`);
97
+ }
98
+ }
99
+ };
100
+
101
+ export const execSyncNxTags = async (options) => {
102
+ const commandOptions = await promptForCommandOptions(options);
103
+ await fixMissingTags(commandOptions);
104
+ };
@@ -0,0 +1,38 @@
1
+ import inquirer from 'inquirer';
2
+ import { execSyncNxTags } from './execSyncNxTags/index.mjs';
3
+ import {execGitReverseMerge} from './execGitReverseMerge/index.mjs';
4
+
5
+ const COMMANDS = {
6
+ SYNC_NX_TAGS: 'sync-nx-tags',
7
+ REVERSE_MERGE_BRANCH: 'reverse-merge-branch',
8
+ EXIT: 'exit',
9
+ };
10
+ export const checkCommandOrInquire = async (options) => {
11
+ const questions = [];
12
+ if (!options.command) {
13
+ questions.push({
14
+ type: 'list',
15
+ name: 'command',
16
+ message: 'Please choose which command to run',
17
+ choices: Object.values(COMMANDS),
18
+ });
19
+ }
20
+ const answers = await inquirer.prompt(questions);
21
+ return {
22
+ ...options,
23
+ ...answers,
24
+ };
25
+ };
26
+
27
+ export async function executeCommandsMap(args, options) {
28
+ switch (options.command) {
29
+ case COMMANDS.SYNC_NX_TAGS:
30
+ execSyncNxTags(options);
31
+ break;
32
+ case COMMANDS.REVERSE_MERGE_BRANCH:
33
+ execGitReverseMerge(options);
34
+ break;
35
+ default:
36
+ break;
37
+ }
38
+ }
@@ -1,14 +1,70 @@
1
1
  import { jestConfig } from '@elliemae/pui-cli';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
5
+ const __dirname = path.dirname(__filename); // get the name of the directory
6
+ const getFileFromCurrentFolder = (fileName) => path.normalize(path.resolve(__dirname, fileName));
7
+ /**
8
+ * Generates an array of setup files for Jest based on provided flags.
9
+ *
10
+ * @param {Object} options - The options object.
11
+ * @param {boolean} [options.silenceConsole=false] - Flag to determine if console output should be silenced.
12
+ * @returns {string[]} An array of setup files for Jest.
13
+ */
14
+ const getSetupFilesAfterEnvBasedOnFlags = ({ silenceConsole = false }) => {
15
+ const setupFilesAfterEnv = [];
2
16
 
3
- export const config = (specificFile = undefined, specialRules = {}) => ({
4
- ...jestConfig,
5
- collectCoverage: false,
6
- collectCoverageFrom: [],
7
- coverageDirectory: undefined,
8
- coverageReporters: undefined,
9
- ...specialRules,
10
- // testRegex: 'tests/.*\\.test\\.[jt]sx?$',
11
- testRegex: specificFile ? `.*/tests/${specificFile}$` : 'tests/.*\\.test\\.[jt]sx?$',
12
- testTimeout: 120000,
13
- });
17
+ if (silenceConsole) {
18
+ setupFilesAfterEnv.push(getFileFromCurrentFolder('noConsoleMode.mjs'));
19
+ }
20
+
21
+ return setupFilesAfterEnv;
22
+ };
23
+
24
+ /**
25
+ * Generates a Jest configuration object.
26
+ *
27
+ * @param {Object} options - Configuration options.
28
+ * @param {string} [options.relativePathAfterTestsFolder] - Relative path after the tests folder to match test files.
29
+ * @param {Object} [options.firstLevelSpread] - Configuration options to override defaults.
30
+ * @param {Object} options.flags - Flags for configuration.
31
+ * @param {boolean} [options.flags.silenceConsole=false] - Flag to silence the console during tests.
32
+ * @returns {import('jest').Config} Jest configuration object.
33
+ *
34
+ * @example
35
+ * config({
36
+ * // matches tests/GlobalHeader.keyboard.test.js
37
+ * relativePathAfterTestsFolder: 'GlobalHeader.keyboard.test.js',
38
+ * // overrides the default configuration for jest config 'verbose' to true
39
+ * firstLevelSpread: { verbose: true },
40
+ * // silences the console during tests via custom setup file
41
+ * flags: { silenceConsole: true }
42
+ * });
43
+ *
44
+ * @example
45
+ * // matches tests/GlobalHeader/a11y/test.keys.js
46
+ * config({
47
+ * relativePathAfterTestsFolder: 'GlobalHeader/a11y/test.keys.js',
48
+ * flags: { silenceConsole: true }
49
+ * });
50
+ */
51
+ export const config = ({ relativePathAfterTestsFolder = undefined, firstLevelSpread = {}, flags = {} } = {}) => {
52
+ const silenceConsole = flags.silenceConsole ?? process.env.JEST_FORCE_SILENT_CONSOLE ?? false;
53
+
54
+ /** @type {import('jest').Config} */
55
+ return {
56
+ ...jestConfig,
57
+ collectCoverage: false,
58
+ collectCoverageFrom: [],
59
+ coverageDirectory: undefined,
60
+ coverageReporters: undefined,
61
+ // testRegex: 'tests/.*\\.test\\.[jt]sx?$',
62
+ testRegex: relativePathAfterTestsFolder
63
+ ? `.*/tests/${relativePathAfterTestsFolder}$`
64
+ : 'tests/.*\\.test\\.[jt]sx?$',
65
+ testTimeout: 120000,
66
+ setupFilesAfterEnv: [...jestConfig.setupFilesAfterEnv, ...getSetupFilesAfterEnvBasedOnFlags({ silenceConsole })],
67
+ ...firstLevelSpread,
68
+ };
69
+ };
14
70
  export default config;
@@ -0,0 +1 @@
1
+ global.console = { jestLog: console.log, error: jest.fn(), warn: jest.fn(), log: jest.fn() };
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@elliemae/ds-monorepo-devops",
3
- "version": "3.50.1-next.8",
3
+ "version": "3.51.0-beta.1",
4
4
  "license": "MIT",
5
5
  "description": "ICE MT - Dimsum - Monorepo Devops",
6
+ "type": "module",
6
7
  "files": [
8
+ "bin",
7
9
  "configs"
8
10
  ],
11
+ "bin": {
12
+ "@elliemae/ds-monorepo-devops": "./bin/ds-monorepo-devops.mjs",
13
+ "ds-monorepo-devops": "./bin/ds-monorepo-devops.mjs"
14
+ },
9
15
  "exports": {
10
16
  "./configs/jest.config": {
11
17
  "import": "./configs/jest.config.mjs",
@@ -19,7 +25,7 @@
19
25
  },
20
26
  "engines": {
21
27
  "pnpm": ">=8",
22
- "node": ">=18"
28
+ "node": ">=22"
23
29
  },
24
30
  "author": "ICE MT",
25
31
  "jestSonar": {
@@ -29,7 +35,11 @@
29
35
  "indent": 4
30
36
  },
31
37
  "peerDependencies": {
32
- "@elliemae/pui-cli": "9.0.0-next.31",
38
+ "@elliemae/pui-cli": "9.0.0-next.55",
39
+ "arg": "~5.0.2",
40
+ "glob": "~10.2.5",
41
+ "ignore": "^5.3.0",
42
+ "inquirer": "~12.0.0",
33
43
  "jest": "~29.7.0"
34
44
  },
35
45
  "publishConfig": {