diff --git a/README.md b/README.md index 7eee4ee..2976c48 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,10 @@ export default defineConfig({ 'unocss': 'ignore', // regex starts and ends with '/' '/vue/': 'latest' + }, + // disable checking for "overrides" package.json field + depFields: { + overrides: false } }) ``` diff --git a/src/io/dependencies.ts b/src/io/dependencies.ts index b31d282..e5a2d30 100644 --- a/src/io/dependencies.ts +++ b/src/io/dependencies.ts @@ -1,7 +1,23 @@ import type { DepType, RawDep, ResolvedDepChange } from '../types' +interface FlattenPkgData { [key: string]: { version: string, parents: string[] } } + +function flatten(obj: any, parents: string[] = []): FlattenPkgData { + if (!obj) + return obj + + let flattenData: FlattenPkgData = {} + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'object') + flattenData = { ...flattenData, ...flatten(value, [...parents, key]) } + else if (typeof value === 'string') + flattenData[key] = { version: value, parents } + } + return flattenData +} + export function getByPath(obj: any, path: string) { - return path.split('.').reduce((o, i) => o?.[i], obj) + return flatten(path.split('.').reduce((o, i) => o?.[i], obj)) } export function setByPath(obj: any, path: string, value: any) { @@ -12,13 +28,14 @@ export function setByPath(obj: any, path: string, value: any) { } export function parseDependencies(pkg: any, type: DepType, shouldUpdate: (name: string) => boolean): RawDep[] { - return Object.entries(getByPath(pkg, type) || {}).map(([name, version]) => parseDependency(name, version as string, type, shouldUpdate)) + return Object.entries(getByPath(pkg, type) || {}).map(([name, { version, parents }]) => parseDependency(name, version, type, shouldUpdate, parents)) } -export function parseDependency(name: string, version: string, type: DepType, shouldUpdate: (name: string) => boolean): RawDep { +export function parseDependency(name: string, version: string, type: DepType, shouldUpdate: (name: string) => boolean, parents?: string[]): RawDep { return { name, currentVersion: version, + parents, source: type, // when `updated` marked to `false`, it will be bypassed on resolving update: shouldUpdate(name), @@ -26,16 +43,24 @@ export function parseDependency(name: string, version: string, type: DepType, sh } export function dumpDependencies(deps: ResolvedDepChange[], type: DepType) { - const data: Record = {} + const data: Record = {} deps .filter(i => i.source === type) .sort((a, b) => a.name.localeCompare(b.name)) .forEach((i) => { const version = i.update ? i.targetVersion : i.currentVersion + let targetLeaf = data + + i.parents?.reduce((tree, parent) => { + tree[parent] ??= {} + targetLeaf = tree[parent] + return tree[parent] + }, data) + if (i.aliasName === undefined) - data[i.name] = version + targetLeaf[i.name] = version else - data[i.aliasName] = `npm:${i.name}${version ? `@${version}` : ''}` + targetLeaf[i.aliasName] = `npm:${i.name}${version ? `@${version}` : ''}` }) return data diff --git a/src/io/resolves.ts b/src/io/resolves.ts index 61153ff..bdef4ff 100644 --- a/src/io/resolves.ts +++ b/src/io/resolves.ts @@ -9,6 +9,7 @@ import type { CheckOptions, DependencyFilter, DependencyResolvedCallback, DiffTy import { diffSorter } from '../filters/diff-sorter' import { getMaxSatisfying, getPrefixedVersion } from '../utils/versions' import { getPackageMode } from '../utils/config' +import { parsePnpmPackagePath, parseYarnPackagePath } from '../utils/package' const debug = { cache: _debug('taze:cache'), @@ -207,7 +208,20 @@ export async function resolveDependency( } } - const pkgData = await getPackageData(dep.name) + let resolvedName = dep.name + + // manage Yarn resolutions (e.g. "foo@1/bar") + if (dep.source === 'resolutions') { + const packages = parseYarnPackagePath(dep.name) + resolvedName = packages.pop() ?? dep.name + } + // manage pnpm overrides (e.g. "foo@1>bar") + else if (dep.source === 'pnpm.overrides') { + const packages = parsePnpmPackagePath(dep.name) + resolvedName = packages.pop() ?? dep.name + } + + const pkgData = await getPackageData(resolvedName) const { tags, error } = pkgData dep.pkgData = pkgData let err: Error | string | null = null diff --git a/src/types.ts b/src/types.ts index 18dcdd8..e341e4d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface RawDep { currentVersion: string source: DepType update: boolean + parents?: string[] } export type DiffType = 'major' | 'minor' | 'patch' | 'error' | null diff --git a/src/utils/package.ts b/src/utils/package.ts new file mode 100644 index 0000000..1e32f2b --- /dev/null +++ b/src/utils/package.ts @@ -0,0 +1,13 @@ +/** + * Parse input string like `package-1/package-2` to an array of packages + */ +export function parseYarnPackagePath(input: string): string[] { + return input.match(/(@[^\/]+\/)?([^/]+)/g) || [] +} + +/** + * Parse input string like `package-1>package-2` to an array of packages + */ +export function parsePnpmPackagePath(input: string): string[] { + return input.match(/[^>]+/g) || [] +} diff --git a/test/dumpDependencies.test.ts b/test/dumpDependencies.test.ts index 4093be5..c307c47 100644 --- a/test/dumpDependencies.test.ts +++ b/test/dumpDependencies.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest' import { dumpDependencies } from '../src/io/dependencies' -import type { DepType, ResolvedDepChange } from '../src/types' +import type { ResolvedDepChange } from '../src/types' describe('dumpDependencies', () => { - function getPackageBySource(source: DepType) { + function makeDepChange(source: string, override: Partial = {}) { return { name: '@types/semver', currentVersion: '^7.3.10', @@ -11,11 +11,12 @@ describe('dumpDependencies', () => { update: true, targetVersion: '^7.3.12', diff: 'patch', + ...override, } as ResolvedDepChange } it('dump `dependencies` type', () => { - const dump = dumpDependencies([getPackageBySource('dependencies')], 'dependencies') + const dump = dumpDependencies([makeDepChange('dependencies')], 'dependencies') expect(dump).toMatchInlineSnapshot(` { "@types/semver": "^7.3.12", @@ -23,7 +24,7 @@ describe('dumpDependencies', () => { `) }) it('dump `devDependencies` type', () => { - const dump = dumpDependencies([getPackageBySource('devDependencies')], 'devDependencies') + const dump = dumpDependencies([makeDepChange('devDependencies')], 'devDependencies') expect(dump).toMatchInlineSnapshot(` { "@types/semver": "^7.3.12", @@ -31,7 +32,7 @@ describe('dumpDependencies', () => { `) }) it('dump `pnpm.overrides` type', () => { - const dump = dumpDependencies([getPackageBySource('pnpm.overrides')], 'pnpm.overrides') + const dump = dumpDependencies([makeDepChange('pnpm.overrides')], 'pnpm.overrides') expect(dump).toMatchInlineSnapshot(` { "@types/semver": "^7.3.12", @@ -40,19 +41,51 @@ describe('dumpDependencies', () => { }) it('dump `resolutions` type', () => { - const dump = dumpDependencies([getPackageBySource('resolutions')], 'resolutions') + const dump = dumpDependencies([ + makeDepChange('resolutions'), + makeDepChange('resolutions', { + name: '@taze/pkg/@taze/nested-foo', + targetVersion: '^1.0.0', + }), + makeDepChange('resolutions', { + name: '@taze/pkg/@taze/nested-bar@2.0.0', + targetVersion: '^2.0.0', + }), + ], 'resolutions') expect(dump).toMatchInlineSnapshot(` { + "@taze/pkg/@taze/nested-bar@2.0.0": "^2.0.0", + "@taze/pkg/@taze/nested-foo": "^1.0.0", "@types/semver": "^7.3.12", } `) }) it('dump `overrides` type', () => { - const dump = dumpDependencies([getPackageBySource('overrides')], 'overrides') + const dump = dumpDependencies([ + makeDepChange('overrides', { + name: '@taze/nested-foo', + parents: ['@taze/pkg'], + targetVersion: '^1.0.0', + }), + makeDepChange('overrides', { + name: '@taze/nested-lvl2', + targetVersion: '^2.0.0', + parents: [ + '@taze/pkg', + '@taze/nested-bar', + ], + }), + ], 'overrides') + expect(dump).toMatchInlineSnapshot(` { - "@types/semver": "^7.3.12", + "@taze/pkg": { + "@taze/nested-bar": { + "@taze/nested-lvl2": "^2.0.0", + }, + "@taze/nested-foo": "^1.0.0", + }, } `) }) diff --git a/test/parseDependencies.test.ts b/test/parseDependencies.test.ts index 514db41..4ea1340 100644 --- a/test/parseDependencies.test.ts +++ b/test/parseDependencies.test.ts @@ -17,12 +17,14 @@ describe('parseDependencies', () => { { "currentVersion": "^4.13.19", "name": "@taze/not-exists", + "parents": [], "source": "dependencies", "update": true, }, { "currentVersion": "npm:@types/web@^0.0.80", "name": "@typescript/lib-dom", + "parents": [], "source": "dependencies", "update": true, }, @@ -45,12 +47,14 @@ describe('parseDependencies', () => { { "currentVersion": "^4.13.19", "name": "@taze/not-exists", + "parents": [], "source": "devDependencies", "update": true, }, { "currentVersion": "npm:@types/web@^0.0.80", "name": "@typescript/lib-dom", + "parents": [], "source": "devDependencies", "update": true, }, @@ -75,16 +79,119 @@ describe('parseDependencies', () => { { "currentVersion": "^4.13.19", "name": "@taze/not-exists", + "parents": [], "source": "pnpm.overrides", "update": true, }, { "currentVersion": "npm:@types/web@^0.0.80", "name": "@typescript/lib-dom", + "parents": [], "source": "pnpm.overrides", "update": true, }, ] `) }) + + it('parse package `resolutions`', () => { + const myPackage = { + name: '@taze/package1', + private: true, + resolutions: { + '@taze/not-exists': '^4.13.19', + '@typescript/lib-dom': 'npm:@types/web@^0.0.80', + '@taze/pkg/@taze/nested-foo': '^1.0.0', + '@taze/pkg/@taze/nested-foo@2.0.0': '^1.0.0', + }, + } + const result = parseDependencies(myPackage, 'resolutions', () => true) + expect(result).toMatchInlineSnapshot(` + [ + { + "currentVersion": "^4.13.19", + "name": "@taze/not-exists", + "parents": [], + "source": "resolutions", + "update": true, + }, + { + "currentVersion": "npm:@types/web@^0.0.80", + "name": "@typescript/lib-dom", + "parents": [], + "source": "resolutions", + "update": true, + }, + { + "currentVersion": "^1.0.0", + "name": "@taze/pkg/@taze/nested-foo", + "parents": [], + "source": "resolutions", + "update": true, + }, + { + "currentVersion": "^1.0.0", + "name": "@taze/pkg/@taze/nested-foo@2.0.0", + "parents": [], + "source": "resolutions", + "update": true, + }, + ] + `) + }) + + it('parse package `overrides`', () => { + const myPackage = { + name: '@taze/package1', + private: true, + overrides: { + '@taze/not-exists': '^4.13.19', + '@typescript/lib-dom': 'npm:@types/web@^0.0.80', + '@taze/pkg': { + '@taze/nested-foo': '^1.0.0', + '@taze/nested-bar': { + '@taze/nested-lvl2': 'npm:@taze/override', + }, + }, + }, + } + const result = parseDependencies(myPackage, 'overrides', () => true) + expect(result).toMatchInlineSnapshot(` + [ + { + "currentVersion": "^4.13.19", + "name": "@taze/not-exists", + "parents": [], + "source": "overrides", + "update": true, + }, + { + "currentVersion": "npm:@types/web@^0.0.80", + "name": "@typescript/lib-dom", + "parents": [], + "source": "overrides", + "update": true, + }, + { + "currentVersion": "^1.0.0", + "name": "@taze/nested-foo", + "parents": [ + "@taze/pkg", + ], + "source": "overrides", + "update": true, + }, + { + "currentVersion": "npm:@taze/override", + "name": "@taze/nested-lvl2", + "parents": [ + "@taze/pkg", + "@taze/nested-bar", + ], + "source": "overrides", + "update": true, + }, + ] + `) + }) }) diff --git a/test/resolves.test.ts b/test/resolves.test.ts index c680d6f..744bbf9 100644 --- a/test/resolves.test.ts +++ b/test/resolves.test.ts @@ -27,6 +27,26 @@ function makeLocalPkg(ver: string): RawDep { return pkg } +function makePkgForResolutions(name: string, ver: string): RawDep { + const pkg: RawDep = { + name, + currentVersion: ver, + source: 'resolutions', + update: true, + } + return pkg +} + +function makePkgForPnpmOverrides(name: string, ver: string): RawDep { + const pkg: RawDep = { + name, + currentVersion: ver, + source: 'pnpm.overrides', + update: true, + } + return pkg +} + const options: CheckOptions = { cwd: process.cwd(), loglevel: 'silent', @@ -104,6 +124,22 @@ it('resolveDependency', async () => { expect(false).toBe((await resolveDependency(makeLocalPkg('workspace:*'), options, filter)).update) const target = await resolveDependency(makeLocalPkg('1.0.0'), options, filter) expect(target.resolveError).not.toBeNull() + + // yarn resolutions + expect(true).toBe((await resolveDependency(makePkgForResolutions('typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('typescript@5.0.0', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('typescript', 'npm:typescript@^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('foo/typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('foo/**/typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('**/typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('@foo/bar/typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForResolutions('@foo/bar/typescript@5.1.0', '^4.0.0'), options, filter)).update) + + // pnpm overrides + expect(true).toBe((await resolveDependency(makePkgForPnpmOverrides('typescript', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForPnpmOverrides('typescript', 'npm:typescript@^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForPnpmOverrides('typescript@5.0.0', '^4.0.0'), options, filter)).update) + expect(true).toBe((await resolveDependency(makePkgForPnpmOverrides('foo@1>typescript', '^4.0.0'), options, filter)).update) }, 10000) it('getDiff', () => {