Skip to content

Commit

Permalink
feat(cli): validate yarn.lock in 'start' and 'build' commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mediremi committed Oct 4, 2021
1 parent c99c736 commit 524767e
Show file tree
Hide file tree
Showing 8 changed files with 765 additions and 1 deletion.
4 changes: 4 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@dhis2/app-shell": "8.1.1",
"@dhis2/cli-helpers-engine": "^3.0.0",
"@jest/core": "^27.0.6",
"@yarnpkg/lockfile": "^1.1.0",
"archiver": "^3.1.1",
"axios": "^0.20.0",
"babel-jest": "^27.0.6",
Expand Down Expand Up @@ -66,5 +67,8 @@
"src/commands/test.js"
],
"testEnvironment": "node"
},
"devDependencies": {
"outdent": "^0.8.0"
}
}
14 changes: 14 additions & 0 deletions cli/src/lib/paths.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const fs = require('fs')
const path = require('path')
const { reporter } = require('@dhis2/cli-helpers-engine')

Expand All @@ -6,6 +7,18 @@ const shellSource = path.dirname(
)
const shellAppDirname = 'src/D2App'

const findYarnLock = base => {
if (base === '/') {
return null
}

const yarnLock = path.join(base, './yarn.lock')
if (fs.existsSync(yarnLock)) {
return yarnLock
}
return findYarnLock(path.dirname(base))
}

