Skip to content

Commit a1b836d

Browse files
committed
feat: add full option, output all versions with body or error
1 parent f8113be commit a1b836d

File tree

4 files changed

+125
-86
lines changed

4 files changed

+125
-86
lines changed

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,35 @@ list breaking changes in newer major versions of packages
1111
# How it works
1212

1313
Right now only packages hosted on GitHub are supported. `what-broke` will get
14-
the package info and repository URL from `npm`, then try to fetch and parse the
15-
package's `CHANGELOG.md` or `changelog.md` in the `master` branch.
16-
If no changelog file exists it will try to fetch GitHub releases instead
17-
(which work way better for a tool like this than changelog files, so please use
18-
GitHub releases!)
14+
the package info and repository URL from `npm`, then try to fetch the GitHub
15+
release for each relevant version tag. If no GitHub release is found it will
16+
fall back to trying to parse the package's `CHANGELOG.md` or `changelog.md`.
17+
GitHub releases are way more reliable for this purpose though, so please use
18+
them!
1919

20-
# GitHub token
20+
# API Tokens
2121

2222
GitHub heavily rate limits public API requests, but allows more throughput for
2323
authenticated requests. If you set the `GH_TOKEN` environment variable to a
2424
personal access token, `what-broke` will use it when requesting GitHub releases.
2525

26+
`what-broke` will also use the `NPM_TOKEN` environment variable or try to get
27+
the npm token from your `~/.npmrc`, so that it can get information for private
28+
packages you request.
29+
2630
# CLI
2731

2832
```
2933
npm i -g what-broke
3034
```
3135

3236
```
33-
what-broke <package> [<from verison> [<to version>]]
37+
what-broke <package> [--full] [<from verison> [<to version>]]
3438
```
3539

3640
Will print out the changelog contents for all major and prerelease versions in
37-
the given range.
41+
the given range. (If `--full` is given, it will also include minor and patch
42+
versions.)
3843

3944
If `package` is installed in the current working directory, `<from version>`
4045
will default to the installed version.
@@ -53,6 +58,7 @@ async function whatBroke(
5358
options?: {
5459
fromVersion?: ?string,
5560
toVersion?: ?string,
61+
full?: ?boolean,
5662
}
5763
): Promise<Array<{version: string, body: string}>>
5864
```

src/changelog-parser.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ export type Release = {
66
version: string,
77
body?: string,
88
date?: Date,
9+
error?: Error,
910
}
1011

11-
export default function parseChangelog(text: string): Array<Release> {
12-
const result = []
12+
export default function parseChangelog(text: string): { [string]: Release } {
13+
const result = {}
1314
const versionHeaderRx = new RegExp(
1415
`^#+\\s+(${versionRx})(.*)$|^(${versionRx})(.*)(\r\n?|\n)=+`,
1516
'mg'
@@ -22,7 +23,7 @@ export default function parseChangelog(text: string): Array<Release> {
2223
const version = match[1] || match[4]
2324
if (release) release.body = text.substring(start, match.index).trim()
2425
release = { version }
25-
result.push(release)
26+
result[version] = release
2627
start = match.index + match[0].length
2728
}
2829
if (release) release.body = text.substring(start).trim()

src/index.js

Lines changed: 89 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,114 +8,125 @@ import parseChangelog, { type Release } from './changelog-parser'
88
import semver from 'semver'
99
import Octokit from '@octokit/rest'
1010
import getNpmToken from './getNpmToken'
11+
import memoize from './util/memoize'
1112

1213
const { GH_TOKEN } = process.env
1314

1415
const octokitOptions = {}
1516
if (GH_TOKEN) octokitOptions.auth = `token ${GH_TOKEN}`
1617
const octokit = new Octokit(octokitOptions)
1718

