Skip to content

Commit

Permalink
feat!: support pnpm.overrides and resolutions, remove --dev and…
Browse files Browse the repository at this point in the history
… `--prod` cli option (#92)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
patzick and antfu authored Nov 30, 2023
1 parent 9ad7f0a commit b9e8eb2
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 62 deletions.
12 changes: 0 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,6 @@ function commonOptions(args: Argv<object>): Argv<CommonOptions> {
type: 'string',
describe: 'exclude dependencies to be checked, will override --include options',
})
.option('dev', {
alias: 'D',
type: 'boolean',
describe: 'update only for devDependencies',
conflicts: ['prod'],
})
.option('prod', {
alias: 'P',
type: 'boolean',
describe: 'update only for dependencies',
conflicts: ['dev'],
})
}

// eslint-disable-next-line no-unused-expressions
Expand Down
3 changes: 1 addition & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export const DEFAULT_COMMON_OPTIONS: CommonOptions = {
ignorePaths: '',
include: '',
exclude: '',
dev: false,
prod: false,
depFields: {},
}

export const DEFAULT_USAGE_OPTIONS: UsageOptions = {
Expand Down
13 changes: 12 additions & 1 deletion src/io/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import type { DepType, RawDep, ResolvedDepChange } from '../types'

export function getByPath(obj: any, path: string) {
return path.split('.').reduce((o, i) => o?.[i], obj)
}

export function setByPath(obj: any, path: string, value: any) {
const keys = path.split('.')
const lastKey = keys.pop() as string
const target = keys.reduce((o, i) => o[i] = o[i] || {}, obj)
target[lastKey] = value
}

export function parseDependencies(pkg: any, type: DepType, shouldUpdate: (name: string) => boolean): RawDep[] {
return Object.entries(pkg[type] || {}).map(([name, version]) => parseDependency(name, version as string, type, shouldUpdate))
return Object.entries(getByPath(pkg, type) || {}).map(([name, version]) => parseDependency(name, version as string, type, shouldUpdate))
}

export function parseDependency(name: string, version: string, type: DepType, shouldUpdate: (name: string) => boolean): RawDep {
Expand Down
84 changes: 45 additions & 39 deletions src/io/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fg from 'fast-glob'
import detectIndent from 'detect-indent'
import type { CommonOptions, PackageMeta, RawDep } from '../types'
import { createDependenciesFilter } from '../utils/dependenciesFilter'
import { dumpDependencies, parseDependencies, parseDependency } from './dependencies'
import { dumpDependencies, getByPath, parseDependencies, parseDependency, setByPath } from './dependencies'

export async function readJSON(filepath: string) {
return JSON.parse(await fs.readFile(filepath, 'utf-8'))
Expand All @@ -17,58 +17,64 @@ export async function writeJSON(filepath: string, data: any) {
return await fs.writeFile(filepath, `${JSON.stringify(data, null, fileIndent)}\n`, 'utf-8')
}

const depsFields = [
'dependencies',
'devDependencies',
'optionalDependencies',
'packageManager',
'pnpm.overrides',
'resolutions',
'overrides',
] as const

export async function writePackage(pkg: PackageMeta, options: CommonOptions) {
const { raw, filepath, resolved } = pkg

let changed = false

const depKeys = [
['dependencies', !options.dev],
['devDependencies', !options.prod],
['optionalDependencies', !options.prod && !options.dev],
] as const

depKeys.forEach(([key, shouldWrite]) => {
if (raw[key] && shouldWrite) {
raw[key] = dumpDependencies(resolved, key)
changed = true
depsFields.forEach((key) => {
if (options.depFields?.[key] === false)
return
if (key === 'packageManager') {
const value = Object.entries(dumpDependencies(resolved, 'packageManager'))[0]
if (value) {
raw.packageManager = `${value[0]}@${value[1].replace('^', '')}`
changed = true
}
}
})

if (raw.packageManager) {
const value = Object.entries(dumpDependencies(resolved, 'packageManager'))[0]
if (value) {
raw.packageManager = `${value[0]}@${value[1].replace('^', '')}`
changed = true
else {
if (getByPath(raw, key)) {
setByPath(raw, key, dumpDependencies(resolved, key))
changed = true
}
}
}
})

if (changed)
await writeJSON(filepath, raw)
}

export async function loadPackage(relative: string, options: CommonOptions, shouldUpdate: (name: string) => boolean): Promise<PackageMeta> {
export async function loadPackage(
relative: string,
options: CommonOptions,
shouldUpdate: (name: string) => boolean,
): Promise<PackageMeta> {
const filepath = path.resolve(options.cwd ?? '', relative)
const raw = await readJSON(filepath)
let deps: RawDep[] = []

if (options.prod) {
deps = parseDependencies(raw, 'dependencies', shouldUpdate)
}
else if (options.dev) {
deps = parseDependencies(raw, 'devDependencies', shouldUpdate)
}
else {
deps = [
...parseDependencies(raw, 'dependencies', shouldUpdate),
...parseDependencies(raw, 'devDependencies', shouldUpdate),
...parseDependencies(raw, 'optionalDependencies', shouldUpdate),
]
}

if (raw.packageManager) {
const [name, version] = raw.packageManager.split('@')
deps.push(parseDependency(name, `^${version}`, 'packageManager', shouldUpdate))
const deps: RawDep[] = []

for (const key of depsFields) {
if (options.depFields?.[key] !== false) {
if (key === 'packageManager') {
if (raw.packageManager) {
const [name, version] = raw.packageManager.split('@')
deps.push(parseDependency(name, `^${version}`, 'packageManager', shouldUpdate))
}
}
else {
deps.push(...parseDependencies(raw, key, shouldUpdate))
}
}
}

return {
Expand Down
31 changes: 23 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import type { SortOption } from './utils/sort'

export type RangeMode = 'default' | 'major' | 'minor' | 'patch' | 'latest' | 'newest'
export type PackageMode = Omit<RangeMode, 'default'> | 'ignore'
export type DepType = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'packageManager'
export type DepType = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'packageManager' | 'pnpm.overrides' | 'resolutions' | 'overrides'

export const DependenciesTypeShortMap = {
dependencies: '',
devDependencies: 'dev',
peerDependencies: 'peer',
optionalDependencies: 'optional',
packageManager: 'package-manager',
'dependencies': '',
'devDependencies': 'dev',
'peerDependencies': 'peer',
'optionalDependencies': 'optional',
'packageManager': 'package-manager',
'pnpm.overrides': 'pnpm-overrides',
'resolutions': 'resolutions',
'overrides': 'overrides',
}

export interface RawDep {
Expand Down Expand Up @@ -49,12 +53,21 @@ export interface CommonOptions {
ignorePaths?: string | string[]
include?: string | string[]
exclude?: string | string[]
prod?: boolean
dev?: boolean
loglevel?: LogLevel
failOnOutdated?: boolean
silent?: boolean
/**
* Fields in package.json to be checked
* By default all fields will be checked
*/
depFields?: DepFieldOptions
/**
* Bypass cache
*/
force?: boolean
/**
* Override bumping mode for specific dependencies
*/
packageMode?: { [name: string]: PackageMode }
}

Expand All @@ -63,6 +76,8 @@ export interface UsageOptions extends CommonOptions {
recursive?: true
}

export type DepFieldOptions = Partial<Record<DepType, boolean>>

export interface CheckOptions extends CommonOptions {
mode?: RangeMode
write?: boolean
Expand Down
59 changes: 59 additions & 0 deletions test/dumpDependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest'
import { dumpDependencies } from '../src/io/dependencies'
import type { DepType, ResolvedDepChange } from '../src/types'

describe('dumpDependencies', () => {
function getPackageBySource(source: DepType) {
return {
name: '@types/semver',
currentVersion: '^7.3.10',
source,
update: true,
targetVersion: '^7.3.12',
diff: 'patch',
} as ResolvedDepChange
}

it('dump `dependencies` type', () => {
const dump = dumpDependencies([getPackageBySource('dependencies')], 'dependencies')
expect(dump).toMatchInlineSnapshot(`
{
"@types/semver": "^7.3.12",
}
`)
})
it('dump `devDependencies` type', () => {
const dump = dumpDependencies([getPackageBySource('devDependencies')], 'devDependencies')
expect(dump).toMatchInlineSnapshot(`
{
"@types/semver": "^7.3.12",
}
`)
})
it('dump `pnpm.overrides` type', () => {
const dump = dumpDependencies([getPackageBySource('pnpm.overrides')], 'pnpm.overrides')
expect(dump).toMatchInlineSnapshot(`
{
"@types/semver": "^7.3.12",
}
`)
})

it('dump `resolutions` type', () => {
const dump = dumpDependencies([getPackageBySource('resolutions')], 'resolutions')
expect(dump).toMatchInlineSnapshot(`
{
"@types/semver": "^7.3.12",
}
`)
})

it('dump `overrides` type', () => {
const dump = dumpDependencies([getPackageBySource('overrides')], 'overrides')
expect(dump).toMatchInlineSnapshot(`
{
"@types/semver": "^7.3.12",
}
`)
})
})
90 changes: 90 additions & 0 deletions test/parseDependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { parseDependencies } from '../src/io/dependencies'

describe('parseDependencies', () => {
it('parse package `dependencies`', () => {
const myPackage = {
name: '@taze/package1',
private: true,
dependencies: {
'@taze/not-exists': '^4.13.19',
'@typescript/lib-dom': 'npm:@types/web@^0.0.80',
},
}
const result = parseDependencies(myPackage, 'dependencies', () => true)
expect(result).toMatchInlineSnapshot(`
[
{
"currentVersion": "^4.13.19",
"name": "@taze/not-exists",
"source": "dependencies",
"update": true,
},
{
"currentVersion": "npm:@types/web@^0.0.80",
"name": "@typescript/lib-dom",
"source": "dependencies",
"update": true,
},
]
`)
})

it('parse package `devDependencies`', () => {
const myPackage = {
name: '@taze/package1',
private: true,
devDependencies: {
'@taze/not-exists': '^4.13.19',
'@typescript/lib-dom': 'npm:@types/web@^0.0.80',
},
}
const result = parseDependencies(myPackage, 'devDependencies', () => true)
expect(result).toMatchInlineSnapshot(`
[
{
"currentVersion": "^4.13.19",
"name": "@taze/not-exists",
"source": "devDependencies",
"update": true,
},
{
"currentVersion": "npm:@types/web@^0.0.80",
"name": "@typescript/lib-dom",
"source": "devDependencies",
"update": true,
},
]
`)
})

it('parse package `pnpm.overrides`', () => {
const myPackage = {
name: '@taze/package1',
private: true,
pnpm: {
overrides: {
'@taze/not-exists': '^4.13.19',
'@typescript/lib-dom': 'npm:@types/web@^0.0.80',
},
},
}
const result = parseDependencies(myPackage, 'pnpm.overrides', () => true)
expect(result).toMatchInlineSnapshot(`
[
{
"currentVersion": "^4.13.19",
"name": "@taze/not-exists",
"source": "pnpm.overrides",
"update": true,
},
{
"currentVersion": "npm:@types/web@^0.0.80",
"name": "@typescript/lib-dom",
"source": "pnpm.overrides",
"update": true,
},
]
`)
})
})

0 comments on commit b9e8eb2

Please sign in to comment.