linkinator
Advanced tools
Comparing version 1.3.0 to 1.3.1
#!/usr/bin/env node | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -52,61 +44,59 @@ const meow = require("meow"); | ||
skip: { type: 'string', alias: 's' }, | ||
format: { type: 'string', alias: 'f' } | ||
} | ||
format: { type: 'string', alias: 'f' }, | ||
}, | ||
}); | ||
let flags; | ||
function main() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (cli.input.length !== 1) { | ||
cli.showHelp(); | ||
return; | ||
async function main() { | ||
if (cli.input.length !== 1) { | ||
cli.showHelp(); | ||
return; | ||
} | ||
flags = cli.flags; | ||
const start = Date.now(); | ||
log(`🏊♂️ crawling ${cli.input}`); | ||
const checker = new index_1.LinkChecker(); | ||
checker.on('pagestart', url => { | ||
log(`\n Scanning ${chalk_1.default.grey(url)}`); | ||
}); | ||
checker.on('link', (link) => { | ||
let state = ''; | ||
switch (link.state) { | ||
case index_1.LinkState.BROKEN: | ||
state = `[${chalk_1.default.red(link.status.toString())}]`; | ||
break; | ||
case index_1.LinkState.OK: | ||
state = `[${chalk_1.default.green(link.status.toString())}]`; | ||
break; | ||
case index_1.LinkState.SKIPPED: | ||
state = `[${chalk_1.default.grey('SKP')}]`; | ||
break; | ||
default: | ||
throw new Error('Invalid state.'); | ||
} | ||
flags = cli.flags; | ||
const start = Date.now(); | ||
log(`🏊♂️ crawling ${cli.input}`); | ||
const checker = new index_1.LinkChecker(); | ||
checker.on('pagestart', url => { | ||
log(`\n Scanning ${chalk_1.default.grey(url)}`); | ||
}); | ||
checker.on('link', (link) => { | ||
let state = ''; | ||
switch (link.state) { | ||
case index_1.LinkState.BROKEN: | ||
state = `[${chalk_1.default.red(link.status.toString())}]`; | ||
break; | ||
case index_1.LinkState.OK: | ||
state = `[${chalk_1.default.green(link.status.toString())}]`; | ||
break; | ||
case index_1.LinkState.SKIPPED: | ||
state = `[${chalk_1.default.grey('SKP')}]`; | ||
break; | ||
default: | ||
throw new Error('Invalid state.'); | ||
} | ||
log(` ${state} ${chalk_1.default.gray(link.url)}`); | ||
}); | ||
const opts = { path: cli.input[0], recurse: cli.flags.recurse }; | ||
if (cli.flags.skip) { | ||
const skips = cli.flags.skip; | ||
opts.linksToSkip = skips.split(' ').filter(x => !!x); | ||
} | ||
const result = yield checker.check(opts); | ||
log(); | ||
const format = flags.format ? flags.format.toLowerCase() : null; | ||
if (format === 'json') { | ||
console.log(result); | ||
return; | ||
} | ||
else if (format === 'csv') { | ||
const csv = yield toCSV(result.links); | ||
console.log(csv); | ||
return; | ||
} | ||
const total = (Date.now() - start) / 1000; | ||
if (!result.passed) { | ||
const borked = result.links.filter(x => x.state === index_1.LinkState.BROKEN); | ||
console.error(chalk_1.default.bold(`${chalk_1.default.red('ERROR')}: Detected ${borked.length} broken links. Scanned ${chalk_1.default.yellow(result.links.length.toString())} links in ${chalk_1.default.cyan(total.toString())} seconds.`)); | ||
process.exit(1); | ||
} | ||
log(chalk_1.default.bold(`🤖 Successfully scanned ${chalk_1.default.green(result.links.length.toString())} links in ${chalk_1.default.cyan(total.toString())} seconds.`)); | ||
log(` ${state} ${chalk_1.default.gray(link.url)}`); | ||
}); | ||
const opts = { path: cli.input[0], recurse: cli.flags.recurse }; | ||
if (cli.flags.skip) { | ||
const skips = cli.flags.skip; | ||
opts.linksToSkip = skips.split(' ').filter(x => !!x); | ||
} | ||
const result = await checker.check(opts); | ||
log(); | ||
const format = flags.format ? flags.format.toLowerCase() : null; | ||
if (format === 'json') { | ||
console.log(result); | ||
return; | ||
} | ||
else if (format === 'csv') { | ||
const csv = await toCSV(result.links); | ||
console.log(csv); | ||
return; | ||
} | ||
const total = (Date.now() - start) / 1000; | ||
if (!result.passed) { | ||
const borked = result.links.filter(x => x.state === index_1.LinkState.BROKEN); | ||
console.error(chalk_1.default.bold(`${chalk_1.default.red('ERROR')}: Detected ${borked.length} broken links. Scanned ${chalk_1.default.yellow(result.links.length.toString())} links in ${chalk_1.default.cyan(total.toString())} seconds.`)); | ||
process.exit(1); | ||
} | ||
log(chalk_1.default.bold(`🤖 Successfully scanned ${chalk_1.default.green(result.links.length.toString())} links in ${chalk_1.default.cyan(total.toString())} seconds.`)); | ||
} | ||
@@ -113,0 +103,0 @@ function log(message = '\n') { |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -32,29 +24,27 @@ const events_1 = require("events"); | ||
*/ | ||
check(options) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
options.linksToSkip = options.linksToSkip || []; | ||
options.linksToSkip.push('^mailto:'); | ||
let server; | ||
if (!options.path.startsWith('http')) { | ||
const port = options.port || 5000 + Math.round(Math.random() * 1000); | ||
server = yield this.startWebServer(options.path, port); | ||
enableDestroy(server); | ||
options.path = `http://localhost:${port}`; | ||
} | ||
const results = yield this.crawl({ | ||
url: options.path, | ||
crawl: true, | ||
checkOptions: options, | ||
results: [], | ||
cache: new Set() | ||
}); | ||
const result = { | ||
links: results, | ||
passed: results.filter(x => x.state === LinkState.BROKEN).length === 0 | ||
}; | ||
if (server) { | ||
server.destroy(); | ||
} | ||
return result; | ||
async check(options) { | ||
options.linksToSkip = options.linksToSkip || []; | ||
options.linksToSkip.push('^mailto:'); | ||
let server; | ||
if (!options.path.startsWith('http')) { | ||
const port = options.port || 5000 + Math.round(Math.random() * 1000); | ||
server = await this.startWebServer(options.path, port); | ||
enableDestroy(server); | ||
options.path = `http://localhost:${port}`; | ||
} | ||
const results = await this.crawl({ | ||
url: options.path, | ||
crawl: true, | ||
checkOptions: options, | ||
results: [], | ||
cache: new Set(), | ||
}); | ||
const result = { | ||
links: results, | ||
passed: results.filter(x => x.state === LinkState.BROKEN).length === 0, | ||
}; | ||
if (server) { | ||
server.destroy(); | ||
} | ||
return result; | ||
} | ||
@@ -70,3 +60,4 @@ /** | ||
return new Promise(resolve => { | ||
const server = http.createServer(ecstatic({ root })) | ||
const server = http | ||
.createServer(ecstatic({ root })) | ||
.listen(port, () => resolve(server)); | ||
@@ -81,76 +72,73 @@ }); | ||
*/ | ||
crawl(opts) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// Check to see if we've already scanned this url | ||
if (opts.cache.has(opts.url)) { | ||
return opts.results; | ||
} | ||
opts.cache.add(opts.url); | ||
// Check for links that should be skipped | ||
const skips = opts.checkOptions.linksToSkip | ||
.map(linkToSkip => { | ||
return (new RegExp(linkToSkip)).test(opts.url); | ||
}) | ||
.filter(match => !!match); | ||
if (skips.length > 0) { | ||
const result = { url: opts.url, state: LinkState.SKIPPED }; | ||
opts.results.push(result); | ||
this.emit('link', result); | ||
return opts.results; | ||
} | ||
// Perform a HEAD or GET request based on the need to crawl | ||
let status = 0; | ||
let state = LinkState.BROKEN; | ||
let data = ''; | ||
let shouldRecurse = false; | ||
try { | ||
let res = yield gaxios.request({ | ||
method: opts.crawl ? 'GET' : 'HEAD', | ||
async crawl(opts) { | ||
// Check to see if we've already scanned this url | ||
if (opts.cache.has(opts.url)) { | ||
return opts.results; | ||
} | ||
opts.cache.add(opts.url); | ||
// Check for links that should be skipped | ||
const skips = opts.checkOptions | ||
.linksToSkip.map(linkToSkip => { | ||
return new RegExp(linkToSkip).test(opts.url); | ||
}) | ||
.filter(match => !!match); | ||
if (skips.length > 0) { | ||
const result = { url: opts.url, state: LinkState.SKIPPED }; | ||
opts.results.push(result); | ||
this.emit('link', result); | ||
return opts.results; | ||
} | ||
// Perform a HEAD or GET request based on the need to crawl | ||
let status = 0; | ||
let state = LinkState.BROKEN; | ||
let data = ''; | ||
let shouldRecurse = false; | ||
try { | ||
let res = await gaxios.request({ | ||
method: opts.crawl ? 'GET' : 'HEAD', | ||
url: opts.url, | ||
responseType: opts.crawl ? 'text' : 'stream', | ||
validateStatus: () => true, | ||
}); | ||
// If we got an HTTP 405, the server may not like HEAD. GET instead! | ||
if (res.status === 405) { | ||
res = await gaxios.request({ | ||
method: 'GET', | ||
url: opts.url, | ||
responseType: opts.crawl ? 'text' : 'stream', | ||
validateStatus: () => true | ||
responseType: 'stream', | ||
validateStatus: () => true, | ||
}); | ||
// If we got an HTTP 405, the server may not like HEAD. GET instead! | ||
if (res.status === 405) { | ||
res = yield gaxios.request({ | ||
method: 'GET', | ||
url: opts.url, | ||
responseType: 'stream', | ||
validateStatus: () => true | ||
}); | ||
} | ||
// Assume any 2xx status is 👌 | ||
status = res.status; | ||
if (res.status >= 200 && res.status < 300) { | ||
state = LinkState.OK; | ||
} | ||
data = res.data; | ||
shouldRecurse = isHtml(res); | ||
} | ||
catch (err) { | ||
// request failure: invalid domain name, etc. | ||
// Assume any 2xx status is 👌 | ||
status = res.status; | ||
if (res.status >= 200 && res.status < 300) { | ||
state = LinkState.OK; | ||
} | ||
const result = { url: opts.url, status, state }; | ||
opts.results.push(result); | ||
this.emit('link', result); | ||
// If we need to go deeper, scan the next level of depth for links and crawl | ||
if (opts.crawl && shouldRecurse) { | ||
this.emit('pagestart', opts.url); | ||
const urls = links_1.getLinks(data, opts.url); | ||
for (const url of urls) { | ||
// only crawl links that start with the same host | ||
const crawl = opts.checkOptions.recurse && | ||
url.startsWith(opts.checkOptions.path); | ||
yield this.crawl({ | ||
url, | ||
crawl, | ||
cache: opts.cache, | ||
results: opts.results, | ||
checkOptions: opts.checkOptions | ||
}); | ||
} | ||
data = res.data; | ||
shouldRecurse = isHtml(res); | ||
} | ||
catch (err) { | ||
// request failure: invalid domain name, etc. | ||
} | ||
const result = { url: opts.url, status, state }; | ||
opts.results.push(result); | ||
this.emit('link', result); | ||
// If we need to go deeper, scan the next level of depth for links and crawl | ||
if (opts.crawl && shouldRecurse) { | ||
this.emit('pagestart', opts.url); | ||
const urls = links_1.getLinks(data, opts.url); | ||
for (const url of urls) { | ||
// only crawl links that start with the same host | ||
const crawl = opts.checkOptions.recurse && url.startsWith(opts.checkOptions.path); | ||
await this.crawl({ | ||
url, | ||
crawl, | ||
cache: opts.cache, | ||
results: opts.results, | ||
checkOptions: opts.checkOptions, | ||
}); | ||
} | ||
// Return the aggregate results | ||
return opts.results; | ||
}); | ||
} | ||
// Return the aggregate results | ||
return opts.results; | ||
} | ||
@@ -163,8 +151,6 @@ } | ||
*/ | ||
function check(options) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const checker = new LinkChecker(); | ||
const results = yield checker.check(options); | ||
return results; | ||
}); | ||
async function check(options) { | ||
const checker = new LinkChecker(); | ||
const results = await checker.check(options); | ||
return results; | ||
} | ||
@@ -171,0 +157,0 @@ exports.check = check; |
@@ -17,4 +17,12 @@ "use strict"; | ||
src: [ | ||
'audio', 'embed', 'frame', 'iframe', 'img', 'input', 'script', 'source', | ||
'track', 'video' | ||
'audio', | ||
'embed', | ||
'frame', | ||
'iframe', | ||
'img', | ||
'input', | ||
'script', | ||
'source', | ||
'track', | ||
'video', | ||
], | ||
@@ -33,3 +41,5 @@ srcset: ['img', 'source'], | ||
}); | ||
const sanitized = links.filter(link => !!link).map(link => normalizeLink(link, baseUrl)); | ||
const sanitized = links | ||
.filter(link => !!link) | ||
.map(link => normalizeLink(link, baseUrl)); | ||
return sanitized; | ||
@@ -41,3 +51,5 @@ } | ||
case 'srcset': | ||
return value.split(',').map((pair) => pair.trim().split(/\s+/)[0]); | ||
return value | ||
.split(',') | ||
.map((pair) => pair.trim().split(/\s+/)[0]); | ||
default: | ||
@@ -44,0 +56,0 @@ return [value]; |
{ | ||
"name": "linkinator", | ||
"description": "Find broken links, missing images, etc in your HTML. Scurry around your site and find all those broken links.", | ||
"version": "1.3.0", | ||
"version": "1.3.1", | ||
"license": "MIT", | ||
@@ -26,3 +26,3 @@ "repository": "JustinBeckwith/linkinator", | ||
"ecstatic": "^4.0.0", | ||
"gaxios": "^1.7.0", | ||
"gaxios": "^2.0.0", | ||
"jsonexport": "^2.4.1", | ||
@@ -45,3 +45,3 @@ "meow": "^5.0.0", | ||
"codecov": "^3.2.0", | ||
"gts": "^0.9.0", | ||
"gts": "^1.0.0", | ||
"mocha": "^6.0.0", | ||
@@ -48,0 +48,0 @@ "nock": "^10.0.6", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
32049
389
+ Addedagent-base@6.0.2(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addedgaxios@2.3.4(transitive)
+ Addedhttps-proxy-agent@5.0.1(transitive)
+ Addedis-stream@2.0.1(transitive)
- Removedagent-base@4.3.0(transitive)
- Removeddebug@3.2.7(transitive)
- Removedes6-promise@4.2.8(transitive)
- Removedes6-promisify@5.0.0(transitive)
- Removedgaxios@1.8.4(transitive)
- Removedhttps-proxy-agent@2.2.4(transitive)
Updatedgaxios@^2.0.0