@toptal/davinci-update 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @toptal/davinci-update
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2393](https://github.com/toptal/davinci/pull/2393) [`8400c60f`](https://github.com/toptal/davinci/commit/8400c60f55ef8c82e99e22404009d2b1ad57aa40) Thanks [@mkrl](https://github.com/mkrl)!
8
+
9
+ ### davinci-update
10
+
11
+ - adds a utility that updates Picasso and Davinci alpha packages using a PR link
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import cliEngine from '@toptal/davinci-cli-shared'
3
+ import { loadCommands, bootstrap } from '@toptal/davinci-cli-shared'
4
4
 
5
- import updater from '../src/index.mjs'
5
+ import { fromCommand } from '../src/commands/from.mjs'
6
6
 
7
- const { commands } = updater
8
-
9
- cliEngine.loadCommands(commands, 'davinci-update')
10
- cliEngine.bootstrap()
7
+ loadCommands([fromCommand], 'davinci-update')
8
+ bootstrap()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toptal/davinci-update",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Update Picasso, Davinci and Topkit packages from a pull request",
5
5
  "repository": "toptal/davinci.git",
6
6
  "author": "Toptal",
@@ -17,7 +17,7 @@
17
17
  "CHANGELOG.md"
18
18
  ],
19
19
  "scripts": {
20
- "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' yarn jest"
20
+ "test": "exit 0"
21
21
  },
