@toptal/davinci-cli-shared 2.6.1 → 3.0.1-alpha-ff-79-27a22fcb.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Change Log
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#2643](https://github.com/toptal/davinci/pull/2643) [`ab9666f`](https://github.com/toptal/davinci/commit/ab9666f999e7a610f6068e0145200257d8511b46) Thanks [@rocodesign](https://github.com/rocodesign)!
8
+ Migrate package manager from Yarn to pnpm
9
+ **WHAT (breaking change):**
10
+ - switch package manager from Yarn to pnpm across the entire Davinci monorepo
11
+ - add `pnpm-workspace.yaml`, `.npmrc`, and `packageManager` field for corepack support
12
+ - convert Yarn `resolutions` to `pnpm.overrides` in root `package.json`
13
+ - update lerna `npmClient` from `yarn` to `pnpm`
14
+ - replace `yarn` with `pnpm` in root scripts (postinstall, test, test:unit:update)
15
+ **HOW to update:**
16
+ - install pnpm (`corepack enable` or `npm install -g pnpm@10.6.1`)
17
+ - replace `yarn install` with `pnpm install` in local workflows
18
+ - replace `yarn <script>` with `pnpm <script>` when running commands
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [[`ab9666f`](https://github.com/toptal/davinci/commit/ab9666f999e7a610f6068e0145200257d8511b46)]:
23
+ - @toptal/davinci-workspace-root@2.0.0
24
+
3
25
  ## 2.6.1
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toptal/davinci-cli-shared",
3
- "version": "2.6.1",
3
+ "version": "3.0.1-alpha-ff-79-27a22fcb.1+27a22fcb",
4
4
  "description": "Shared CLI code and CLI engine for davinci",
5
5
  "author": "Toptal",
6
6
  "license": "SEE LICENSE IN LICENSE.MD",
@@ -15,11 +15,11 @@
15
15
  "CHANGELOG.md"
16
16
  ],
17
17
  "scripts": {
18
- "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' yarn jest"
18
+ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' pnpm jest"
19
19
  },
20
20
  "dependencies": {
21
21
  "@segment/analytics-node": "^2.1.3",
22
- "@toptal/davinci-workspace-root": "^1.0.1",
22
+ "@toptal/davinci-workspace-root": "^2.0.1-alpha-ff-79-27a22fcb.1+27a22fcb",
23
23
  "chalk": "^4.1.2",
24
24
  "commander": "^11.0.0",
25
25
  "execa": "^5.1.1",
@@ -39,5 +39,6 @@
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
42
- }
42
+ },
43
+ "gitHead": "27a22fcb012002a99ab87a4a996721e3ba8a92c4"
43
44
  }
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import { createArgument, createOption } from 'commander'
2
2
 
3
3
  import cliPrint from './utils/print.cjs'
4
4
  import { run, runSync } from './utils/run.js'
5
+ import { resolveBin } from './utils/resolve-bin.js'
5
6
  import { confirm, password, input, checkbox } from './utils/prompt.js'
6
7
  import { convertToCLIParameters } from './utils/convert-to-cli-parameters.js'
7
8
  import davinciConfig from './utils/davinci-project-config.cjs'
