linkinator
Advanced tools
Comparing version 6.0.6 to 6.0.7
#!/usr/bin/env node | ||
import process from 'node:process'; | ||
import chalk from 'chalk'; | ||
import meow from 'meow'; | ||
import chalk from 'chalk'; | ||
import { getConfig } from './config.js'; | ||
import { Format, Logger, LogLevel } from './logger.js'; | ||
import { LinkChecker, LinkState, } from './index.js'; | ||
import { Format, LogLevel, Logger } from './logger.js'; | ||
const cli = meow(` | ||
@@ -131,3 +131,3 @@ Usage | ||
case LinkState.BROKEN: { | ||
state = `[${chalk.red(link.status.toString())}]`; | ||
state = `[${chalk.red(link.status?.toString())}]`; | ||
logger.error(`${state} ${chalk.gray(link.url)}`); | ||
@@ -137,3 +137,3 @@ break; | ||
case LinkState.OK: { | ||
state = `[${chalk.green(link.status.toString())}]`; | ||
state = `[${chalk.green(link.status?.toString())}]`; | ||
logger.warn(`${state} ${chalk.gray(link.url)}`); | ||
@@ -212,3 +212,2 @@ break; | ||
// } | ||
// eslint-disable-next-line unicorn/no-array-reduce | ||
const parents = result.links.reduce((accumulator, current) => { | ||
@@ -245,3 +244,3 @@ const parent = current.parent || ''; | ||
case LinkState.BROKEN: { | ||
state = `[${chalk.red(link.status.toString())}]`; | ||
state = `[${chalk.red(link.status?.toString())}]`; | ||
logger.error(` ${state} ${chalk.gray(link.url)}`); | ||
@@ -252,3 +251,3 @@ logger.debug(JSON.stringify(link.failureDetails, null, 2)); | ||
case LinkState.OK: { | ||
state = `[${chalk.green(link.status.toString())}]`; | ||
state = `[${chalk.green(link.status?.toString())}]`; | ||
logger.warn(` ${state} ${chalk.gray(link.url)}`); | ||
@@ -255,0 +254,0 @@ break; |
import { promises as fs } from 'node:fs'; | ||
import path from 'node:path'; | ||
import process from 'node:process'; | ||
import path from 'node:path'; | ||
const validConfigExtensions = ['.js', '.mjs', '.cjs', '.json']; | ||
export async function getConfig(flags) { | ||
// Check to see if a config file path was passed | ||
const configPath = flags.config || 'linkinator.config.json'; | ||
let config = {}; | ||
let config; | ||
if (flags.config) { | ||
config = await parseConfigFile(configPath); | ||
config = await parseConfigFile(flags.config); | ||
} | ||
else { | ||
config = (await tryGetDefaultConfig()) || {}; | ||
} | ||
// `meow` is set up to pass boolean flags as `undefined` if not passed. | ||
@@ -17,3 +20,2 @@ // copy the struct, and delete properties that are `undefined` so the merge | ||
if (value === undefined || (Array.isArray(value) && value.length === 0)) { | ||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
delete strippedFlags[key]; | ||
@@ -27,3 +29,17 @@ } | ||
} | ||
const validConfigExtensions = ['.js', '.mjs', '.cjs', '.json']; | ||
/** | ||
* Attempt to load `linkinator.config.json`, assuming the user hasn't | ||
* passed a specific path to a config. | ||
* @returns The contents of the default config if present, or an empty config. | ||
*/ | ||
async function tryGetDefaultConfig() { | ||
const defaultConfigPath = path.join(process.cwd(), 'linkinator.config.json'); | ||
try { | ||
const config = await parseConfigFile(defaultConfigPath); | ||
return config; | ||
} | ||
catch (e) { | ||
return {}; | ||
} | ||
} | ||
async function parseConfigFile(configPath) { | ||
@@ -57,3 +73,2 @@ const typeOfConfig = getTypeOfConfig(configPath); | ||
// whoever is reading the code. | ||
// eslint-disable-next-line no-new-func | ||
const _import = new Function('p', 'return import(p)'); | ||
@@ -60,0 +75,0 @@ const config = (await _import(`file://${path.resolve(process.cwd(), configPath)}`)); |
@@ -1,7 +0,5 @@ | ||
/// <reference types="node" resolution-mode="require"/> | ||
import { EventEmitter } from 'node:events'; | ||
import { type GaxiosResponse } from 'gaxios'; | ||
import { type CheckOptions } from './options.js'; | ||
import { Queue } from './queue.js'; | ||
import { type CheckOptions } from './options.js'; | ||
export { getConfig } from './config.js'; | ||
export declare enum LinkState { | ||
@@ -8,0 +6,0 @@ OK = "OK", |
@@ -5,7 +5,6 @@ import { EventEmitter } from 'node:events'; | ||
import { request } from 'gaxios'; | ||
import { getLinks } from './links.js'; | ||
import { processOptions, } from './options.js'; | ||
import { Queue } from './queue.js'; | ||
import { getLinks } from './links.js'; | ||
import { startWebServer } from './server.js'; | ||
import { processOptions, } from './options.js'; | ||
export { getConfig } from './config.js'; | ||
export var LinkState; | ||
@@ -25,2 +24,3 @@ (function (LinkState) { | ||
export class LinkChecker extends EventEmitter { | ||
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||
on(event, listener) { | ||
@@ -45,3 +45,3 @@ return super.on(event, listener); | ||
server = await startWebServer({ | ||
root: options.serverRoot, | ||
root: options.serverRoot ?? '', | ||
port, | ||
@@ -166,2 +166,5 @@ markdown: options.markdown, | ||
const timeout = options.delayCache.get(options.url.host); | ||
if (timeout === undefined) { | ||
throw new Error('timeout not found'); | ||
} | ||
if (timeout > Date.now()) { | ||
@@ -216,3 +219,4 @@ options.queue.add(async () => { | ||
try { | ||
// Some sites don't respond to a stream response type correctly, especially with a HEAD. Try a GET with a text response type | ||
// Some sites don't respond well to HEAD requests, even if they don't return a 405. | ||
// This is a last gasp effort to see if the link is valid. | ||
if ((response === undefined || | ||
@@ -252,3 +256,3 @@ response.status < 200 || | ||
} | ||
else { | ||
else if (response !== undefined) { | ||
failures.push(response); | ||
@@ -302,5 +306,8 @@ } | ||
options.queue.add(async () => { | ||
if (result.url === undefined) { | ||
throw new Error('url is undefined'); | ||
} | ||
await this.crawl({ | ||
url: result.url, | ||
crawl, | ||
crawl: crawl ?? false, | ||
cache: options.cache, | ||
@@ -350,5 +357,5 @@ delayCache: options.delayCache, | ||
// Check to see if there is already a request to wait for this host | ||
if (options.delayCache.has(options.url.host)) { | ||
const currentTimeout = options.delayCache.get(options.url.host); | ||
if (currentTimeout !== undefined) { | ||
// Use whichever time is higher in the cache | ||
const currentTimeout = options.delayCache.get(options.url.host); | ||
if (retryAfter > currentTimeout) { | ||
@@ -390,5 +397,6 @@ options.delayCache.set(options.url.host, retryAfter); | ||
let currentRetries = 1; | ||
if (options.retryErrorsCache.has(options.url.href)) { | ||
const cachedRetries = options.retryErrorsCache.get(options.url.href); | ||
if (cachedRetries !== undefined) { | ||
// Use whichever time is higher in the cache | ||
currentRetries = options.retryErrorsCache.get(options.url.href); | ||
currentRetries = cachedRetries; | ||
if (currentRetries > maxRetries) | ||
@@ -395,0 +403,0 @@ return false; |
@@ -1,3 +0,2 @@ | ||
/// <reference types="node" resolution-mode="require"/> | ||
import { type Readable } from 'node:stream'; | ||
import type { Readable } from 'node:stream'; | ||
export type ParsedUrl = { | ||
@@ -4,0 +3,0 @@ link: string; |
@@ -49,3 +49,2 @@ import { WritableStream } from 'htmlparser2/lib/WritableStream'; | ||
// ignore href properties for link tags where rel is likely to fail | ||
// eslint-disable-next-line unicorn/prevent-abbreviations | ||
const relValuesToIgnore = ['dns-prefetch', 'preconnect']; | ||
@@ -59,3 +58,2 @@ if (tag === 'link' && relValuesToIgnore.includes(attributes.rel)) { | ||
try { | ||
// eslint-disable-next-line no-new | ||
new URL(attributes.content); | ||
@@ -62,0 +60,0 @@ } |
@@ -52,3 +52,2 @@ import { promises as fs } from 'node:fs'; | ||
fullPath = fullPath.split(path.sep).join('/'); | ||
// eslint-disable-next-line no-await-in-loop | ||
const expandedPaths = await glob(fullPath); | ||
@@ -99,3 +98,3 @@ if (expandedPaths.length === 0) { | ||
const pathParts = options.path[0].split(path.sep); | ||
options.path = [path.join('.', pathParts.at(-1))]; | ||
options.path = [path.join('.', pathParts.at(-1) ?? '')]; | ||
options.serverRoot = pathParts.slice(0, -1).join(path.sep) || '.'; | ||
@@ -102,0 +101,0 @@ } |
@@ -1,2 +0,1 @@ | ||
/// <reference types="node" resolution-mode="require"/> | ||
import { EventEmitter } from 'node:events'; | ||
@@ -3,0 +2,0 @@ export type QueueOptions = { |
@@ -18,2 +18,3 @@ import { EventEmitter } from 'node:events'; | ||
} | ||
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||
on(event, listener) { | ||
@@ -46,3 +47,2 @@ return super.on(event, listener); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/prefer-for-of | ||
for (let i = 0; i < this.q.length; i++) { | ||
@@ -55,2 +55,5 @@ // Check if we have too many concurrent functions executing | ||
const item = this.q.shift(); | ||
if (item === undefined) { | ||
throw new Error('unexpected undefined item in queue'); | ||
} | ||
// Make sure this element is ready to execute - if not, to the back of the stack | ||
@@ -60,3 +63,2 @@ if (item.timeToRun <= Date.now()) { | ||
this.activeFunctions++; | ||
// eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
item.fn().finally(() => { | ||
@@ -63,0 +65,0 @@ this.activeFunctions--; |
@@ -1,2 +0,1 @@ | ||
/// <reference types="node" resolution-mode="require"/> | ||
import http from 'node:http'; | ||
@@ -3,0 +2,0 @@ export type WebServerOptions = { |
@@ -0,8 +1,8 @@ | ||
import { Buffer } from 'node:buffer'; | ||
import { promises as fs } from 'node:fs'; | ||
import http from 'node:http'; | ||
import path from 'node:path'; | ||
import { Buffer } from 'node:buffer'; | ||
import { promises as fs } from 'node:fs'; | ||
import escapeHtml from 'escape-html'; | ||
import { marked } from 'marked'; | ||
import mime from 'mime'; | ||
import escape from 'escape-html'; | ||
import enableDestroy from 'server-destroy'; | ||
@@ -35,3 +35,2 @@ /** | ||
.filter(Boolean) | ||
// eslint-disable-next-line unicorn/no-array-callback-reference | ||
.map(decodeURIComponent); | ||
@@ -58,3 +57,3 @@ const originalPath = path.join(root, ...pathParts); | ||
response.setHeader('Content-Length', Buffer.byteLength(document)); | ||
response.setHeader('Location', request.url + '/'); | ||
response.setHeader('Location', `${request.url}/`); | ||
response.end(document); | ||
@@ -86,3 +85,3 @@ return; | ||
} | ||
response.setHeader('Content-Type', mimeType); | ||
response.setHeader('Content-Type', mimeType || ''); | ||
response.setHeader('Content-Length', Buffer.byteLength(data)); | ||
@@ -97,3 +96,3 @@ response.writeHead(200); | ||
const fileList = files | ||
.filter((f) => escape(f)) | ||
.filter((f) => escapeHtml(f)) | ||
.map((f) => `<li><a href="${f}">${f}</a></li>`) | ||
@@ -106,3 +105,2 @@ .join('\r\n'); | ||
catch (error_) { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const error__ = error_; | ||
@@ -109,0 +107,0 @@ return404(response, error__); |
150
package.json
{ | ||
"name": "linkinator", | ||
"description": "Find broken links, missing images, etc in your HTML. Scurry around your site and find all those broken links.", | ||
"version": "6.0.6", | ||
"license": "MIT", | ||
"repository": "JustinBeckwith/linkinator", | ||
"author": "Justin Beckwith", | ||
"exports": "./build/src/index.js", | ||
"types": "./build/src/index.d.ts", | ||
"type": "module", | ||
"bin": { | ||
"linkinator": "./build/src/cli.js" | ||
}, | ||
"scripts": { | ||
"pretest": "npm run compile", | ||
"prepare": "npm run compile", | ||
"coverage": "c8 report --reporter=json", | ||
"compile": "tsc -p .", | ||
"test": "c8 mocha build/test", | ||
"fix": "xo --prettier --fix", | ||
"lint": "xo --prettier", | ||
"build-binaries": "pkg . --out-path build/binaries", | ||
"docs-test": "node build/src/cli.js ./README.md" | ||
}, | ||
"dependencies": { | ||
"chalk": "^5.0.0", | ||
"escape-html": "^1.0.3", | ||
"gaxios": "^6.0.0", | ||
"glob": "^10.3.10", | ||
"htmlparser2": "^9.0.0", | ||
"marked": "^13.0.0", | ||
"meow": "^13.0.0", | ||
"mime": "^4.0.0", | ||
"server-destroy": "^1.0.1", | ||
"srcset": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/escape-html": "^1.0.1", | ||
"@types/mocha": "^10.0.0", | ||
"@types/node": "^20.0.0", | ||
"@types/server-destroy": "^1.0.1", | ||
"@types/sinon": "^17.0.0", | ||
"c8": "^10.0.0", | ||
"execa": "^9.0.0", | ||
"mocha": "^10.0.0", | ||
"nock": "^13.2.1", | ||
"pkg": "^5.4.1", | ||
"semantic-release": "^24.0.0", | ||
"sinon": "^18.0.0", | ||
"strip-ansi": "^7.0.1", | ||
"typescript": "^5.0.0", | ||
"xo": "^0.58.0" | ||
}, | ||
"engines": { | ||
"node": ">=18" | ||
}, | ||
"files": [ | ||
"build/src" | ||
], | ||
"keywords": [ | ||
"404", | ||
"html", | ||
"hyperlink", | ||
"links", | ||
"seo", | ||
"url", | ||
"broken link checker", | ||
"broken", | ||
"link", | ||
"checker" | ||
], | ||
"xo": { | ||
"rules": { | ||
"unicorn/prefer-event-target": "off", | ||
"complexity": "off", | ||
"@typescript-eslint/prefer-nullish-coalescing": "off" | ||
}, | ||
"ignores": [ | ||
"test/fixtures" | ||
] | ||
} | ||
"name": "linkinator", | ||
"description": "Find broken links, missing images, etc in your HTML. Scurry around your site and find all those broken links.", | ||
"version": "6.0.7", | ||
"license": "MIT", | ||
"repository": "JustinBeckwith/linkinator", | ||
"author": "Justin Beckwith", | ||
"exports": "./build/src/index.js", | ||
"types": "./build/src/index.d.ts", | ||
"type": "module", | ||
"bin": { | ||
"linkinator": "./build/src/cli.js" | ||
}, | ||
"scripts": { | ||
"pretest": "npm run build", | ||
"prepare": "npm run build", | ||
"coverage": "c8 report --reporter=json", | ||
"build": "tsc -p .", | ||
"test": "c8 mocha build/test", | ||
"fix": "biome check --write .", | ||
"lint": "biome check .", | ||
"build-binaries": "pkg . --out-path build/binaries", | ||
"docs-test": "node build/src/cli.js ./README.md" | ||
}, | ||
"dependencies": { | ||
"chalk": "^5.0.0", | ||
"escape-html": "^1.0.3", | ||
"gaxios": "^6.0.0", | ||
"glob": "^10.3.10", | ||
"htmlparser2": "^9.0.0", | ||
"marked": "^13.0.0", | ||
"meow": "^13.0.0", | ||
"mime": "^4.0.0", | ||
"server-destroy": "^1.0.1", | ||
"srcset": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"@biomejs/biome": "1.8.3", | ||
"@types/escape-html": "^1.0.1", | ||
"@types/mocha": "^10.0.0", | ||
"@types/node": "^20.0.0", | ||
"@types/server-destroy": "^1.0.1", | ||
"@types/sinon": "^17.0.0", | ||
"c8": "^10.0.0", | ||
"execa": "^9.0.0", | ||
"mocha": "^10.0.0", | ||
"nock": "^13.2.1", | ||
"pkg": "^5.4.1", | ||
"semantic-release": "^24.0.0", | ||
"sinon": "^18.0.0", | ||
"strip-ansi": "^7.0.1", | ||
"typescript": "^5.5.2" | ||
}, | ||
"engines": { | ||
"node": ">=18" | ||
}, | ||
"files": [ | ||
"build/src" | ||
], | ||
"keywords": [ | ||
"404", | ||
"html", | ||
"hyperlink", | ||
"links", | ||
"seo", | ||
"url", | ||
"broken link checker", | ||
"broken", | ||
"link", | ||
"checker" | ||
] | ||
} |
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
1490
69857