22
22
  "dependencies": {
23
23
  "@toptal/davinci-cli-shared": "^2.4.0",
package/src/actions.mjs CHANGED
@@ -1,31 +1,28 @@
1
- import { $, print } from '@toptal/davinci-cli-shared'
1
+ import { $, print, spinner, globby, prompt } from '@toptal/davinci-cli-shared'
2
2
  import { promises as fs } from 'fs'
3
3
 
4
- export const ghInstalled = async () => {
5
- const isInstalled = (await $`gh version`.exitCode) === 0
6
- if (!isInstalled) {
4
+ export const ensureGhIsInstalled = async () => {
5
+ try {
6
+ await $`gh version`
7
+ return true
8
+ } catch (err) {
7
9
  print.red(
8
10
  'GitHub CLI is not installed. Please install and authenticate before running this command.'
9
11
  )
10
12
  print.red('https://cli.github.com/')
11
13
  process.exit(1)
12
14
  }
13
- return isInstalled
14
15
  }
15
16
 
16
17
  export const fetchComments = async prLink => {
17
18
  const spinner = print.startSpinner('Fetching PR comments...')
18
19
  const request = await $`gh pr view ${prLink} --json comments`
19
- if (request.exitCode !== 0) {
20
- print.red('Error fetching comments')
21
- process.exit(1)
22
- }
23
20
  spinner.stop()
24
21
  print.green('Fetched PR comments')
25
22
  return JSON.parse(request.stdout)
26
23
  }
27
24
 
28
- export const parseLatestAlpha = comments => {
25
+ export const extractLatestAlphaFromComments = comments => {
29
26
  const alphaComments = comments.filter(comment =>
30
27
  comment.body.includes('Your alpha package is ready')
31
28
  )
@@ -35,9 +32,9 @@ export const parseLatestAlpha = comments => {
35
32
  )
36
33
  process.exit(1)
37
34
  }
38
- const latestAlpha = alphaComments.sort(
39
- (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
40
- )[0]
35
+ const [latestAlpha] = alphaComments.sort(
36
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
37
+ )
41
38
  const packages = latestAlpha.body.split('<br/>').slice(1).slice(0, -1)
42
39
  const packagesSanitized = packages.map(line =>
43
40
  line.replace(/.*`yarn add (.*)`.*/, '$1')
@@ -61,29 +58,15 @@ const processPackageJson = async (file, packages) => {
61
58
  const packageJson = JSON.parse(await fs.readFile(file, 'utf-8'))
62
59
  let packageUpdated = false
63
60
  Object.keys(packages).forEach(pkg => {
64
- if (
65
- packageJson.dependencies &&
66
- packageJson.dependencies[pkg] &&
67
- packages[pkg] !== packageJson.dependencies[pkg]
68
- ) {
69
- packageJson.dependencies[pkg] = packages[pkg]
70
- packageUpdated = true
71
- }
72
- if (
73
- packageJson.devDependencies &&
74
- packageJson.devDependencies[pkg] &&
75
- packages[pkg] !== packageJson.devDependencies[pkg]
76
- ) {
77
- packageJson.devDependencies[pkg] = packages[pkg]
78
- packageUpdated = true
79
- }
80
- if (
81
- packageJson.peerDependencies &&
82
- packageJson.peerDependencies[pkg] &&
83
- packages[pkg] !== packageJson.peerDependencies[pkg]
84
- ) {
85
- packageJson.peerDependencies[pkg] = packages[pkg]
86
- packageUpdated = true
61
+ for (const deps of [
62
+ packageJson.dependencies,
63
+ packageJson.devDependencies,
64
+ packageJson.peerDependencies,
65
+ ]) {
66
+ if (deps?.[pkg] && deps[pkg] !== packages[pkg]) {
67
+ deps[pkg] = packages[pkg]
68
+ packageUpdated = true
69
+ }
87
70
  }
88
71
  })
89
72
  if (packageUpdated) {
@@ -97,18 +80,51 @@ const processPackageJson = async (file, packages) => {
97
80
  }
98
81
  }
99
82
 
83
+ export const updateResolutions = async packages => {
84
+ try {
85
+ const selectedPackages = await prompt.checkbox(
86
+ 'packages',
87
+ 'Select packages to update',
88
+ Object.keys(packages).map(pkg => ({
89
+ name: pkg,
90
+ value: pkg,
91
+ })),
92
+ 15
93
+ )
94
+ const packageJson = JSON.parse(await fs.readFile('./package.json', 'utf-8'))
95
+ packageJson.resolutions = packageJson.resolutions || {}
96
+ selectedPackages.forEach(pkg => {
97
+ packageJson.resolutions[pkg] = packages[pkg]
98
+ })
99
+ await fs.writeFile(
100
+ './package.json',
101
+ `${JSON.stringify(packageJson, null, 2)}\n`
102
+ )
103
+ } catch (e) {
104
+ print.red(`Could not update resolutions in package.json`)
105
+ console.error(e)
106
+ process.exit(1)
107
+ }
108
+ print.green('Updated resolutions in package.json')
109
+ return true
110
+ }
111
+
100
112
  export const updatePackageJsonFiles = async packages => {
101
- const spinner = print.startSpinner(
102
- 'Updating package.json files in the current repository...'
103
- )
104
- const packageJsonFiles =
105
- await $`find . -type f -name "package.json" | grep -v '/node_modules/' | grep -v '/dist-package/' | grep -v '/dist/' | grep -v '/__tests__/'`
106
- const files = packageJsonFiles.stdout.split('\n').filter(Boolean)
107
- const resolvedPackages = await Promise.all(
108
- files.map(file => processPackageJson(file, packages))
113
+ const updatedPackages = await spinner(
114
+ 'Updating package.json files in the current repository...',
115
+ async () => {
116
+ const packageJsonFiles = await globby(
117
+ ['**/package.json', 'package.json'],
118
+ {
119
+ gitignore: true,
120
+ }
121
+ )
122
+ const resolvedPackages = await Promise.all(
123
+ packageJsonFiles.map(file => processPackageJson(file, packages))
124
+ )
125
+ return resolvedPackages.filter(Boolean)
126
+ }
109
127
  )
110
- spinner.stop()
111
- const updatedPackages = resolvedPackages.filter(Boolean)
112
128
  if (updatedPackages.length === 0) {
113
129
  print.yellow('No package.json files were updated.')
114
130
  return false
@@ -118,47 +134,63 @@ export const updatePackageJsonFiles = async packages => {
118
134
  }
119
135
 
120
136
  export const runSyncpack = async () => {
121
- const spinner = print.startSpinner('Running syncpack fix-mismatches ...')
122
- const request = await $`yarn run syncpack fix-mismatches`
123
- spinner.stop()
124
- if (request.exitCode !== 0) {
125
- print.red('Error running syncpack')
126
- }
137
+ await spinner(
138
+ 'Running syncpack fix-mismatches ...',
139
+ async () => await $`yarn run syncpack fix-mismatches`
140
+ )
127
141
  print.green('Running syncpack fix-mismatches finished successfully')
128
142
  }
129
143
 
130
144
  export const yarnInstall = async () => {
131
- const spinner = print.startSpinner('Running yarn install...')
132
- const request = await $`yarn install`
133
- spinner.stop()
134
- if (request.exitCode !== 0) {
135
- print.red('Error running yarn install')
136
- process.exit(1)
137
- }
145
+ await spinner('Running yarn install...', async () => await $`yarn install`)
138
146
  print.green('A new yarn.lock is generated!')
139
147
  }
140
148
 
141
- export const createPullRequest = async prLink => {
142
- print.grey(`Creating a Pull Request with the updated packages`)
143
-
149
+ export const createOrUpdatePullRequest = async prLink => {
144
150
  const branchName = `fx-${prLink.split('/').at(-3)}-pr-${prLink
145
151
  .split('/')
146
152
  .at(-1)}-update-alpha`
147
- const spinner1 = print.startSpinner(
148
- `Committing changes to a new branch ${branchName}`
149
- )
150
- await $`git checkout -b ${branchName}`
151
- await $`git add .`
152
- await $`git commit -m "Update to alpha packages"`
153
- spinner1.stop()
154
- const spinner2 = print.startSpinner(`Pushing...`)
155
- await $`git push --set-upstream origin ${branchName}`
156
- spinner2.stop()
157
- const spinner3 = print.startSpinner(
158
- `Creating a draft Pull Request to master...`
159
- )
160
- const result =
161
- await $`gh pr create -d --title "Update to alpha packages" --body "This PR updates packages to the latest alpha versions found in ${prLink}" --base master`
162
- spinner3.stop()
163
- print.success(`Created a draft Pull Request: ${result.stdout}`)
153
+ try {
154
+ await spinner(`Checking if a ${branchName} already exists...`, async () => {
155
+ await $`git fetch`
156
+ // Using --exit-code on a remote origin, this will fail if the branch exists
157
+ await $`git ls-remote --exit-code --heads origin ${branchName}`
158
+ })
159
+ // Branch already exists
160
+ await spinner(
161
+ `Branch ${branchName} already exists, pushing new changes to the branch...`,
162
+ async () => {
163
+ await $`git checkout ${branchName}`
164
+ await $`git add .`
165
+ await $`git commit -m "Update to alpha packages"`
166
+ await $`git push origin ${branchName}`
167
+ }
168
+ )
169
+ print.success(`Pushed new changes to remote ${branchName}`)
170
+ } catch {
171
+ // Branch does not exist
172
+ print.grey(`Creating a Pull Request with the updated packages`)
173
+ try {
174
+ await spinner(
175
+ `Committing changes to a new branch ${branchName}`,
176
+ async () => {
177
+ await $`git checkout -b ${branchName}`
178
+ await $`git add .`
179
+ await $`git commit -m "Update to alpha packages"`
180
+ }
181
+ )
182
+ await spinner(`Pushing...`, async () => {
183
+ await $`git push --set-upstream origin ${branchName}`
184
+ })
185
+ const result = await spinner(
186
+ `Creating a draft Pull Request to master...`,
187
+ () =>
188
+ $`gh pr create -d --title "Update to alpha packages" --body "This PR updates packages to the latest alpha versions found in ${prLink}" --base master`
189
+ )
190
+ print.success(`Created a draft Pull Request: ${result.stdout}`)
191
+ } catch {
192
+ print.red('Failed to commit changes and create a PR')
193
+ process.exit(1)
194
+ }
195
+ }
164
196
  }
@@ -1,36 +1,45 @@
1
1
  import { print, createArgument, $ } from '@toptal/davinci-cli-shared'
2
2
  import {
3
- ghInstalled,
3
+ ensureGhIsInstalled,
4
4
  fetchComments,
5
- parseLatestAlpha,
5
+ extractLatestAlphaFromComments,
6
6
  updatePackageJsonFiles,
7
7
  runSyncpack,
8
8
  yarnInstall,
9
- createPullRequest,
9
+ createOrUpdatePullRequest,
10
+ updateResolutions,
10
11
  } from '../actions.mjs'
11
12
 
12
- const updateFrom = async (prLink, { createPr, syncpack }) => {
13
+ const updateFrom = async (prLink, { createPr, syncpack, resolutions }) => {
13
14
  $.verbose = false
14
15
  print.header(
15
- 'Updating packages in the current repository to the latest alpha in ',
16
+ `Updating ${
17
+ resolutions ? 'resolutions' : 'packages'
18
+ } in the current repository to the latest alpha in `,
16
19
  prLink,
17
- createPr ? 'and creating a new PR' : ''
20
+ createPr ? 'and creating (or updating) a new PR' : ''
18
21
  )
19
22
 
20
- if (await ghInstalled()) {
23
+ if (await ensureGhIsInstalled()) {
21
24
  const { comments } = await fetchComments(prLink)
22
- const packages = parseLatestAlpha(comments)
23
- const didUpdate = await updatePackageJsonFiles(packages)
25
+ const packages = extractLatestAlphaFromComments(comments)
26
+ const didUpdate = resolutions
27
+ ? await updateResolutions(packages)
28
+ : await updatePackageJsonFiles(packages)
24
29
  if (didUpdate) {
25
- if (syncpack) {
30
+ if (syncpack && !resolutions) {
26
31
  await runSyncpack()
27
32
  } else {
28
33
  print.yellow('Skipping syncpack')
29
34
  }
30
35
  await yarnInstall()
31
36
  if (createPr) {
32
- await createPullRequest(prLink)
37
+ await createOrUpdatePullRequest(prLink)
33
38
  }
39
+ } else {
40
+ print.yellow(
41
+ 'Nothing to update. Perhaps you already have the versions you need or using a wrong directory? Exiting...'
42
+ )
34
43
  }
35
44
  }
36
45
  process.exit(0)
@@ -46,10 +55,17 @@ export const fromCommand = program => {
46
55
  'Link to a Picasso, Davinci or Topkit pull request with alpha packages generated'
47
56
  )
48
57
  )
49
- .option('-C, --create-pr', 'Automatically create a PR when finished')
58
+ .option(
59
+ '-C, --create-pr',
60
+ 'Automatically create or update a PR when finished'
61
+ )
50
62
  .option(
51
63
  '-S, --no-syncpack',
52
64
  'Do not run syncpack fix after updating package.json files'
53
65
  )
66
+ .option(
67
+ '-R, --resolutions',
68
+ 'Update resolutions in root package.json. This will only update one file and prompt you to select the packages. Skips running syncpack automatically.'
69
+ )
54
70
  .action(updateFrom)
55
71
  }
package/src/index.mjs DELETED
@@ -1,9 +0,0 @@
1
- import { fromCommand } from './commands/from.mjs'
2
-
3
- export const commands = [
4
- fromCommand,
5
- ]
6
-
7
- export default {
8
- commands
9
- }