@@ -39,6 +40,7 @@ export const loadCommands = (
39
40
 
40
41
  export {
41
42
  davinciProjectConfig,
43
+ resolveBin,
42
44
  run,
43
45
  runSync,
44
46
  convertToCLIParameters,
@@ -84,6 +86,7 @@ export default {
84
86
  bootstrap,
85
87
  loadCommands,
86
88
  print,
89
+ resolveBin,
87
90
  run,
88
91
  runSync,
89
92
  prompt,
@@ -17,7 +17,9 @@ export const getPackageFileContent = async (davinciPackageName, filename) => {
17
17
  }
18
18
 
19
19
  export const getPackageFilePath = (davinciPackageName, filename) => {
20
- const packageIndexPath = require.resolve(davinciPackageName)
20
+ const packageIndexPath = require.resolve(davinciPackageName, {
21
+ paths: [process.cwd()],
22
+ })
21
23
  const davinciQARoot = path.join(packageIndexPath, '../..')
22
24
 
23
25
  return path.join(davinciQARoot, filename)
@@ -31,7 +33,7 @@ export const getProjectRootFilePath = filename => {
31
33
 
32
34
  export const projectHasPackage = pkg => {
33
35
  try {
34
- require.resolve(pkg)
36
+ require.resolve(pkg, { paths: [process.cwd()] })
35
37
 
36
38
  return true
37
39
  } catch {
@@ -1,20 +1,25 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { createRequire } from 'module'
1
4
  import semver from 'semver'
2
5
  import readPkgUp from 'read-pkg-up'
3
6
 
4
- import yarnAdapter from './yarn-adapter.js'
5
-
6
7
  export const getPackagePackageJson = cwd => {
7
8
  return readPkgUp.sync({ cwd }).packageJson
8
9
  }
9
10
 
10
11
  const getInstalledPackageVersion = packageName => {
11
- const packageInfoList = yarnAdapter.list(packageName)
12
+ const requireFromCwd = createRequire(path.join(process.cwd(), 'package.json'))
13
+
14
+ try {
15
+ const packageEntryPath = requireFromCwd.resolve(packageName)
16
+ const packageJsonPath = readPkgUp.sync({ cwd: packageEntryPath }).path
17
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
12
18
 
13
- if (!packageInfoList.size) {
19
+ return packageJson.version
20
+ } catch {
14
21
  return null
15
22
  }
16
-
17
- return packageInfoList.get(packageName)
18
23
  }
19
24
 
20
25
  export const checkPrerequirement = ({ dependencies, packageName }) => {
@@ -1,23 +1,20 @@
1
- import { jest } from '@jest/globals'
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
2
4
 
3
5
  import { checkPrerequirement } from './package.js'
4
- import yarnAdapter from './yarn-adapter.js'
5
-
6
- const yarnAdapterListMock = jest.spyOn(yarnAdapter, 'list')
7
6
 
8
7
  const packageName = '@toptal/davinci-engine'
9
8
  const packageValidVersions = [
10
9
  '4.0.0',
11
- '4',
12
- '4.4',
10
+ '4.4.0',
11
+ '5.6.7',
13
12
  '4.1.13-alpha-FX-1234-my-branch.1',
14
- '^3',
15
- '~5.6.7',
16
13
  ]
17
14
  const packageInvalidVersions = [
18
15
  '2.0.0',
19
- '6',
20
- '1.4',
16
+ '6.0.0',
17
+ '1.4.0',
21
18
  '1.1.13-alpha-FX-1234-my-branch.1',
22
19
  ]
23
20
  const validVersionRange = '3 - 5'
@@ -25,11 +22,54 @@ const packageJsonDependencies = {
25
22
  [packageName]: validVersionRange,
26
23
  }
27
24
 
25
+ const createProjectPackageJson = tempDir => {
26
+ fs.writeFileSync(
27
+ path.join(tempDir, 'package.json'),
28
+ JSON.stringify({
29
+ name: 'test-project',
30
+ private: true,
31
+ version: '1.0.0',
32
+ })
33
+ )
34
+ }
35
+
36
+ const installPackage = (tempDir, version) => {
37
+ const packageDir = path.join(
38
+ tempDir,
39
+ 'node_modules',
40
+ ...packageName.split('/')
41
+ )
42
+
43
+ fs.mkdirSync(packageDir, { recursive: true })
44
+ fs.writeFileSync(
45
+ path.join(packageDir, 'package.json'),
46
+ JSON.stringify({
47
+ name: packageName,
48
+ main: 'index.js',
49
+ version,
50
+ })
51
+ )
52
+ fs.writeFileSync(path.join(packageDir, 'index.js'), 'module.exports = {}')
53
+ }
54
+
28
55
  describe('package utils', () => {
56
+ let initialCwd
57
+ let tempDir
58
+
59
+ beforeEach(() => {
60
+ initialCwd = process.cwd()
61
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'davinci-package-test-'))
62
+ createProjectPackageJson(tempDir)
63
+ process.chdir(tempDir)
64
+ })
65
+
66
+ afterEach(() => {
67
+ process.chdir(initialCwd)
68
+ fs.rmSync(tempDir, { force: true, recursive: true })
69
+ })
70
+
29
71
  describe('checkPrerequirement', () => {
30
72
  it('should return an error when package is not installed', () => {
31
- yarnAdapterListMock.mockReturnValueOnce(new Map())
32
-
33
73
  const result = checkPrerequirement({
34
74
  dependencies: packageJsonDependencies,
35
75
  packageName,
@@ -41,9 +81,7 @@ describe('package utils', () => {
41
81
  it.each(packageValidVersions)(
42
82
  `should return no errors for version %s in range ${validVersionRange}`,
43
83
  packageValidVersion => {
44
- yarnAdapterListMock.mockReturnValueOnce(
45
- new Map([[packageName, packageValidVersion]])
46
- )
84
+ installPackage(tempDir, packageValidVersion)
47
85
 
48
86
  const result = checkPrerequirement({
49
87
  dependencies: packageJsonDependencies,
@@ -57,9 +95,7 @@ describe('package utils', () => {
57
95
  it.each(packageInvalidVersions)(
58
96
  `should return an error for version %s in range ${validVersionRange}`,
59
97
  packageInvalidVersion => {
60
- yarnAdapterListMock.mockReturnValueOnce(
61
- new Map([[packageName, packageInvalidVersion]])
62
- )
98
+ installPackage(tempDir, packageInvalidVersion)
63
99
 
64
100
  const result = checkPrerequirement({
65
101
  dependencies: packageJsonDependencies,
@@ -0,0 +1,61 @@
1
+ import { createRequire } from 'module'
2
+ import path from 'path'
3
+ import { readFileSync } from 'fs'
4
+
5
+ /**
6
+ * Resolves a binary from a package's own dependencies.
7
+ *
8
+ * Under pnpm, transitive dependency binaries are not hoisted to the
9
+ * consumer's node_modules/.bin. This function uses the caller's
10
+ * import.meta.url so that require.resolve looks up from the Davinci
11
+ * package that actually declares the dependency, not from the consumer.
12
+ *
13
+ * @param {string} packageName npm package that owns the binary (e.g. 'eslint', '@sentry/cli')
14
+ * @param {string} callerUrl pass import.meta.url from the calling module
15
+ * @param {string} [binName] name of the binary when it differs from the package's unscoped name
16
+ * @returns {string} absolute path to the binary
17
+ */
18
+ export const resolveBin = (packageName, callerUrl, binName) => {
19
+ const req = createRequire(callerUrl)
20
+
21
+ let pkgJsonPath
22
+
23
+ try {
24
+ pkgJsonPath = req.resolve(`${packageName}/package.json`)
25
+ } catch {
26
+ // Some packages (e.g. lerna) have an exports map that doesn't expose
27
+ // package.json. Fall back to resolving the main entry and walking up.
28
+ const mainEntry = req.resolve(packageName)
29
+ const nodeModulesSegment = `node_modules/${packageName}`
30
+ const idx = mainEntry.lastIndexOf(nodeModulesSegment)
31
+
32
+ pkgJsonPath = path.join(
33
+ mainEntry.slice(0, idx + nodeModulesSegment.length),
34
+ 'package.json'
35
+ )
36
+ }
37
+
38
+ const pkgDir = path.dirname(pkgJsonPath)
39
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
40
+
41
+ const target = binName || packageName.split('/').pop()
42
+ const binField = pkgJson.bin
43
+
44
+ let binRelPath
45
+
46
+ if (typeof binField === 'string') {
47
+ binRelPath = binField
48
+ } else if (binField && binField[target]) {
49
+ binRelPath = binField[target]
50
+ } else if (binField) {
51
+ binRelPath = Object.values(binField)[0]
52
+ }
53
+
54
+ if (!binRelPath) {
55
+ throw new Error(
56
+ `Cannot resolve bin "${target}" from package "${packageName}"`
57
+ )
58
+ }
59
+
60
+ return path.resolve(pkgDir, binRelPath)
61
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'path'
2
+ import { fileURLToPath, pathToFileURL } from 'url'
3
+
4
+ import { resolveBin } from './resolve-bin.js'
5
+
6
+ // Build a caller URL that resolves from @toptal/davinci-monorepo's context
7
+ // Use direct path instead of require.resolve since pnpm doesn't hoist workspace siblings
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const monorepoDir = path.resolve(__dirname, '../../../monorepo')
10
+ const monorepoCallerUrl = pathToFileURL(
11
+ path.join(monorepoDir, 'src/index.js')
12
+ ).href
13
+
14
+ describe('resolveBin', () => {
15
+ describe('when package exposes package.json in exports', () => {
16
+ it('resolves the binary path for a package with object bin field', () => {
17
+ // semver is a dependency of cli-shared and has bin: { semver: "bin/semver.js" }
18
+ const result = resolveBin('semver', import.meta.url)
19
+
20
+ expect(result).toContain('semver')
21
+ expect(result).toMatch(/bin\/semver\.js$/)
22
+ })
23
+ })
24
+
25
+ describe('when package does not expose package.json in exports', () => {
26
+ it('falls back to resolving via main entry', () => {
27
+ // lerna has an exports map that blocks package.json access
28
+ const result = resolveBin('lerna', monorepoCallerUrl)
29
+
30
+ expect(result).toContain('lerna')
31
+ expect(result).toMatch(/cli\.js$/)
32
+ })
33
+ })
34
+
35
+ describe('when custom binName is provided', () => {
36
+ it('resolves the correct binary', () => {
37
+ const result = resolveBin('npm-check-updates', monorepoCallerUrl, 'ncu')
38
+
39
+ expect(result).toContain('npm-check-updates')
40
+ expect(result).toMatch(/cli\.js$/)
41
+ })
42
+ })
43
+
44
+ describe('when package is not installed', () => {
45
+ it('throws an error', () => {
46
+ expect(() =>
47
+ resolveBin('nonexistent-package-xyz', import.meta.url)
48
+ ).toThrow()
49
+ })
50
+ })
51
+
52
+ describe('when package has no bin field', () => {
53
+ it('throws with a descriptive error', () => {
54
+ // ramda has no bin field
55
+ expect(() => resolveBin('ramda', monorepoCallerUrl)).toThrow(
56
+ /Cannot resolve bin/
57
+ )
58
+ })
59
+ })
60
+ })
@@ -1,47 +0,0 @@
1
- import { runSync } from './run.js'
2
-
3
- /**
4
- *
5
- * @param {string} treeName
6
- * @example '@toptal/davinci-engine@7.5.2'
7
- * @returns {Array} Pair package name and version, ex. ['@toptal/davinci-engine', '16.0.2']
8
- */
9
- const splitNameAndVersion = treeName => {
10
- const atPos = treeName.lastIndexOf('@')
11
-
12
- return [treeName.slice(0, atPos), treeName.slice(atPos + 1)]
13
- }
14
-
15
- export const createList = syncRun => packageName => {
16
- const { stdout } = syncRun(
17
- 'yarn',
18
- [
19
- 'list',
20
- '--json',
21
- '--no-progress',
22
- '--non-interactive',
23
- '--depth=0',
24
- '--pattern',
25
- packageName,
26
- ],
27
- {
28
- stdio: undefined,
29
- }
30
- )
31
-
32
- const listInfo = JSON.parse(stdout)
33
- const packagesFound = listInfo?.data?.trees || []
34
-
35
- if (packagesFound.length <= 0) {
36
- return new Map()
37
- }
38
-
39
- return new Map(packagesFound.map(pkg => splitNameAndVersion(pkg.name)))
40
- }
41
-
42
- export const list = createList(runSync)
43
-
44
- export default {
45
- list,
46
- createList,
47
- }
@@ -1,58 +0,0 @@
1
- import { jest } from '@jest/globals'
2
-
3
- import { createList } from './yarn-adapter.js'
4
- import run from './run.js'
5
-
6
- const packageName = 'find-up'
7
- const packageVersion = '5.0.0'
8
- const existingPackageResponseMock = `{"type":"tree","data":{"type":"list","trees":[{"name":"${packageName}@${packageVersion}","children":[],"hint":null,"color":null,"depth":0}]}}`
9
- const nonExistingPackageResponseMock = `{"type":"tree","data":{"type":"list","trees":[]}}`
10
- const invalidResponseMock = 'invalid-response'
11
-
12
- const runSyncMock = jest.spyOn(run, 'runSync')
13
- const yarnListInjected = createList(runSyncMock)
14
-
15
- describe('yarnAdapter', () => {
16
- describe('list', () => {
17
- it('should return a version for the existing package', () => {
18
- runSyncMock.mockReturnValueOnce({
19
- stdout: existingPackageResponseMock,
20
- })
21
-
22
- const result = yarnListInjected(packageName)
23
-
24
- expect(result.size).toBe(1)
25
- expect(result.get(packageName)).toBe(packageVersion)
26
- })
27
-
28
- it('should return empty map for non-existing package', () => {
29
- runSyncMock.mockImplementationOnce(() => ({
30
- stdout: nonExistingPackageResponseMock,
31
- }))
32
-
33
- const result = yarnListInjected(packageName)
34
-
35
- expect(result.size).toBe(0)
36
- })
37
-
38
- it('should throw an error for invalid response', () => {
39
- runSyncMock.mockReturnValueOnce({
40
- stdout: invalidResponseMock,
41
- })
42
-
43
- expect(() => {
44
- yarnListInjected(packageName)
45
- }).toThrow()
46
- })
47
-
48
- it('should throw an error for empty response', () => {
49
- runSyncMock.mockReturnValueOnce({
50
- stdout: '',
51
- })
52
-
53
- expect(() => {
54
- yarnListInjected(packageName)
55
- }).toThrow()
56
- })
57
- })
58
- })