1
1
mirror of https://github.com/primer/css.git synced 2024-12-28 00:24:36 +03:00
css/docs/script/check-links
2019-01-11 14:29:22 -08:00

158 lines
4.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
const spinner = require('char-spinner')
const {gray, green, yellow, red, bold} = require('colorette')
const {SiteChecker} = require('broken-link-checker')
const yargs = require('yargs')
.option('excluded-schemes', {type: String, alias: 's', default: ['dash-feed', 'mailto']})
.option('filter-level', {type: Number, alias: 'L', default: 3})
.option('max-sockets-per-host', {type: Number, alias: 'm', default: 1})
.option('verbose', {type: Boolean, alias: 'v', default: false})
const options = yargs.argv
const args = options._
const VERBOSE = options.verbose
const pages = []
const seen = new Set()
const excepted = new Map()
const OK = ' ✓ '
const NOT_OK = ' ✘ '
const TAG_LENGTH = 3
const URL = args[0]
if (URL) {
log(green('go!'), bold(URL))
} else {
log('err', 'you must provide a URL')
process.exit(1)
}
let page = {url: URL, links: []}
const exceptions = {
'GitHub private repo': url =>
[
// this is a list of known GitHub private repos
'https://github.com/github/accessibility',
'https://github.com/github/design',
'https://github.com/github/design-systems',
'https://github.com/github/github',
'https://github.com/github/sentinel'
].some(repo => url.startsWith(repo))
}
const checker = new SiteChecker(options, {
page(error, url) {
if (error) {
log(red('ERR'), `${url} (${error.code})`)
} else if (page) {
const {url, response, links = []} = page
const num = String(links.length).padEnd(TAG_LENGTH)
let message = `${bold(num)}${pages.length ? ' unique' : ''} links`
if (!VERBOSE) message = `${message} on ${yellow(url)}`
log(OK, message)
if (links.length) {
pages.push(page)
}
}
},
html(tree, robots, response, url) {
if (VERBOSE) log(yellow('get'), url)
page = {url, links: []}
},
junk(result) {
const url = result.url.resolved || result.url.original
if (!url || seen.has(url)) {
return
} else if (VERBOSE) {
log(' '.repeat(TAG_LENGTH), gray(`skip ${shorten(url)}`))
} else if (result.excluded && url.indexOf(URL) !== 0) {
log(yellow('---'), gray(`excluded: ${url}`))
}
seen.add(url)
},
link(result) {
const url = result.url.resolved || result.url.original
if (VERBOSE && !seen.has(url)) log(' + ', gray('link'), shorten(url))
for (const [reason, test] of Object.entries(exceptions)) {
if (test(url)) {
log(yellow('---'), gray(`skip ${url}`), yellow(reason))
excepted.set(url, reason)
seen.add(url)
return
}
}
if (!seen.has(url)) page.links.push(result)
seen.add(url)
},
end() {
const allBroken = []
for (const page of pages) {
const broken = page.links.filter(link => link.broken)
allBroken.push(...broken)
if (!broken.length && !VERBOSE) {
continue
} else {
const num = broken.length ? red(` ${broken.length}`.padEnd(TAG_LENGTH)) : green(' 0').padEnd(TAG_LENGTH)
log(bold(num), `broken links on ${bold(page.url)}`)
}
for (const link of page.links) {
if (!link.broken && !VERBOSE) continue
const tag = link.broken ? red(NOT_OK) : green(OK)
const reason = link.broken ? link.brokenReason.replace(/HTTP_/, '') : ''
const url = link.url.resolved || link.url.original
log(tag, shorten(url), yellow(reason))
link.source = url
link.reason = reason
}
log('')
}
if (excepted.size) {
log(yellow(OK), `Excepted ${excepted.size} links:`)
const exceptedURLs = Array.from(excepted.keys()).sort()
for (const url of exceptedURLs) {
log(yellow(OK), `${yellow(excepted.get(url))} ${gray(url)}`)
}
log('')
}
if (allBroken.length) {
log(red(NOT_OK), `${red(allBroken.length)} broken links:`)
for (const link of allBroken) {
log(red(NOT_OK), red(link.reason), link.url.original, gray('from'), shorten(link.source))
}
log('')
process.exitCode = 1
} else {
log(green(OK), bold('0'), 'broken links')
}
log('')
}
})
spinner()
checker.enqueue(URL)
checker.resume()
function log(tag, ...args) {
spinner.clear()
console.log(tag ? gray(`[${tag}]`) : '', ...args)
}
function shorten(url) {
return String(url).indexOf(URL) === 0 ? gray(URL) + (url.substr(URL.length) || '/') : url
}