Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: JustinBeckwith/linkinator
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.9.0
Choose a base ref
...
head repository: JustinBeckwith/linkinator
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.10.0
Choose a head ref
  • 1 commit
  • 4 files changed
  • 1 contributor

Commits on Dec 24, 2020

  1. Copy the full SHA
    cf29469 View commit details
Showing with 49 additions and 20 deletions.
  1. +1 −0 src/cli.ts
  2. +13 −6 src/index.ts
  3. +9 −0 test/test.ts
  4. +26 −14 test/zcli.ts
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -190,6 +190,7 @@ async function main() {
case LinkState.BROKEN:
state = `[${chalk.red(link.status!.toString())}]`;
logger.error(` ${state} ${chalk.gray(link.url)}`);
logger.debug(JSON.stringify(link.failureDetails, null, 2));
break;
case LinkState.OK:
state = `[${chalk.green(link.status!.toString())}]`;
19 changes: 13 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {EventEmitter} from 'events';
import * as gaxios from 'gaxios';
import {request, GaxiosResponse} from 'gaxios';
import * as http from 'http';
import enableDestroy = require('server-destroy');
import finalhandler = require('finalhandler');
@@ -41,6 +41,7 @@ export interface LinkResult {
status?: number;
state: LinkState;
parent?: string;
failureDetails?: {}[];
}

export interface CrawlResult {
@@ -319,9 +320,10 @@ export class LinkChecker extends EventEmitter {
let state = LinkState.BROKEN;
let data = '';
let shouldRecurse = false;
let res: gaxios.GaxiosResponse<string> | undefined = undefined;
let res: GaxiosResponse<string> | undefined = undefined;
const failures: {}[] = [];
try {
res = await gaxios.request<string>({
res = await request<string>({
method: opts.crawl ? 'GET' : 'HEAD',
url: opts.url.href,
headers,
@@ -332,7 +334,7 @@ export class LinkChecker extends EventEmitter {

// If we got an HTTP 405, the server may not like HEAD. GET instead!
if (res.status === 405) {
res = await gaxios.request<string>({
res = await request<string>({
method: 'GET',
url: opts.url.href,
headers,
@@ -345,6 +347,7 @@ export class LinkChecker extends EventEmitter {
// request failure: invalid domain name, etc.
// this also occasionally catches too many redirects, but is still valid (e.g. https://www.ebay.com)
// for this reason, we also try doing a GET below to see if the link is valid
failures.push(err);
}

try {
@@ -353,7 +356,7 @@ export class LinkChecker extends EventEmitter {
(res === undefined || res.status < 200 || res.status >= 300) &&
!opts.crawl
) {
res = await gaxios.request<string>({
res = await request<string>({
method: 'GET',
url: opts.url.href,
responseType: 'text',
@@ -363,6 +366,7 @@ export class LinkChecker extends EventEmitter {
});
}
} catch (ex) {
failures.push(ex);
// catch the next failure
}

@@ -375,13 +379,16 @@ export class LinkChecker extends EventEmitter {
// Assume any 2xx status is 👌
if (status >= 200 && status < 300) {
state = LinkState.OK;
} else {
failures.push(res!);
}

const result: LinkResult = {
url: opts.url.href,
status,
state,
parent: opts.parent,
failureDetails: failures,
};
opts.results.push(result);
this.emit('link', result);
@@ -455,7 +462,7 @@ export async function check(options: CheckOptions) {
* @param {object} response Page response.
* @returns {boolean}
*/
function isHtml(response: gaxios.GaxiosResponse): boolean {
function isHtml(response: GaxiosResponse): boolean {
const contentType = response.headers['content-type'] || '';
return (
!!contentType.match(/text\/html/g) ||
9 changes: 9 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
@@ -436,4 +436,13 @@ describe('linkinator', () => {
assert.ok(results.passed);
scopes.forEach(x => x.done());
});

it('should surface call stacks on failures in the API', async () => {
const results = await check({
path: 'http://fake.local',
});
assert.ok(!results.passed);
const err = results.links[0].failureDetails![0] as Error;
assert.ok(/Nock: Disallowed net connect for/.test(err.message));
});
});
40 changes: 26 additions & 14 deletions test/zcli.ts
Original file line number Diff line number Diff line change
@@ -11,15 +11,15 @@ describe('cli', () => {
const res = await execa('npx', ['linkinator', 'test/fixtures/basic'], {
reject: false,
});
assert.include(res.stderr, 'ERROR: Detected 1 broken links');
assert.match(res.stderr, /ERROR: Detected 1 broken links/);
});

it('should pass successful markdown scan', async () => {
const res = await execa('npx', [
'linkinator',
'test/fixtures/markdown/README.md',
]);
assert.include(res.stderr, 'Successfully scanned');
assert.match(res.stderr, /Successfully scanned/);
});

it('should allow multiple paths', async () => {
@@ -28,14 +28,14 @@ describe('cli', () => {
'test/fixtures/markdown/unlinked.md',
'test/fixtures/markdown/README.md',
]);
assert.include(res.stderr, 'Successfully scanned');
assert.match(res.stderr, /Successfully scanned/);
});

it('should show help if no params are provided', async () => {
const res = await execa('npx', ['linkinator'], {
reject: false,
});
assert.include(res.stdout, '$ linkinator LOCATION [ --arguments ]');
assert.match(res.stdout, /\$ linkinator LOCATION \[ --arguments \]/);
});

it('should flag skipped links', async () => {
@@ -47,7 +47,7 @@ describe('cli', () => {
'LICENSE.md',
'test/fixtures/markdown/README.md',
]);
assert.include(res.stdout, '[SKP]');
assert.match(res.stdout, /\[SKP\]/);
});

it('should provide CSV if asked nicely', async () => {
@@ -58,7 +58,7 @@ describe('cli', () => {
'csv',
'test/fixtures/markdown/README.md',
]);
assert.include(res.stdout, '/README.md,200,OK,');
assert.match(res.stdout, /\/README.md,200,OK,/);
});

it('should provide JSON if asked nicely', async () => {
@@ -69,7 +69,7 @@ describe('cli', () => {
'json',
'test/fixtures/markdown/README.md',
]);
assert.include(res.stdout, '{');
assert.match(res.stdout, /{/);
});

it('should not show links if --silent', async () => {
@@ -78,7 +78,7 @@ describe('cli', () => {
'--silent',
'test/fixtures/markdown/README.md',
]);
assert.strictEqual(res.stdout.indexOf('['), -1);
assert.notMatch(res.stdout, /\[/);
});

it('should accept a server-root', async () => {
@@ -89,7 +89,7 @@ describe('cli', () => {
'test/fixtures/markdown',
'README.md',
]);
assert.ok(res.stderr.includes('Successfully scanned'));
assert.match(res.stderr, /Successfully scanned/);
});

it('should accept globs', async () => {
@@ -98,7 +98,7 @@ describe('cli', () => {
'test/fixtures/markdown/*.md',
'test/fixtures/markdown/**/*.md',
]);
assert.ok(res.stderr.includes('Successfully scanned'));
assert.match(res.stderr, /Successfully scanned/);
});

it('should throw on invalid format', async () => {
@@ -109,7 +109,7 @@ describe('cli', () => {
reject: false,
}
);
assert.include(res.stderr, 'FORMAT must be');
assert.match(res.stderr, /FORMAT must be/);
});

it('should throw on invalid format', async () => {
@@ -120,7 +120,7 @@ describe('cli', () => {
reject: false,
}
);
assert.include(res.stderr, 'FORMAT must be');
assert.match(res.stderr, /FORMAT must be/);
});

it('should throw on invalid verbosity', async () => {
@@ -131,7 +131,7 @@ describe('cli', () => {
reject: false,
}
);
assert.include(res.stderr, 'VERBOSITY must be');
assert.match(res.stderr, /VERBOSITY must be/);
});

it('should throw when verbosity and silent are flagged', async () => {
@@ -142,7 +142,7 @@ describe('cli', () => {
reject: false,
}
);
assert.include(res.stderr, 'The SILENT and VERBOSITY flags');
assert.match(res.stderr, /The SILENT and VERBOSITY flags/);
});

it('should show no output for verbosity=NONE', async () => {
@@ -157,4 +157,16 @@ describe('cli', () => {
assert.strictEqual(res.stdout, '');
assert.strictEqual(res.stderr, '');
});

it('should show callstacks for verbosity=DEBUG', async () => {
const res = await execa(
'npx',
['linkinator', 'test/fixtures/basic', '--verbosity', 'DEBUG'],
{
reject: false,
}
);
assert.strictEqual(res.exitCode, 1);
assert.match(res.stdout, /reason: getaddrinfo/);
});
});