@toptal/davinci-update 1.0.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/LICENSE.MD ADDED
@@ -0,0 +1,4 @@
1
+ /* Copyright (C) Toptal LLC - All Rights Reserved
2
+ * Unauthorized copying of this file, via any medium is strictly prohibited
3
+ * Proprietary and confidential
4
+ */
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # `@toptal/davinci-update`
2
+
3
+ Updates Picasso, Topkit and Davinci packages
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @toptal/davinci-update from https://github.com/toptal/picasso/pull/4265
9
+ ```
10
+
11
+ This will do the following:
12
+ - Fetch the latest alpha packages from the PR comment and update all matching versions in all package.json files in the current directory
13
+ - Run `yarn install` to update the lockfile
14
+ - Run `syncpack fix-mismatches` to fix any version mismatches and package.json formatting (can be skipped)
15
+
16
+ Please note, in order for this to work, alpha packages must be generated first (usually, by commenting `@toptal-bot run package:alpha-release`)
17
+
18
+ ## Options
19
+
20
+ - `--no-syncpack` or `-S` skips running `syncpack fix-mismatches`
21
+ - `--create-pr` or `-C` commits the changes and creates a draft PR in a new branch against `master`
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import cliEngine from '@toptal/davinci-cli-shared'
4
+
5
+ import updater from '../src/index.mjs'
6
+
7
+ const { commands } = updater
8
+
9
+ cliEngine.loadCommands(commands, 'davinci-update')
10
+ cliEngine.bootstrap()
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@toptal/davinci-update",
3
+ "version": "1.0.0",
4
+ "description": "Update Picasso, Davinci and Topkit packages from a pull request",
5
+ "repository": "toptal/davinci.git",
6
+ "author": "Toptal",
7
+ "license": "SEE LICENSE IN LICENSE.MD",
8
+ "bugs": "https://github.com/toptal/davinci/issues",
9
+ "homepage": "https://github.com/toptal/davinci/tree/master/packages/code#readme",
10
+ "bin": {
11
+ "davinci-update": "./bin/davinci-update.mjs"
12
+ },
13
+ "main": "src/index.mjs",
14
+ "files": [
15
+ "src",
16
+ "bin",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' yarn jest"
21
+ },
22
+ "dependencies": {
23
+ "@toptal/davinci-cli-shared": "^2.4.0",
24
+ "hygen": "^6.2.11",
25
+ "lodash.camelcase": "^4.3.0",
26
+ "lodash.kebabcase": "^4.1.1"
27
+ },
28
+ "devDependencies": {
29
+ "@jest/globals": "^29.7.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
@@ -0,0 +1,164 @@
1
+ import { $, print } from '@toptal/davinci-cli-shared'
2
+ import { promises as fs } from 'fs'
3
+
4
+ export const ghInstalled = async () => {
5
+ const isInstalled = (await $`gh version`.exitCode) === 0
6
+ if (!isInstalled) {
7
+ print.red(
8
+ 'GitHub CLI is not installed. Please install and authenticate before running this command.'
9
+ )
10
+ print.red('https://cli.github.com/')
11
+ process.exit(1)
12
+ }
13
+ return isInstalled
14
+ }
15
+
16
+ export const fetchComments = async prLink => {
17
+ const spinner = print.startSpinner('Fetching PR comments...')
18
+ 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
+ spinner.stop()
24
+ print.green('Fetched PR comments')
25
+ return JSON.parse(request.stdout)
26
+ }
27
+
28
+ export const parseLatestAlpha = comments => {
29
+ const alphaComments = comments.filter(comment =>
30
+ comment.body.includes('Your alpha package is ready')
31
+ )
32
+ if (alphaComments.length === 0) {
33
+ print.red(
34
+ 'No generated alpha packages found in the PR, please use @toptal-bot run package:alpha-release'
35
+ )
36
+ process.exit(1)
37
+ }
38
+ const latestAlpha = alphaComments.sort(
39
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
40
+ )[0]
41
+ const packages = latestAlpha.body.split('<br/>').slice(1).slice(0, -1)
42
+ const packagesSanitized = packages.map(line =>
43
+ line.replace(/.*`yarn add (.*)`.*/, '$1')
44
+ )
45
+
46
+ const packageMap = packagesSanitized.reduce((acc, pkg) => {
47
+ const atIndex = pkg.lastIndexOf('@')
48
+ const name = pkg.substring(0, atIndex)
49
+ acc[name] = pkg.substring(atIndex + 1)
50
+ return acc
51
+ }, {})
52
+
53
+ print.grey(
54
+ `Found ${packagesSanitized.length} alpha versions in ${latestAlpha.url}`
55
+ )
56
+ return packageMap
57
+ }
58
+
59
+ const processPackageJson = async (file, packages) => {
60
+ try {
61
+ const packageJson = JSON.parse(await fs.readFile(file, 'utf-8'))
62
+ let packageUpdated = false
63
+ 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
87
+ }
88
+ })
89
+ if (packageUpdated) {
90
+ await fs.writeFile(file, JSON.stringify(packageJson, null, 2))
91
+ return file
92
+ }
93
+ } catch (e) {
94
+ print.red(`Error processing ${file}`)
95
+ console.error(e)
96
+ process.exit(1)
97
+ }
98
+ }
99
+
100
+ 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))
109
+ )
110
+ spinner.stop()
111
+ const updatedPackages = resolvedPackages.filter(Boolean)
112
+ if (updatedPackages.length === 0) {
113
+ print.yellow('No package.json files were updated.')
114
+ return false
115
+ }
116
+ print.green(`Updated ${updatedPackages.length} package.json files.`)
117
+ return true
118
+ }
119
+
120
+ 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
+ }
127
+ print.green('Running syncpack fix-mismatches finished successfully')
128
+ }
129
+
130
+ 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
+ }
138
+ print.green('A new yarn.lock is generated!')
139
+ }
140
+
141
+ export const createPullRequest = async prLink => {
142
+ print.grey(`Creating a Pull Request with the updated packages`)
143
+
144
+ const branchName = `fx-${prLink.split('/').at(-3)}-pr-${prLink
145
+ .split('/')
146
+ .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}`)
164
+ }
@@ -0,0 +1,55 @@
1
+ import { print, createArgument, $ } from '@toptal/davinci-cli-shared'
2
+ import {
3
+ ghInstalled,
4
+ fetchComments,
5
+ parseLatestAlpha,
6
+ updatePackageJsonFiles,
7
+ runSyncpack,
8
+ yarnInstall,
9
+ createPullRequest,
10
+ } from '../actions.mjs'
11
+
12
+ const updateFrom = async (prLink, { createPr, syncpack }) => {
13
+ $.verbose = false
14
+ print.header(
15
+ 'Updating packages in the current repository to the latest alpha in ',
16
+ prLink,
17
+ createPr ? 'and creating a new PR' : ''
18
+ )
19
+
20
+ if (await ghInstalled()) {
21
+ const { comments } = await fetchComments(prLink)
22
+ const packages = parseLatestAlpha(comments)
23
+ const didUpdate = await updatePackageJsonFiles(packages)
24
+ if (didUpdate) {
25
+ if (syncpack) {
26
+ await runSyncpack()
27
+ } else {
28
+ print.yellow('Skipping syncpack')
29
+ }
30
+ await yarnInstall()
31
+ if (createPr) {
32
+ await createPullRequest(prLink)
33
+ }
34
+ }
35
+ }
36
+ process.exit(0)
37
+ }
38
+
39
+ export const fromCommand = program => {
40
+ return program
41
+ .createCommand('from', { isDefault: true })
42
+ .description('Update packages from a Pull Request')
43
+ .addArgument(
44
+ createArgument(
45
+ '<prLink>',
46
+ 'Link to a Picasso, Davinci or Topkit pull request with alpha packages generated'
47
+ )
48
+ )
49
+ .option('-C, --create-pr', 'Automatically create a PR when finished')
50
+ .option(
51
+ '-S, --no-syncpack',
52
+ 'Do not run syncpack fix after updating package.json files'
53
+ )
54
+ .action(updateFrom)
55
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import { fromCommand } from './commands/from.mjs'
2
+
3
+ export const commands = [
4
+ fromCommand,
5
+ ]
6
+
7
+ export default {
8
+ commands
9
+ }