18-
export async function getChangelog(
19-
owner: string,
20-
repo: string
21-
): Promise<Array<Release>> {
22-
let changelog
23-
for (const file of ['CHANGELOG.md', 'changelog.md']) {
24-
try {
25-
const {
26-
data: { content },
27-
} = await octokit.repos.getContents({
28-
owner,
29-
repo,
30-
path: file,
31-
})
32-
changelog = Base64.decode(content)
33-
break
34-
} catch (error) {
35-
continue
19+
export const getChangelog = memoize(
20+
async (owner: string, repo: string): Promise<{ [string]: Release }> => {
21+
let changelog
22+
for (const file of ['CHANGELOG.md', 'changelog.md']) {
23+
try {
24+
const {
25+
data: { content },
26+
} = await octokit.repos.getContents({
27+
owner,
28+
repo,
29+
path: file,
30+
})
31+
changelog = Base64.decode(content)
32+
break
33+
} catch (error) {
34+
continue
35+
}
3636
}
37-
}
38-
if (!changelog) throw new Error('failed to get changelog')
39-
return await parseChangelog(changelog)
37+
if (!changelog) throw new Error('failed to get changelog')
38+
return await parseChangelog(changelog)
39+
},
40+
(owner, repo) => `${owner}/${repo}`
41+
)
42+
43+
function parseRepositoryUrl(url: string): { owner: string, repo: string } {
44+
const match = /github\.com[:/]([^\\]+)\/([^.\\]+)/i.exec(url)
45+
if (!match) throw new Error(`repository.url not supported: ${url}`)
46+
const [owner, repo] = match.slice(1)
47+
return { owner, repo }
4048
}
4149

4250
export async function whatBroke(
4351
pkg: string,
4452
{
4553
fromVersion,
4654
toVersion,
55+
full,
4756
}: {
4857
fromVersion?: ?string,
4958
toVersion?: ?string,
59+
full?: ?boolean,
5060
} = {}
5161
): Promise<Object> {
5262
const npmInfo = await npmRegistryFetch.json(pkg, {
5363
token: await getNpmToken(),
5464
})
55-
const { repository: { url } = {} } = npmInfo
56-
if (!url) throw new Error('failed to get repository.url')
57-
const match = /github\.com\/([^\\]+)\/([^.\\]+)/i.exec(url)
58-
if (!match) throw new Error(`repository.url not supported: ${url}`)
59-
const [owner, repo] = match.slice(1)
60-
let changelog
61-
await getChangelog(owner, repo)
62-
.then(c => (changelog = c))
63-
.catch(() => {})
6465

65-
const result = []
66+
const versions = Object.keys(npmInfo.versions).filter(
67+
(v: string): boolean => {
68+
if (fromVersion && !semver.gt(v, fromVersion)) return false
69+
if (toVersion && !semver.lt(v, toVersion)) return false
70+
return true
71+
}
72+
)
73+
74+
const releases = []
6675

67-
if (changelog) {
68-
let prevVersion = fromVersion
69-
for (const release of changelog.reverse()) {
70-
const { version } = release
71-
if (!version) continue
72-
if (prevVersion && semver.lte(version, prevVersion)) continue
73-
if (toVersion && semver.gt(version, toVersion)) break
74-
if (
75-
prevVersion == null ||
76-
semver.prerelease(version) ||
77-
!semver.satisfies(version, `^${prevVersion}`)
78-
) {
79-
result.push(release)
80-
prevVersion = version
81-
}
76+
let prevVersion = fromVersion
77+
for (let version of versions) {
78+
if (
79+
!full &&
80+
prevVersion != null &&
81+
!semver.prerelease(version) &&
82+
semver.satisfies(version, `^${prevVersion}`) &&
83+
!(semver.prerelease(prevVersion) && !semver.prerelease(version))
84+
) {
85+
continue
8286
}
83-
} else {
84-
const versions = Object.keys(npmInfo.versions)
87+
prevVersion = version
88+
89+
const release: Release = { version, date: new Date(npmInfo.time[version]) }
90+
releases.push(release)
8591

86-
let prevVersion = fromVersion
87-
for (const version of versions) {
88-
if (!version) continue
89-
if (prevVersion && semver.lte(version, prevVersion)) continue
90-
if (toVersion && semver.gt(version, toVersion)) break
91-
if (
92-
prevVersion == null ||
93-
semver.prerelease(version) ||
94-
!semver.satisfies(version, `^${prevVersion}`)
95-
) {
96-
await octokit.repos
97-
.getReleaseByTag({
98-
owner,
99-
repo,
100-
tag: `v${version}`,
101-
})
102-
.then(({ data: { body } }: Object) => {
103-
result.push({ version, body })
104-
})
105-
.catch(() => {})
92+
const { url } =
93+
npmInfo.versions[version].repository || npmInfo.repository || {}
94+
if (!url) {
95+
release.error = new Error('failed to get repository url from npm')
96+
}
10697

107-
prevVersion = version
98+
try {
99+
const { owner, repo } = parseRepositoryUrl(url)
100+
101+
try {
102+
release.body = (await octokit.repos.getReleaseByTag({
103+
owner,
104+
repo,
105+
tag: `v${version}`,
106+
})).data.body
107+
} catch (error) {
108+
const changelog = await getChangelog(owner, repo)
109+
if (changelog[version]) release.body = changelog[version].body
110+
}
111+
if (!release.body) {
112+
release.error = new Error(
113+
`failed to find GitHub release or changelog entry for version ${version}`
114+
)
108115
}
116+
} catch (error) {
117+
release.error = error
109118
}
110119
}
111120

112-
return result
121+
return releases
113122
}
114123

115124
if (!module.parent) {
116-
const pkg = process.argv[2]
117-
let fromVersion = process.argv[3],
118-
toVersion = process.argv[4]
125+
const full = process.argv.indexOf('--full') >= 0
126+
const args = process.argv.slice(2).filter(a => a[0] !== '-')
127+
const pkg = args[0]
128+
let fromVersion = args[1],
129+
toVersion = args[2]
119130
if (!fromVersion) {
120131
try {
121132
// $FlowFixMe
@@ -130,11 +141,14 @@ if (!module.parent) {
130141
}
131142
}
132143
/* eslint-env node */
133-
whatBroke(pkg, { fromVersion, toVersion }).then(
144+
whatBroke(pkg, { fromVersion, toVersion, full }).then(
134145
(changelog: Array<Release>) => {
135-
for (const { version, body } of changelog) {
146+
for (const { version, body, error } of changelog) {
136147
process.stdout.write(chalk.bold(version) + '\n\n')
137148
if (body) process.stdout.write(body + '\n\n')
149+
if (error) {
150+
process.stdout.write(`Failed to get changelog: ${error.stack}\n\n`)
151+
}
138152
}
139153
process.exit(0)
140154
},

src/util/memoize.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @flow
3+
* @prettier
4+
*/
5+
6+
export default function memoize<F: (...args: any[]) => any>(
7+
fn: F,
8+
resolver?: (...args: any[]) => any = first => String(first)
9+
): F {
10+
const cache = new Map()
11+
return ((...args: any[]): any => {
12+
const key = resolver(...args)
13+
if (cache.has(key)) return cache.get(key)
14+
const result = fn(...args)
15+
cache.set(key, result)
16+
return result
17+
}: any)
18+
}

0 commit comments

Comments
 (0)