Files
WRNexusJS/packages/dev-toolbar/src/rules/links.ts
T
2026-07-21 12:30:46 +05:30

70 lines
2.4 KiB
TypeScript

import type { DevToolbarRule } from "./types.ts";
import { createIssue } from "./helpers.ts";
export const linkRules: DevToolbarRule[] = [
{
id: "links/basics",
category: "links",
defaultSeverity: "warning",
description: "Checks unsafe and incomplete links.",
run: ({ root, url }) => {
const issues = [];
for (const anchor of root.querySelectorAll<HTMLAnchorElement>("a")) {
const raw = anchor.getAttribute("href");
if (!raw || raw === "#")
issues.push(
createIssue({
ruleId: "links/empty",
category: "links",
severity: "warning",
title: "Link has no useful destination",
message: `The href is ${raw ? '"#"' : "missing"}.`,
element: anchor,
recommendation: "Provide a real URL or use a button for an action.",
}),
);
if (raw?.trim().toLowerCase().startsWith("javascript:"))
issues.push(
createIssue({
ruleId: "links/javascript-url",
category: "security",
severity: "error",
title: "JavaScript URL used",
message: "javascript: links are unsafe and inaccessible.",
element: anchor,
recommendation: "Use a button and a normal event handler.",
}),
);
if (
anchor.target === "_blank" &&
!anchor.rel.split(/\s+/).some((value) => value === "noopener" || value === "noreferrer")
)
issues.push(
createIssue({
ruleId: "links/blank-rel",
category: "security",
severity: "warning",
title: "New-tab link lacks rel protection",
message: "The opened page may retain access to window.opener.",
element: anchor,
recommendation: 'Add rel="noopener noreferrer".',
}),
);
if (raw?.startsWith("http://") && url.protocol === "https:")
issues.push(
createIssue({
ruleId: "links/mixed-content",
category: "security",
severity: "error",
title: "Insecure link on HTTPS page",
message: "This link uses HTTP from an HTTPS page.",
element: anchor,
recommendation: "Use an HTTPS destination.",
}),
);
}
return issues;
},
},
];