@toptal/davinci-monorepo 7.0.5 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/davinci-monorepo.js +5 -6
- package/docs/graph-generate.md +2 -1
- package/package.json +1 -2
- package/src/commands/__snapshots__/graph-generate.test.js.snap +105 -28
- package/src/commands/graph-generate.js +57 -112
- package/src/commands/graph-generate.test.js +19 -3
- package/src/index.js +2 -2
- package/src/utils/graph/packages.js +83 -0
- package/src/utils/graph/packages.test.js +109 -0
- package/src/utils/graph/utils.js +65 -0
- package/src/utils/graph/utils.test.js +102 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @toptal/davinci-monorepo
|
|
2
2
|
|
|
3
|
+
## 7.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1876](https://github.com/toptal/davinci/pull/1876) [`0346b98e`](https://github.com/toptal/davinci/commit/0346b98ec60a105fed14dae179ac2dff0f7b99a0) Thanks [@OndrejTuma](https://github.com/OndrejTuma)! - ---
|
|
8
|
+
|
|
9
|
+
- add optional `--webpack-config` flag to `graph generate` command
|
|
10
|
+
|
|
3
11
|
## 7.0.5
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/bin/davinci-monorepo.js
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
import cliEngine from '@toptal/davinci-cli-shared'
|
|
4
4
|
|
|
5
5
|
import detectCircularityCommand from '../src/commands/detect-circularity.js'
|
|
6
|
-
import
|
|
6
|
+
import { createGraphGenerateCommand } from '../src/commands/graph-generate.js'
|
|
7
7
|
import metricsCommand from '../src/commands/metrics.js'
|
|
8
8
|
|
|
9
|
-
cliEngine.loadCommands(
|
|
10
|
-
detectCircularityCommand,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
], 'davinci-monorepo')
|
|
9
|
+
cliEngine.loadCommands(
|
|
10
|
+
[detectCircularityCommand, createGraphGenerateCommand, metricsCommand],
|
|
11
|
+
'davinci-monorepo'
|
|
12
|
+
)
|
|
14
13
|
cliEngine.bootstrap()
|
package/docs/graph-generate.md
CHANGED
|
@@ -24,10 +24,11 @@ davinci-monorepo graph generate [options]
|
|
|
24
24
|
### Options
|
|
25
25
|
|
|
26
26
|
| Option | Default | Description |
|
|
27
|
-
| ----------------- | ------------------
|
|
27
|
+
| ----------------- | ------------------ |--------------------------------------------------|
|
|
28
28
|
| -o, --output-file | monorepo-graph.svg | Name of the output file (.svg) |
|
|
29
29
|
| --ts-config | tsconfig.json | Name of the tsconfig file |
|
|
30
30
|
| -d, --diff | | Base branch to use for showing affected packages |
|
|
31
|
+
| --webpack-config | | Use a webpack configuration |
|
|
31
32
|
|
|
32
33
|
## FAQs
|
|
33
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toptal/davinci-monorepo",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Monorepo utility tools",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"dependency-cruiser": "^12.5.0",
|
|
40
40
|
"execa": "^5.1.1",
|
|
41
41
|
"glob": "^8.0.3",
|
|
42
|
-
"ora": "^5.4.1",
|
|
43
42
|
"ramda": "^0.28.0"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
@@ -1,39 +1,116 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
-
exports[`
|
|
4
|
-
{
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"_name": "command",
|
|
9
|
-
"argChoices": [
|
|
10
|
-
"generate",
|
|
11
|
-
],
|
|
12
|
-
"defaultValue": undefined,
|
|
13
|
-
"defaultValueDescription": undefined,
|
|
14
|
-
"description": "Graph related command to execute",
|
|
15
|
-
"parseArg": [Function],
|
|
16
|
-
"required": true,
|
|
17
|
-
"variadic": false,
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
"command": "graph",
|
|
21
|
-
"description": "Generate dependency graph in a monorepo",
|
|
3
|
+
exports[`createGraphGenerateCommand has the correct command structure 1`] = `
|
|
4
|
+
"{
|
|
5
|
+
"_events": {},
|
|
6
|
+
"_eventsCount": 4,
|
|
7
|
+
"commands": [],
|
|
22
8
|
"options": [
|
|
23
9
|
{
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
10
|
+
"flags": "-o, --output-file [outputFile]",
|
|
11
|
+
"description": "Output file name. Default is monorepo-graph.svg",
|
|
12
|
+
"required": false,
|
|
13
|
+
"optional": true,
|
|
14
|
+
"variadic": false,
|
|
15
|
+
"mandatory": false,
|
|
16
|
+
"short": "-o",
|
|
17
|
+
"long": "--output-file",
|
|
18
|
+
"negate": false,
|
|
19
|
+
"defaultValue": "monorepo-graph.svg",
|
|
20
|
+
"hidden": false,
|
|
21
|
+
"conflictsWith": []
|
|
27
22
|
},
|
|
28
23
|
{
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
24
|
+
"flags": "--ts-config [tsConfig]",
|
|
25
|
+
"description": "Path to typescript configuration file. Default is tsconfig.json",
|
|
26
|
+
"required": false,
|
|
27
|
+
"optional": true,
|
|
28
|
+
"variadic": false,
|
|
29
|
+
"mandatory": false,
|
|
30
|
+
"long": "--ts-config",
|
|
31
|
+
"negate": false,
|
|
32
|
+
"defaultValue": "tsconfig.json",
|
|
33
|
+
"hidden": false,
|
|
34
|
+
"conflictsWith": []
|
|
32
35
|
},
|
|
33
36
|
{
|
|
34
|
-
"
|
|
35
|
-
"
|
|
37
|
+
"flags": "-d, --diff <branch>",
|
|
38
|
+
"description": "Branch to use as a base for comparison and display of affected packages",
|
|
39
|
+
"required": true,
|
|
40
|
+
"optional": false,
|
|
41
|
+
"variadic": false,
|
|
42
|
+
"mandatory": false,
|
|
43
|
+
"short": "-d",
|
|
44
|
+
"long": "--diff",
|
|
45
|
+
"negate": false,
|
|
46
|
+
"hidden": false,
|
|
47
|
+
"conflictsWith": []
|
|
36
48
|
},
|
|
49
|
+
{
|
|
50
|
+
"flags": "--webpack-config [webpackConfig]",
|
|
51
|
+
"description": "Path to webpack configuration file",
|
|
52
|
+
"required": false,
|
|
53
|
+
"optional": true,
|
|
54
|
+
"variadic": false,
|
|
55
|
+
"mandatory": false,
|
|
56
|
+
"long": "--webpack-config",
|
|
57
|
+
"negate": false,
|
|
58
|
+
"hidden": false,
|
|
59
|
+
"conflictsWith": []
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"parent": null,
|
|
63
|
+
"_allowUnknownOption": false,
|
|
64
|
+
"_allowExcessArguments": true,
|
|
65
|
+
"_args": [
|
|
66
|
+
{
|
|
67
|
+
"description": "Graph related command to execute",
|
|
68
|
+
"variadic": false,
|
|
69
|
+
"argChoices": [
|
|
70
|
+
"generate"
|
|
71
|
+
],
|
|
72
|
+
"required": true,
|
|
73
|
+
"_name": "command"
|
|
74
|
+
}
|
|
37
75
|
],
|
|
38
|
-
|
|
76
|
+
"args": [],
|
|
77
|
+
"rawArgs": [],
|
|
78
|
+
"processedArgs": [],
|
|
79
|
+
"_scriptPath": null,
|
|
80
|
+
"_name": "graph",
|
|
81
|
+
"_optionValues": {
|
|
82
|
+
"outputFile": "monorepo-graph.svg",
|
|
83
|
+
"tsConfig": "tsconfig.json"
|
|
84
|
+
},
|
|
85
|
+
"_optionValueSources": {
|
|
86
|
+
"outputFile": "default",
|
|
87
|
+
"tsConfig": "default"
|
|
88
|
+
},
|
|
89
|
+
"_storeOptionsAsProperties": false,
|
|
90
|
+
"_executableHandler": false,
|
|
91
|
+
"_executableFile": null,
|
|
92
|
+
"_executableDir": null,
|
|
93
|
+
"_defaultCommandName": null,
|
|
94
|
+
"_exitCallback": null,
|
|
95
|
+
"_aliases": [],
|
|
96
|
+
"_combineFlagAndOptionalValue": true,
|
|
97
|
+
"_description": "Generate dependency graph in a monorepo",
|
|
98
|
+
"_summary": "",
|
|
99
|
+
"_enablePositionalOptions": false,
|
|
100
|
+
"_passThroughOptions": false,
|
|
101
|
+
"_lifeCycleHooks": {},
|
|
102
|
+
"_showHelpAfterError": false,
|
|
103
|
+
"_showSuggestionAfterError": true,
|
|
104
|
+
"_outputConfiguration": {},
|
|
105
|
+
"_hidden": false,
|
|
106
|
+
"_hasHelpOption": true,
|
|
107
|
+
"_helpFlags": "-h, --help",
|
|
108
|
+
"_helpDescription": "display help for command",
|
|
109
|
+
"_helpShortFlag": "-h",
|
|
110
|
+
"_helpLongFlag": "--help",
|
|
111
|
+
"_helpCommandName": "help",
|
|
112
|
+
"_helpCommandnameAndArgs": "help [command]",
|
|
113
|
+
"_helpCommandDescription": "display help for command",
|
|
114
|
+
"_helpConfiguration": {}
|
|
115
|
+
}"
|
|
39
116
|
`;
|
|
@@ -1,132 +1,77 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
1
|
import execa from 'execa'
|
|
3
|
-
import ora from 'ora'
|
|
4
2
|
import { createArgument, print } from '@toptal/davinci-cli-shared'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
3
|
|
|
7
|
-
import
|
|
4
|
+
import { getDepcruiseCommandOptions } from '../utils/graph/utils.js'
|
|
5
|
+
import { getPackageLocations } from '../utils/graph/packages.js'
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
export const graphGenerateCommand = async ({
|
|
8
|
+
outputFile,
|
|
9
|
+
...depcruiseOptions
|
|
10
|
+
}) => {
|
|
11
|
+
console.log(print.davinciGradient('Dependency graph generator\n'))
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return packages.map(({ location }) => location)
|
|
21
|
-
}
|
|
13
|
+
const spinner = print.startSpinner('Getting Packages Locations...\n', {
|
|
14
|
+
prefixText: '[GRAPH GENERATOR]',
|
|
15
|
+
})
|
|
22
16
|
|
|
23
|
-
/**
|
|
24
|
-
* @param {string} baseBranch
|
|
25
|
-
* @returns {Array<string>}
|
|
26
|
-
*/
|
|
27
|
-
const getChangedFiles = baseBranch => {
|
|
28
17
|
try {
|
|
29
|
-
const
|
|
30
|
-
'--no-pager',
|
|
31
|
-
'diff',
|
|
32
|
-
'--name-only',
|
|
33
|
-
`origin/${baseBranch}`,
|
|
34
|
-
])
|
|
35
|
-
|
|
36
|
-
return stdout.split('\n')
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return []
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {Array<string>} changedPackages
|
|
44
|
-
* @returns {string}
|
|
45
|
-
*/
|
|
46
|
-
const evaluateChangedPackages = changedPackages =>
|
|
47
|
-
changedPackages.length === 0
|
|
48
|
-
? 'undefined'
|
|
49
|
-
: `^(${changedPackages.join('|')})/`
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} baseBranch
|
|
53
|
-
* @param {Array<string>} packageLocations
|
|
54
|
-
* @returns {Array<string>}
|
|
55
|
-
*/
|
|
56
|
-
const getChangedPackages = (baseBranch, packageLocations) => {
|
|
57
|
-
const changedFiles = getChangedFiles(baseBranch)
|
|
18
|
+
const packageLocations = getPackageLocations()
|
|
58
19
|
|
|
59
|
-
|
|
60
|
-
changedFiles.find(filePath => filePath.startsWith(packageLocation + '/'))
|
|
61
|
-
)
|
|
62
|
-
}
|
|
20
|
+
spinner.succeed('Packages Locations are fetched!')
|
|
63
21
|
|
|
64
|
-
|
|
65
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
66
|
-
const packageLocations = getPackageLocations()
|
|
22
|
+
spinner.start('Generating dependency graph...\n')
|
|
67
23
|
|
|
68
|
-
|
|
24
|
+
const graphGenerationOutput = await execa(
|
|
25
|
+
'depcruise',
|
|
26
|
+
getDepcruiseCommandOptions({
|
|
27
|
+
...depcruiseOptions,
|
|
28
|
+
packageLocations,
|
|
29
|
+
}),
|
|
30
|
+
{ stdout: 'pipe' }
|
|
31
|
+
)
|
|
69
32
|
|
|
70
|
-
|
|
71
|
-
'--config',
|
|
72
|
-
path.join(__dirname, '..', 'configs', 'depcruise-config.cjs'),
|
|
73
|
-
'--ts-config',
|
|
74
|
-
tsConfig,
|
|
75
|
-
'--output-type',
|
|
76
|
-
'archi',
|
|
77
|
-
'--include-only',
|
|
78
|
-
`^(${packageLocations.join('|')})/`,
|
|
79
|
-
'--collapse',
|
|
80
|
-
`^(${packageLocations.join('|')})/`,
|
|
81
|
-
...(diff
|
|
82
|
-
? [
|
|
83
|
-
'--reaches',
|
|
84
|
-
evaluateChangedPackages(getChangedPackages(diff, packageLocations)),
|
|
85
|
-
]
|
|
86
|
-
: []),
|
|
87
|
-
'.',
|
|
88
|
-
])
|
|
89
|
-
const graphTransformProcess = execa('dot', ['-T', 'svg', '-o', outputFile])
|
|
33
|
+
spinner.succeed('Dependency graph has been generated!')
|
|
90
34
|
|
|
91
|
-
|
|
35
|
+
spinner.start(`Creating SVG file to ${outputFile}...\n`)
|
|
92
36
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
37
|
+
await execa('dot', ['-T', 'svg', '-o', outputFile], {
|
|
38
|
+
input: graphGenerationOutput.stdout,
|
|
39
|
+
})
|
|
96
40
|
|
|
97
|
-
spinner.succeed('Graph file is created
|
|
41
|
+
spinner.succeed('Graph SVG file is created!')
|
|
98
42
|
} catch (error) {
|
|
99
|
-
spinner.fail(
|
|
43
|
+
spinner.fail()
|
|
44
|
+
console.error(error)
|
|
100
45
|
process.exit(1)
|
|
101
46
|
}
|
|
102
47
|
}
|
|
103
48
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
name
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
49
|
+
export const createGraphGenerateCommand = program => {
|
|
50
|
+
return program
|
|
51
|
+
.createCommand('graph')
|
|
52
|
+
.description('Generate dependency graph in a monorepo')
|
|
53
|
+
.addArgument(
|
|
54
|
+
createArgument('<command>', 'Graph related command to execute').choices([
|
|
55
|
+
'generate',
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
.option(
|
|
59
|
+
'-o, --output-file [outputFile]',
|
|
60
|
+
'Output file name. Default is monorepo-graph.svg',
|
|
61
|
+
'monorepo-graph.svg'
|
|
62
|
+
)
|
|
63
|
+
.option(
|
|
64
|
+
'--ts-config [tsConfig]',
|
|
65
|
+
'Path to typescript configuration file. Default is tsconfig.json',
|
|
66
|
+
'tsconfig.json'
|
|
67
|
+
)
|
|
68
|
+
.option(
|
|
69
|
+
'-d, --diff <branch>',
|
|
70
|
+
'Branch to use as a base for comparison and display of affected packages'
|
|
71
|
+
)
|
|
72
|
+
.option(
|
|
73
|
+
'--webpack-config [webpackConfig]',
|
|
74
|
+
'Path to webpack configuration file'
|
|
75
|
+
)
|
|
76
|
+
.action((_, options) => graphGenerateCommand(options))
|
|
130
77
|
}
|
|
131
|
-
|
|
132
|
-
export default graphGenerateCommandCreator
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
import { createGraphGenerateCommand } from './graph-generate.js'
|
|
6
|
+
|
|
7
|
+
describe('createGraphGenerateCommand', () => {
|
|
8
|
+
let program
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
program = new Command()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks()
|
|
16
|
+
})
|
|
2
17
|
|
|
3
|
-
describe('graphGenerateCommandCreator', () => {
|
|
4
18
|
it('has the correct command structure', () => {
|
|
5
|
-
|
|
19
|
+
const command = createGraphGenerateCommand(program)
|
|
20
|
+
|
|
21
|
+
expect(JSON.stringify(command, null, 2)).toMatchSnapshot()
|
|
6
22
|
})
|
|
7
23
|
})
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import detectCircularityCommandCreator from './commands/detect-circularity.js'
|
|
2
2
|
import metricsCommandCreator from './commands/metrics.js'
|
|
3
|
-
import
|
|
3
|
+
import { createGraphGenerateCommand } from './commands/graph-generate.js'
|
|
4
4
|
import checkIfMonorepo from './utils/check-if-monorepo.js'
|
|
5
5
|
import getPackages from './utils/get-packages.js'
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ export const utils = {
|
|
|
12
12
|
export const commands = [
|
|
13
13
|
detectCircularityCommandCreator,
|
|
14
14
|
metricsCommandCreator,
|
|
15
|
-
|
|
15
|
+
createGraphGenerateCommand,
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
export default {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import execa from 'execa'
|
|
2
|
+
|
|
3
|
+
import getPackages from '../get-packages.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get Locations of packages in monorepo
|
|
7
|
+
* @return {Array<string>}
|
|
8
|
+
* @example
|
|
9
|
+
* getPackageLocations()
|
|
10
|
+
* // => ['packages/picasso', 'packages/picasso-lab']
|
|
11
|
+
*/
|
|
12
|
+
export const getPackageLocations = () => {
|
|
13
|
+
const packages = getPackages(process.cwd())
|
|
14
|
+
|
|
15
|
+
if (packages.length === 0) {
|
|
16
|
+
throw new Error('Your project does not seem to be a monorepo.')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return packages.map(({ location }) => location)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Transforms an array of package names to a regex string
|
|
24
|
+
* that can be used to match against a file path.
|
|
25
|
+
* Example:
|
|
26
|
+
* ['@toptal/picasso', '@toptal/picasso-lab'] => '^(@toptal\\/picasso|@toptal\\/picasso-lab)\\/'
|
|
27
|
+
* @param {Array<string>} packages
|
|
28
|
+
* @returns {string}
|
|
29
|
+
* @example
|
|
30
|
+
* transformPackagesToRegex(['@toptal/picasso', '@toptal/picasso-lab'])
|
|
31
|
+
* // => '^(@toptal\\/picasso|@toptal\\/picasso-lab)\\/'
|
|
32
|
+
* @example
|
|
33
|
+
* transformPackagesToRegex(['@toptal/picasso'])
|
|
34
|
+
* // => '^(@toptal\\/picasso)\\/'
|
|
35
|
+
* @example
|
|
36
|
+
* transformPackagesToRegex([])
|
|
37
|
+
* // => 'undefined'
|
|
38
|
+
* @example
|
|
39
|
+
*/
|
|
40
|
+
export const transformPackagesToRegex = packages => {
|
|
41
|
+
if (packages.length === 0) {
|
|
42
|
+
return 'undefined'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const regex = packages
|
|
46
|
+
.map(packageName => packageName.replace('/', '\\/'))
|
|
47
|
+
.join('|')
|
|
48
|
+
|
|
49
|
+
return `^(${regex})/`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get changed packages in a monorepo
|
|
54
|
+
* @param {string} baseBranch
|
|
55
|
+
* @param {Array<string>} packageLocations
|
|
56
|
+
* @returns {Array<string>}
|
|
57
|
+
* @example
|
|
58
|
+
* getChangedPackages('master', ['packages/picasso', 'packages/picasso-lab'])
|
|
59
|
+
* // => ['packages/picasso']
|
|
60
|
+
* @example
|
|
61
|
+
* getChangedPackages('master', [])
|
|
62
|
+
* // => []
|
|
63
|
+
*/
|
|
64
|
+
export const getChangedPackages = (baseBranch, packageLocations) => {
|
|
65
|
+
const { stdout } = execa.sync('git', [
|
|
66
|
+
'--no-pager',
|
|
67
|
+
'diff',
|
|
68
|
+
'--name-only',
|
|
69
|
+
`origin/${baseBranch}`,
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const changedFiles = stdout.split('\n')
|
|
73
|
+
|
|
74
|
+
return packageLocations.filter(packageLocation =>
|
|
75
|
+
changedFiles.find(filePath => filePath.startsWith(packageLocation + '/'))
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
getPackageLocations,
|
|
81
|
+
getChangedPackages,
|
|
82
|
+
transformPackagesToRegex,
|
|
83
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, jest } from '@jest/globals'
|
|
2
|
+
import execa from 'execa'
|
|
3
|
+
|
|
4
|
+
const mockedGetPackages = jest.fn()
|
|
5
|
+
|
|
6
|
+
jest.unstable_mockModule('../get-packages.js', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: mockedGetPackages,
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
const { getPackageLocations, transformPackagesToRegex, getChangedPackages } =
|
|
12
|
+
await import('./packages.js')
|
|
13
|
+
|
|
14
|
+
describe('getPackageLocations', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockedGetPackages.mockImplementation(() => [
|
|
17
|
+
{
|
|
18
|
+
location: 'packages/picasso',
|
|
19
|
+
name: '@toptal/picasso',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
location: 'packages/picasso-lab',
|
|
23
|
+
name: '@toptal/picasso-lab',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
location: 'packages/picasso-charts',
|
|
27
|
+
name: '@toptal/picasso-charts',
|
|
28
|
+
},
|
|
29
|
+
])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
jest.resetAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns package locations', () => {
|
|
37
|
+
expect(getPackageLocations()).toEqual([
|
|
38
|
+
'packages/picasso',
|
|
39
|
+
'packages/picasso-lab',
|
|
40
|
+
'packages/picasso-charts',
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('when there are no packages', () => {
|
|
45
|
+
it('throws an error', () => {
|
|
46
|
+
mockedGetPackages.mockImplementation(() => [])
|
|
47
|
+
|
|
48
|
+
expect(getPackageLocations).toThrow(
|
|
49
|
+
'Your project does not seem to be a monorepo.'
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('transformPackagesToRegex', () => {
|
|
56
|
+
it('transforms an array of package names to a regex string', () => {
|
|
57
|
+
expect(
|
|
58
|
+
transformPackagesToRegex(['@toptal/picasso', '@toptal/picasso-lab'])
|
|
59
|
+
).toBe('^(@toptal\\/picasso|@toptal\\/picasso-lab)/')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('transforms an array with single package to a regex string', () => {
|
|
63
|
+
expect(transformPackagesToRegex(['@toptal/picasso'])).toBe(
|
|
64
|
+
'^(@toptal\\/picasso)/'
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('when there are no packages', () => {
|
|
69
|
+
it('returns undefined', () => {
|
|
70
|
+
expect(transformPackagesToRegex([])).toBe('undefined')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('getChangedPackages', () => {
|
|
76
|
+
const baseBranch = 'master'
|
|
77
|
+
const packageLocations = ['packages/picasso', 'packages/picasso-lab']
|
|
78
|
+
|
|
79
|
+
it('returns changed packages', async () => {
|
|
80
|
+
jest.spyOn(execa, 'sync').mockImplementation(() => ({
|
|
81
|
+
stdout: 'packages/picasso/\npackages/picasso-lab/',
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
expect(await getChangedPackages(baseBranch, packageLocations)).toEqual([
|
|
85
|
+
'packages/picasso',
|
|
86
|
+
'packages/picasso-lab',
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('when there are changed files that are not related to packages', () => {
|
|
91
|
+
it('returns an empty array', async () => {
|
|
92
|
+
jest.spyOn(execa, 'sync').mockImplementation(() => ({
|
|
93
|
+
stdout: 'packages/picasso2/\npackages/picasso-lab3/\nREADME.md',
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
expect(await getChangedPackages(baseBranch, packageLocations)).toEqual([])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('when there are no changed packages', () => {
|
|
101
|
+
it('returns an empty array', async () => {
|
|
102
|
+
jest.spyOn(execa, 'sync').mockImplementation(() => ({
|
|
103
|
+
stdout: '',
|
|
104
|
+
}))
|
|
105
|
+
|
|
106
|
+
expect(await getChangedPackages(baseBranch, packageLocations)).toEqual([])
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getChangedPackages, transformPackagesToRegex } from './packages.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get depcruise command options
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {string} options.tsConfig
|
|
10
|
+
* @param {string} options.webpackConfig
|
|
11
|
+
* @param {string} options.diff
|
|
12
|
+
* @param {Array<string>} options.packageLocations
|
|
13
|
+
* @returns {Array<string>}
|
|
14
|
+
* @example
|
|
15
|
+
* getDepcruiseCommandOptions({
|
|
16
|
+
* tsConfig: 'tsconfig.json',
|
|
17
|
+
* webpackConfig: 'webpack.config.js',
|
|
18
|
+
* diff: 'master',
|
|
19
|
+
* packageLocations: ['packages/picasso', 'packages/picasso-lab']
|
|
20
|
+
* })
|
|
21
|
+
* // => [
|
|
22
|
+
* // '--config',
|
|
23
|
+
* // 'packages/monorepo/src/utils/graph/configs/depcruise-config.cjs',
|
|
24
|
+
* // '--ts-config',
|
|
25
|
+
* // 'tsconfig.json',
|
|
26
|
+
* // '--output-type',
|
|
27
|
+
* // 'archi',
|
|
28
|
+
* // '--include-only',
|
|
29
|
+
* // '^(@toptal\\/picasso|@toptal\\/picasso-lab)\\/',
|
|
30
|
+
* // '--collapse',
|
|
31
|
+
* // '^(@toptal\\/picasso|@toptal\\/picasso-lab)\\/',
|
|
32
|
+
* // '--reaches',
|
|
33
|
+
* // '^(@toptal\\/picasso)\\/',
|
|
34
|
+
* // '.',
|
|
35
|
+
* // ]
|
|
36
|
+
*/
|
|
37
|
+
export const getDepcruiseCommandOptions = options => {
|
|
38
|
+
const { tsConfig, webpackConfig, diff, packageLocations } = options
|
|
39
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
'--config',
|
|
43
|
+
path.join(__dirname, '../../', 'configs', 'depcruise-config.cjs'),
|
|
44
|
+
'--ts-config',
|
|
45
|
+
tsConfig,
|
|
46
|
+
'--output-type',
|
|
47
|
+
'archi',
|
|
48
|
+
'--include-only',
|
|
49
|
+
transformPackagesToRegex(packageLocations),
|
|
50
|
+
'--collapse',
|
|
51
|
+
transformPackagesToRegex(packageLocations),
|
|
52
|
+
...(webpackConfig ? ['--webpack-config', webpackConfig] : []),
|
|
53
|
+
...(diff
|
|
54
|
+
? [
|
|
55
|
+
'--reaches',
|
|
56
|
+
transformPackagesToRegex(getChangedPackages(diff, packageLocations)),
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
59
|
+
'.',
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
getDepcruiseCommandOptions,
|
|
65
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const mockedGetChangedPackages = jest.fn()
|
|
5
|
+
const mockedTransformPackagesToRegex = jest.fn()
|
|
6
|
+
|
|
7
|
+
jest.unstable_mockModule('./packages.js', () => ({
|
|
8
|
+
getChangedPackages: mockedGetChangedPackages,
|
|
9
|
+
transformPackagesToRegex: mockedTransformPackagesToRegex,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const { getDepcruiseCommandOptions } = await import('./utils.js')
|
|
13
|
+
|
|
14
|
+
describe('getDepcruiseCommandOptions', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockedGetChangedPackages.mockReturnValue(['packages/picasso-lab'])
|
|
17
|
+
|
|
18
|
+
mockedTransformPackagesToRegex.mockImplementation(packages =>
|
|
19
|
+
packages.join('|')
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
jest
|
|
23
|
+
.spyOn(path, 'join')
|
|
24
|
+
.mockReturnValue(
|
|
25
|
+
'packages/monorepo/src/utils/graph/configs/depcruise-config.cjs'
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
jest.resetAllMocks()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const options = {
|
|
34
|
+
tsConfig: 'tsconfig.json',
|
|
35
|
+
webpackConfig: 'webpack.config.js',
|
|
36
|
+
diff: 'master',
|
|
37
|
+
packageLocations: ['packages/picasso', 'packages/picasso-lab'],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('returns an array with all the required depcruise options', () => {
|
|
41
|
+
const result = getDepcruiseCommandOptions(options)
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual([
|
|
44
|
+
'--config',
|
|
45
|
+
'packages/monorepo/src/utils/graph/configs/depcruise-config.cjs',
|
|
46
|
+
'--ts-config',
|
|
47
|
+
'tsconfig.json',
|
|
48
|
+
'--output-type',
|
|
49
|
+
'archi',
|
|
50
|
+
'--include-only',
|
|
51
|
+
'packages/picasso|packages/picasso-lab',
|
|
52
|
+
'--collapse',
|
|
53
|
+
'packages/picasso|packages/picasso-lab',
|
|
54
|
+
'--webpack-config',
|
|
55
|
+
'webpack.config.js',
|
|
56
|
+
'--reaches',
|
|
57
|
+
'packages/picasso-lab',
|
|
58
|
+
'.',
|
|
59
|
+
])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('when webpackConfig is not provided', () => {
|
|
63
|
+
it('does not include webpackConfig in the result', () => {
|
|
64
|
+
const result = getDepcruiseCommandOptions({
|
|
65
|
+
...options,
|
|
66
|
+
webpackConfig: undefined,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(result).not.toContain('--webpack-config')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('when diff is not provided', () => {
|
|
74
|
+
it('does not include diff in the result', () => {
|
|
75
|
+
const result = getDepcruiseCommandOptions({
|
|
76
|
+
...options,
|
|
77
|
+
diff: undefined,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(result).not.toContain('--reaches')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('when diff is provided', () => {
|
|
85
|
+
it('calls getChangedPackages with the correct arguments', () => {
|
|
86
|
+
getDepcruiseCommandOptions(options)
|
|
87
|
+
|
|
88
|
+
expect(mockedGetChangedPackages).toHaveBeenCalledWith('master', [
|
|
89
|
+
'packages/picasso',
|
|
90
|
+
'packages/picasso-lab',
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('calls transformPackagesToRegex with the correct arguments', () => {
|
|
95
|
+
getDepcruiseCommandOptions(options)
|
|
96
|
+
|
|
97
|
+
expect(mockedTransformPackagesToRegex).toHaveBeenCalledWith([
|
|
98
|
+
'packages/picasso-lab',
|
|
99
|
+
])
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|