module.exports = (cwd = process.cwd()) => {
const base = path.resolve(cwd)
const paths = {
Expand All @@ -31,6 +44,7 @@ module.exports = (cwd = process.cwd()) => {

base,
package: path.join(base, './package.json'),
yarnLock: findYarnLock(base),
dotenv: path.join(base, './.env'),
config: path.join(base, './d2.config.js'),
readme: path.join(base, './README.md'),
Expand Down
9 changes: 8 additions & 1 deletion cli/src/lib/validatePackage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { reporter, chalk } = require('@dhis2/cli-helpers-engine')
const fs = require('fs-extra')
const { validateLockfile } = require('./validators/validateLockfile')
const {
validatePackageExports,
} = require('./validators/validatePackageExports')
Expand All @@ -25,5 +26,11 @@ module.exports.validatePackage = async ({

reporter.debug('Validating package...', { pkg, offerFix, noVerify })

return await validatePackageExports(pkg, { config, paths, offerFix })
const validators = [validatePackageExports, validateLockfile]
for (const validator of validators) {
if (!(await validator(pkg, { config, paths, offerFix }))) {
return false
}
}
return true
}
59 changes: 59 additions & 0 deletions cli/src/lib/validators/validateLockfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const fs = require('fs')
const { reporter, prompt } = require('@dhis2/cli-helpers-engine')
const { listDuplicates, fixDuplicates } = require('../yarnDeduplicate')

const singletonDependencies = [
'@dhis2/app-runtime',
'@dhis2/d2-i18n',
'@dhis2/ui',
'react',
'styled-jsx',
]

const listSingletonDuplicates = yarnLock =>
listDuplicates(yarnLock, {
includePackages: singletonDependencies,
singleton: true,
})

exports.validateLockfile = async (pkg, { paths, offerFix = false }) => {
if (paths.yarnLock === null) {
reporter.warn('Could not find yarn.lock')
return false
}

const yarnLock = fs.readFileSync(paths.yarnLock, 'utf8')

// Yarn v2 and above deduplicate dependencies automatically
if (!yarnLock.includes('# yarn lockfile v1')) {
return true
}

const singletonDuplicates = listSingletonDuplicates(yarnLock)
for (const [name, versions] of singletonDuplicates) {
reporter.warn(
`Found ${
versions.length
} versions of '${name}' in yarn.lock: ${versions.join(', ')}`
)
}

const valid = singletonDuplicates.size === 0
if (!valid && offerFix) {
const { fix } = await prompt({
name: 'fix',
type: 'confirm',
message:
'There are duplicate dependencies in yarn.lock, would you like to correct them now?',
})

if (!fix) {
return false
}
const dedupedYarnLock = fixDuplicates(yarnLock)
fs.writeFileSync(paths.yarnLock, dedupedYarnLock)
return listSingletonDuplicates(dedupedYarnLock).size === 0
}

return valid
}
203 changes: 203 additions & 0 deletions cli/src/lib/validators/validateLockfile.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
const fs = require('fs')
const { reporter, prompt } = require('@dhis2/cli-helpers-engine')
const { validateLockfile } = require('./validateLockfile')

jest.mock('fs')
jest.mock('@dhis2/cli-helpers-engine')

describe('validateLockfile', () => {
afterEach(() => {
jest.clearAllMocks()
})

const mockYarnLock = yarnLock => {
fs.readFileSync.mockReturnValue(yarnLock)
}

it('returns true if there are no duplicates', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"some-dependency":
version "1.0.0"
"other-dependency":
version "1.0.0"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(true)
expect(reporter.warn).toHaveBeenCalledTimes(0)
})

it('returns true if duplicates are solely for non-sensitive dependencies', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"some-dependency":
version "1.0.0"
"some-dependency":
version "1.2.0"
"some-dependency":
version "2.0.0"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(true)
expect(reporter.warn).toHaveBeenCalledTimes(0)
})

it('detects minor version duplicates of sensitive dependencies', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@dhis2/ui@6.1.2":
version "6.1.2"
"@dhis2/ui@6.1.3":
version "6.1.3"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(false)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenLastCalledWith(
`Found 2 versions of '@dhis2/ui' in yarn.lock: 6.1.2, 6.1.3`
)
})

it('detects mjaor version duplicates of sensitive dependencies', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@dhis2/ui@7.0.0":
version "7.0.0"
"@dhis2/ui@6.1.3":
version "6.1.3"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(false)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenLastCalledWith(
`Found 2 versions of '@dhis2/ui' in yarn.lock: 7.0.0, 6.1.3`
)
})

it('only outputs warnings for sensitive dependencies', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"foo@^1.4.5":
version "1.4.5"
"foo@1.2.3":
version "1.2.3"
dependencies:
"@dhis2/ui" "^6.25.0"
"@dhis2/ui@^6.23.0":
version "6.23.0"
"@dhis2/ui@^6.25.0":
version "6.25.0"
"@dhis2/ui@^7.2.0":
version "7.2.1"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(false)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenLastCalledWith(
`Found 3 versions of '@dhis2/ui' in yarn.lock: 6.23.0, 6.25.0, 7.2.1`
)
})

it('can fix lockfiles with duplicate minor versions', async () => {
mockYarnLock(`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@dhis2/ui@^6.0.0":
version "6.0.0"
"@dhis2/ui@^6.2.3":
version "6.2.3"
"@dhis2/ui@^6.1.2":
version "6.1.2"
`)

expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: false,
})
).toBe(false)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(prompt).toHaveBeenCalledTimes(0)

reporter.warn.mockClear()
prompt.mockResolvedValue({ fix: false })
expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: true,
})
).toBe(false)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(prompt).toHaveBeenCalledTimes(1)

reporter.warn.mockClear()
prompt.mockClear()
prompt.mockResolvedValue({ fix: true })
expect(
await validateLockfile(null, {
paths: { yarnLock: 'yarn.lock' },
offerFix: true,
})
).toBe(true)
expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(prompt).toHaveBeenCalledTimes(1)
expect(fs.writeFileSync).toHaveBeenCalledTimes(1)
expect(fs.writeFileSync).toHaveBeenLastCalledWith(
'yarn.lock',
`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@dhis2/ui@^6.0.0", "@dhis2/ui@^6.1.2", "@dhis2/ui@^6.2.3":
version "6.2.3"
`
)
})
})
Loading

0 comments on commit 524767e

Please sign in to comment.