linkinator
Version:
Find broken links, missing images, etc in your HTML. Scurry around your site and find all those broken links.
147 lines (146 loc) • 5.74 kB
JavaScript
import { Buffer } from 'node:buffer';
import { promises as fs } from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import escapeHtml from 'escape-html';
import { marked } from 'marked';
import { gfmHeadingId } from 'marked-gfm-heading-id';
import mime from 'mime';
import enableDestroy from 'server-destroy';
// Configure marked to generate GitHub-style heading IDs for fragment validation
marked.use(gfmHeadingId());
/**
* Spin up a local HTTP server to serve static requests from disk
* @private
* @returns Promise that resolves with the instance of the HTTP server
*/
export async function startWebServer(options) {
const root = path.resolve(options.root);
return new Promise((resolve, reject) => {
const server = http
.createServer(async (request, response) => handleRequest(request, response, root, options))
.listen(options.port || 0, () => {
resolve(server);
})
.on('error', reject);
if (!options.port) {
const addr = server.address();
options.port = addr.port;
}
enableDestroy(server);
});
}
async function handleRequest(request, response, root, options) {
const url = new URL(request.url || '/', `http://localhost:${options.port}`);
const pathParts = url.pathname
.split('/')
.filter(Boolean)
.map(decodeURIComponent);
const originalPath = path.join(root, ...pathParts);
if (url.pathname.endsWith('/')) {
pathParts.push('index.html');
}
const localPath = path.join(root, ...pathParts);
if (!localPath.startsWith(root)) {
response.writeHead(500);
response.end();
return;
}
const maybeListing = options.directoryListing && localPath.endsWith(`${path.sep}index.html`);
try {
const stats = await fs.stat(localPath);
const isDirectory = stats.isDirectory();
if (isDirectory) {
// This means we got a path with no / at the end!
// Create a proper redirect URL that preserves query parameters
// Fix for issue #595 - thanks to @maddsua for the solution in PR #596
const redirectUrl = new URL(url);
if (!redirectUrl.pathname.endsWith('/')) {
redirectUrl.pathname += '/';
}
const document = "<html><body>Redirectin'</body></html>";
response.statusCode = 301;
response.setHeader('Content-Type', 'text/html; charset=UTF-8');
response.setHeader('Content-Length', Buffer.byteLength(document));
response.setHeader('Location', redirectUrl.href);
response.end(document);
return;
}
}
catch (error) {
const error_ = error;
// Try clean URLs: if file not found and cleanUrls is enabled, try adding .html
if (options.cleanUrls && !localPath.endsWith('.html')) {
try {
const htmlPath = `${localPath}.html`;
// Verify it's still within the root directory (security check)
if (htmlPath.startsWith(root)) {
const htmlStats = await fs.stat(htmlPath);
if (!htmlStats.isDirectory()) {
// File exists! Serve it with the original URL
const data = await fs.readFile(htmlPath, { encoding: 'utf8' });
const mimeType = 'text/html; charset=UTF-8';
response.setHeader('Content-Type', mimeType);
response.setHeader('Content-Length', Buffer.byteLength(data));
response.writeHead(200);
response.end(data);
return;
}
}
}
catch {
// Fall through to normal 404 handling
}
}
if (!maybeListing) {
return404(response, error_);
return;
}
}
try {
let data = await fs.readFile(localPath, { encoding: 'utf8' });
let mimeType = mime.getType(localPath);
const isMarkdown = request.url?.toLocaleLowerCase().endsWith('.md');
if (isMarkdown && options.markdown) {
const markedData = marked(data, { gfm: true });
if (typeof markedData === 'string') {
data = markedData;
}
else if ((typeof markedData === 'object' || typeof markedData === 'function') &&
typeof markedData.then === 'function') {
data = await markedData;
}
mimeType = 'text/html; charset=UTF-8';
}
response.setHeader('Content-Type', mimeType || '');
response.setHeader('Content-Length', Buffer.byteLength(data));
response.writeHead(200);
response.end(data);
}
catch (error) {
if (maybeListing) {
try {
const files = await fs.readdir(originalPath);
const fileList = files
.filter((f) => escapeHtml(f))
.map((f) => `<li><a href="${f}">${f}</a></li>`)
.join('\r\n');
const data = `<html><body><ul>${fileList}</ul></body></html>`;
response.writeHead(200);
response.end(data);
}
catch (error_) {
const error__ = error_;
return404(response, error__);
}
}
else {
const error_ = error;
return404(response, error_);
}
}
}
function return404(response, error) {
response.writeHead(404);
response.end(JSON.stringify(error));
}