initial implementation
parent
c09114355f
commit
20e38492c9
|
@ -0,0 +1,14 @@
|
|||
const { npmResolver } = require('./src/npm')
|
||||
|
||||
module.exports = {
|
||||
name: 'my_project',
|
||||
scm: {
|
||||
|
||||
},
|
||||
resolver: {
|
||||
npm: npmResolver()
|
||||
},
|
||||
publisher: {
|
||||
npm: npmPublisher
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
const { npmResolver, npmPublisher } = require('../npm')
|
||||
module.exports = {
|
||||
name: 'my_project',
|
||||
scm: {
|
||||
url: 'https://some-git.com'
|
||||
},
|
||||
resolver: {
|
||||
npm: npmResolver({})
|
||||
},
|
||||
publisher: {
|
||||
npm: npmPublisher({})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { compareVersions, parseSemver } from '../helpers'
|
||||
import { VersionInfo } from '../resolver'
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('parseSemver()', () => {
|
||||
|
||||
it('throws error for invalid semver', () => {
|
||||
expect(() => parseSemver('aa.12.2')).toThrow()
|
||||
})
|
||||
|
||||
it('correctly parses simple semver', () => {
|
||||
expect(parseSemver('12.56.2')).toEqual({
|
||||
version: '12.56.2',
|
||||
major: 12,
|
||||
minor: 56,
|
||||
patch: 2
|
||||
} as VersionInfo)
|
||||
})
|
||||
|
||||
it('correctly parses semver with prerelease', () => {
|
||||
expect(parseSemver('12.56.2-beta3')).toEqual({
|
||||
version: '12.56.2-beta3',
|
||||
major: 12,
|
||||
minor: 56,
|
||||
patch: 2,
|
||||
prerelease: 'beta3'
|
||||
} as VersionInfo)
|
||||
})
|
||||
|
||||
it('correctly parses semver with build', () => {
|
||||
expect(parseSemver('12.56.2+b1')).toEqual({
|
||||
version: '12.56.2+b1',
|
||||
major: 12,
|
||||
minor: 56,
|
||||
patch: 2,
|
||||
build: 'b1',
|
||||
} as VersionInfo)
|
||||
})
|
||||
|
||||
it('correctly parses semver with prerelease and build', () => {
|
||||
expect(parseSemver('12.56.2-beta3+b1')).toEqual({
|
||||
version: '12.56.2-beta3+b1',
|
||||
major: 12,
|
||||
minor: 56,
|
||||
patch: 2,
|
||||
prerelease: 'beta3',
|
||||
build: 'b1',
|
||||
} as VersionInfo)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compareVersions()', () => {
|
||||
it('returns major for different major versions', () => {
|
||||
const versionA = parseSemver('1.0.0')
|
||||
const versionB = parseSemver('2.0.0')
|
||||
expect(compareVersions(versionA, versionB)).toBe('major')
|
||||
})
|
||||
it('returns minor for different minor versions', () => {
|
||||
const versionA = parseSemver('1.0.0')
|
||||
const versionB = parseSemver('1.1.0')
|
||||
expect(compareVersions(versionA, versionB)).toBe('minor')
|
||||
})
|
||||
it('returns patch for different patch versions', () => {
|
||||
const versionA = parseSemver('1.0.0')
|
||||
const versionB = parseSemver('1.0.1')
|
||||
expect(compareVersions(versionA, versionB)).toBe('patch')
|
||||
})
|
||||
it('returns prerelease for different prerelease versions', () => {
|
||||
const versionA = parseSemver('1.0.0-beta1')
|
||||
const versionB = parseSemver('1.0.0-beta2')
|
||||
expect(compareVersions(versionA, versionB)).toBe('prerelease')
|
||||
})
|
||||
it('returns build for different build versions', () => {
|
||||
const versionA = parseSemver('1.0.0+b1')
|
||||
const versionB = parseSemver('1.0.0+b2')
|
||||
expect(compareVersions(versionA, versionB)).toBe('build')
|
||||
})
|
||||
it('returns null for same versions', () => {
|
||||
const versionA = parseSemver('1.0.0')
|
||||
const versionB = parseSemver('1.0.0')
|
||||
expect(compareVersions(versionA, versionB)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import { Project, ProjectRunner, ScmInfo } from '../project'
|
||||
|
||||
describe('ProjectLoader', () => {
|
||||
const newLoader = () => {
|
||||
return new ProjectRunner()
|
||||
}
|
||||
const newProjectOptions = (override?: Partial<Project>): Project => {
|
||||
return {
|
||||
name: 'my_project',
|
||||
scm: {
|
||||
url: 'https://some-git.com',
|
||||
type: 'git',
|
||||
},
|
||||
publisher: {},
|
||||
resolver: {},
|
||||
...override,
|
||||
}
|
||||
}
|
||||
|
||||
describe('validateProject()', () => {
|
||||
it('throws error for missing name', () => {
|
||||
const loader = newLoader()
|
||||
expect(() => loader.validateProject(newProjectOptions({ name: undefined })))
|
||||
.toThrow(/Missing project name/)
|
||||
})
|
||||
it('throws error for missing scm info', () => {
|
||||
const loader = newLoader()
|
||||
expect(() => loader.validateProject(newProjectOptions({ scm: undefined })))
|
||||
.toThrow(/Missing scm/)
|
||||
})
|
||||
it('throws error for invalid scm', () => {
|
||||
const loader = newLoader()
|
||||
expect(() => loader.validateProject(newProjectOptions({ scm: { type: 'git' } as ScmInfo })))
|
||||
.toThrow(/Missing scm url/)
|
||||
})
|
||||
it('does not throw error for missing scm type', () => {
|
||||
const loader = newLoader()
|
||||
expect(() => loader.validateProject(newProjectOptions({ scm: { url: 'https://some-git.com' } })))
|
||||
.not.toThrow()
|
||||
})
|
||||
})
|
||||
describe('loadProject()', () => {
|
||||
it('loads project from js file', () => {
|
||||
const runner = newLoader()
|
||||
runner.loadProject(__dirname + '/deptracker.config.js')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
import { VersionChangeType, VersionInfo } from './resolver'
|
||||
|
||||
export const parseSemver = (version: string): VersionInfo => {
|
||||
const regex = /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?/gi
|
||||
const match = regex.exec(version)
|
||||
if (!match) {
|
||||
throw Error(`Invalid semver '${version}'`)
|
||||
}
|
||||
const [, major, minor, patch, prerelease, build] = match
|
||||
|
||||
return {
|
||||
version,
|
||||
major: parseInt(major),
|
||||
minor: parseInt(minor),
|
||||
patch: parseInt(patch),
|
||||
prerelease,
|
||||
build,
|
||||
}
|
||||
}
|
||||
|
||||
export const compareVersions = (versionA: VersionInfo, versionB: VersionInfo): VersionChangeType | null => {
|
||||
if (versionA.major !== versionB.major) {
|
||||
return 'major'
|
||||
}
|
||||
if (versionA.minor !== versionB.minor) {
|
||||
return 'minor'
|
||||
}
|
||||
if (versionA.patch !== versionB.patch) {
|
||||
return 'patch'
|
||||
}
|
||||
if (versionA.prerelease !== versionB.prerelease) {
|
||||
return 'prerelease'
|
||||
}
|
||||
if (versionA.build !== versionB.build) {
|
||||
return 'build'
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { parseOutdatedLibraries } from '../process-yarn'
|
||||
import { ResolveResult } from '../../resolver'
|
||||
|
||||
describe('process-yarn', () => {
|
||||
describe('parseOutdatedLibraries()', () => {
|
||||
it('contains correct library for input', () => {
|
||||
const input = `
|
||||
yarn outdated v1.22.19
|
||||
info Color legend :
|
||||
"<red>" : Major Update backward-incompatible updates
|
||||
"<yellow>" : Minor Update backward-compatible features
|
||||
"<green>" : Patch Update backward-compatible bug fixes
|
||||
Package Current Wanted Latest Package Type URL
|
||||
@types/jest 23.3.14 23.3.14 29.2.3 devDependencies https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest
|
||||
Done in 12.56s.
|
||||
`
|
||||
expect(parseOutdatedLibraries(input, 'package.json')).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
current: {
|
||||
name: '@types/jest',
|
||||
type: 'npm',
|
||||
version: {
|
||||
version: '23.3.14',
|
||||
major: 23,
|
||||
minor: 3,
|
||||
patch: 14,
|
||||
},
|
||||
details: {
|
||||
package_type: 'devDependencies',
|
||||
url: 'https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest',
|
||||
},
|
||||
location: 'package.json',
|
||||
},
|
||||
recommended: {
|
||||
name: '@types/jest',
|
||||
type: 'npm',
|
||||
version: {
|
||||
version: '29.2.3',
|
||||
major: 29,
|
||||
minor: 2,
|
||||
patch: 3,
|
||||
},
|
||||
details: {
|
||||
package_type: 'devDependencies',
|
||||
url: 'https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest',
|
||||
|
||||
},
|
||||
location: 'package.json',
|
||||
},
|
||||
change_type: 'major',
|
||||
} as ResolveResult,
|
||||
]),
|
||||
)
|
||||
})
|
||||
it('returns correct number of results', () => {
|
||||
const input = `
|
||||
yarn outdated v1.22.19
|
||||
info Color legend :
|
||||
"<red>" : Major Update backward-incompatible updates
|
||||
"<yellow>" : Minor Update backward-compatible features
|
||||
"<green>" : Patch Update backward-compatible bug fixes
|
||||
Package Current Wanted Latest Package Type URL
|
||||
@types/jest 23.3.14 23.3.14 29.2.3 devDependencies https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest
|
||||
jest 28.1.3 28.1.3 29.3.1 devDependencies https://jestjs.io/
|
||||
jest-junit 14.0.1 14.0.1 15.0.0 devDependencies https://github.com/jest-community/jest-junit#readme
|
||||
tslint 5.20.1 5.20.1 6.1.3 devDependencies https://palantir.github.io/tslint
|
||||
Done in 12.56s.`
|
||||
expect(parseOutdatedLibraries(input, 'package.json')).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
import npmResolver from './npm-resolver'
|
||||
import npmPublisher from './npm-publisher'
|
||||
|
||||
export { npmResolver, npmPublisher }
|
|
@ -0,0 +1,29 @@
|
|||
import { Publisher, PublisherFactory } from '../publisher'
|
||||
import { ResolveResult } from '../resolver'
|
||||
import { PackageManager } from './npm-resolver'
|
||||
import * as child_process from 'child_process'
|
||||
|
||||
export interface NpmPublisherOptions {
|
||||
package_manager?: PackageManager
|
||||
}
|
||||
|
||||
export class NpmPublisher implements Publisher<NpmPublisherOptions> {
|
||||
|
||||
readonly options: NpmPublisherOptions
|
||||
|
||||
constructor(options?: NpmPublisherOptions) {
|
||||
this.options = {
|
||||
package_manager: 'yarn',
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
publish(results: ResolveResult[]): void {
|
||||
const cmd = `yarn upgrade ${results.map((r) => `${r.current.name}@${r.recommended.version.version}`).join(' ')}`
|
||||
child_process.execSync(cmd, { stdio: 'inherit' })
|
||||
}
|
||||
}
|
||||
|
||||
const factory: PublisherFactory<NpmPublisherOptions> = (options: NpmPublisherOptions) => new NpmPublisher(options)
|
||||
export default factory
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Resolver, ResolveResult, ResolverFactory } from '../resolver'
|
||||
import * as child_process from 'child_process'
|
||||
import { parseOutdatedLibraries } from './process-yarn'
|
||||
|
||||
export type PackageManager = 'npm' | 'yarn'
|
||||
export interface NpmResolverOptions {
|
||||
package_manager?: PackageManager
|
||||
}
|
||||
|
||||
export class NpmResolver implements Resolver<NpmResolverOptions> {
|
||||
readonly type: string = 'npm'
|
||||
readonly options: NpmResolverOptions
|
||||
|
||||
constructor(options?: NpmResolverOptions) {
|
||||
this.options = {
|
||||
package_manager: 'yarn',
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveWithYarn(): Promise<ResolveResult[]> {
|
||||
try {
|
||||
child_process.execSync('yarn outdated', { })
|
||||
}catch (e: any) {
|
||||
// Unfortunately the command throws an error, that we have to catch
|
||||
const stdout = String(e.stdout)
|
||||
return parseOutdatedLibraries(stdout, 'package.json')
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async resolve(): Promise<ResolveResult[]> {
|
||||
return this.resolveWithYarn()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const factory: ResolverFactory<NpmResolverOptions> = (options) => new NpmResolver(options)
|
||||
export default factory
|
|
@ -0,0 +1,66 @@
|
|||
import { ResolveResult } from '../resolver'
|
||||
import { compareVersions, parseSemver } from '../helpers'
|
||||
|
||||
/**
|
||||
* yarn outdated v1.22.19
|
||||
* info Color legend :
|
||||
* "<red>" : Major Update backward-incompatible updates
|
||||
* "<yellow>" : Minor Update backward-compatible features
|
||||
* "<green>" : Patch Update backward-compatible bug fixes
|
||||
* Package Current Wanted Latest Package Type URL
|
||||
* @types/jest 23.3.14 23.3.14 29.2.3 devDependencies https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest
|
||||
* jest 28.1.3 28.1.3 29.3.1 devDependencies https://jestjs.io/
|
||||
* jest-junit 14.0.1 14.0.1 15.0.0 devDependencies https://github.com/jest-community/jest-junit#readme
|
||||
* tslint 5.20.1 5.20.1 6.1.3 devDependencies https://palantir.github.io/tslint
|
||||
* Done in 12.56s.
|
||||
*/
|
||||
|
||||
|
||||
export const parseOutdatedLibraries = (stdout: string, location: string): ResolveResult[] => {
|
||||
const startHeaders = stdout.indexOf('Package')
|
||||
const endDone = stdout.indexOf('Done in')
|
||||
const data = stdout.substring(startHeaders, endDone)
|
||||
|
||||
const linesRaw = data.split('\n')
|
||||
|
||||
const lines = data.split('\n')
|
||||
.slice(1, linesRaw.length-1) //Ignore headers and 'Done'
|
||||
.map((line) => line.trim())
|
||||
.map((line) => line.split(/\s+/).map((item) => item.trim()))
|
||||
|
||||
const results = lines
|
||||
.map(([name, current, , latest, packageType, url]) => {
|
||||
const currentVersion = parseSemver(current)
|
||||
const recommendedVersion = parseSemver(latest)
|
||||
const changeType = compareVersions(currentVersion, recommendedVersion)
|
||||
|
||||
if (!changeType) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
current: {
|
||||
name,
|
||||
location,
|
||||
type: 'npm',
|
||||
version: currentVersion,
|
||||
details: {
|
||||
url,
|
||||
package_type: packageType,
|
||||
},
|
||||
},
|
||||
recommended: {
|
||||
name,
|
||||
location,
|
||||
type: 'npm',
|
||||
version: recommendedVersion,
|
||||
details: {
|
||||
url,
|
||||
package_type: packageType,
|
||||
},
|
||||
},
|
||||
change_type: changeType,
|
||||
} as ResolveResult
|
||||
})
|
||||
return results as ResolveResult[]
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { Resolver } from './resolver'
|
||||
import { Publisher } from './publisher'
|
||||
|
||||
export interface Project {
|
||||
name: string
|
||||
scm: ScmInfo
|
||||
resolver: Record<string, Resolver<any>>
|
||||
publisher?: Record<string, Publisher<any>>
|
||||
}
|
||||
|
||||
export type ScmType = 'git'
|
||||
|
||||
export interface ScmInfo {
|
||||
type?: ScmType
|
||||
url: string
|
||||
}
|
||||
|
||||
const validateScmInfo = (scm?: ScmInfo) => {
|
||||
if(!scm) {
|
||||
throw Error('Missing scm info')
|
||||
}
|
||||
if(!scm.url) {
|
||||
throw Error('Missing scm url')
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectRunner {
|
||||
|
||||
public loadProject(path: string) {
|
||||
const project = require(path) as Project
|
||||
this.validateProject(project)
|
||||
return project as Project
|
||||
}
|
||||
|
||||
public validateProject(project: Project) {
|
||||
if(!project.name) {
|
||||
throw Error('Missing project name')
|
||||
}
|
||||
validateScmInfo(project.scm)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ResolveResult } from './resolver'
|
||||
|
||||
export interface Publisher<T> {
|
||||
publish: (results: ResolveResult[]) => void
|
||||
}
|
||||
|
||||
export type PublisherFactory<T> = (options: T) => Publisher<T>
|
|
@ -0,0 +1,29 @@
|
|||
export interface VersionInfo {
|
||||
version: string
|
||||
major: number
|
||||
minor: number
|
||||
patch: number
|
||||
prerelease?: string
|
||||
build?: string
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
name: string
|
||||
type: string
|
||||
location: string
|
||||
version: VersionInfo
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type VersionChangeType = 'major' | 'minor' | 'patch' | 'prerelease' | 'build'
|
||||
export interface ResolveResult {
|
||||
current: Library
|
||||
recommended: Library
|
||||
change_type: VersionChangeType
|
||||
}
|
||||
|
||||
export interface Resolver<T> {
|
||||
resolve: () => Promise<ResolveResult[]>
|
||||
}
|
||||
|
||||
export type ResolverFactory<T> = (options: T) => Resolver<T>
|
Loading…
Reference in New Issue