check-html-links
Advanced tools
Comparing version 0.2.3 to 0.2.4
# check-html-links | ||
## 0.2.4 | ||
### Patch Changes | ||
- 97d5fb2: Add external links validation via the flag `--validate-externals`. | ||
You can/should provide an optional `--absolute-base-url` to handle urls starting with it as internal. | ||
```bash | ||
# check external urls | ||
npx check-html-links _site --validate-externals | ||
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal | ||
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev | ||
``` | ||
## 0.2.3 | ||
@@ -4,0 +20,0 @@ |
@@ -29,11 +29,13 @@ export interface Link { | ||
interface Options { | ||
export interface Options { | ||
ignoreLinkPatterns: string[] | null; | ||
validateExternals: boolean; | ||
absoluteBaseUrl: string; | ||
} | ||
export interface CheckHtmlLinksCliOptions { | ||
export interface CheckHtmlLinksCliOptions extends Options { | ||
printOnError: boolean; | ||
rootDir: string; | ||
ignoreLinkPatterns: string[] | null; | ||
continueOnError: boolean; | ||
absoluteBaseUrl: string; | ||
} |
{ | ||
"name": "check-html-links", | ||
"version": "0.2.3", | ||
"version": "0.2.4", | ||
"publishConfig": { | ||
@@ -40,4 +40,5 @@ "access": "public" | ||
"minimatch": "^3.0.4", | ||
"node-fetch": "^3.0.0", | ||
"sax-wasm": "^2.0.0", | ||
"slash": "^3.0.0" | ||
"slash": "^4.0.0" | ||
}, | ||
@@ -44,0 +45,0 @@ "devDependencies": { |
@@ -7,3 +7,3 @@ # Check HTML Links | ||
``` | ||
```shell | ||
npm i -D check-html-links | ||
@@ -14,7 +14,7 @@ ``` | ||
``` | ||
```bash | ||
npx check-html-links _site | ||
``` | ||
For docs please see our homepage [https://rocket.modern-web.dev/docs/tools/check-html-links/](https://rocket.modern-web.dev/docs/tools/check-html-links/). | ||
For docs please see our homepage [https://rocket.modern-web.dev/tools/check-html-links/overview/](https://rocket.modern-web.dev/tools/check-html-links/overview/). | ||
@@ -21,0 +21,0 @@ ## Comparison |
@@ -9,3 +9,3 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ | ||
import commandLineArgs from 'command-line-args'; | ||
import { validateFiles } from './validateFolder.js'; | ||
import { prepareFiles, validateFiles } from './validateFolder.js'; | ||
import { formatErrors } from './formatErrors.js'; | ||
@@ -22,3 +22,5 @@ import { listFiles } from './listFiles.js'; | ||
{ name: 'root-dir', type: String, defaultOption: true }, | ||
{ name: 'continue-on-error', type: Boolean, defaultOption: false }, | ||
{ name: 'continue-on-error', type: Boolean }, | ||
{ name: 'validate-externals', type: Boolean }, | ||
{ name: 'absolute-base-url', type: String }, | ||
]; | ||
@@ -34,2 +36,4 @@ const options = commandLineArgs(mainDefinitions, { | ||
ignoreLinkPatterns: options['ignore-link-pattern'], | ||
validateExternals: options['validate-externals'], | ||
absoluteBaseUrl: options['absolute-base-url'], | ||
}; | ||
@@ -49,19 +53,44 @@ } | ||
async run() { | ||
const { ignoreLinkPatterns, rootDir: userRootDir } = this.options; | ||
const { | ||
ignoreLinkPatterns, | ||
rootDir: userRootDir, | ||
validateExternals, | ||
absoluteBaseUrl, | ||
} = this.options; | ||
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd(); | ||
const performanceStart = process.hrtime(); | ||
console.log('👀 Checking if all internal links work...'); | ||
const files = await listFiles('**/*.html', rootDir); | ||
console.log('Check HTML Links'); | ||
const filesOutput = | ||
files.length == 0 | ||
? '🧐 No files to check. Did you select the correct folder?' | ||
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`; | ||
? ' 🧐 No files to check. Did you select the correct folder?' | ||
: ` 📖 Found ${chalk.green.bold(files.length)} files containing`; | ||
console.log(filesOutput); | ||
const { errors, numberLinks } = await validateFiles(files, rootDir, { ignoreLinkPatterns }); | ||
const { numberLinks, checkLocalFiles, checkExternalLinks } = await prepareFiles( | ||
files, | ||
rootDir, | ||
{ | ||
ignoreLinkPatterns, | ||
validateExternals, | ||
absoluteBaseUrl, | ||
}, | ||
); | ||
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`); | ||
console.log(` 🔗 ${chalk.green.bold(numberLinks)} internal links`); | ||
if (validateExternals) { | ||
console.log(` 🌐 ${chalk.green.bold(checkExternalLinks.length)} external links`); | ||
} | ||
console.log(' 👀 Checking...'); | ||
const { errors } = await validateFiles({ | ||
checkLocalFiles, | ||
validateExternals, | ||
checkExternalLinks, | ||
}); | ||
const performance = process.hrtime(performanceStart); | ||
@@ -77,3 +106,3 @@ /** @type {string[]} */ | ||
output = [ | ||
`❌ Found ${chalk.red.bold( | ||
` ❌ Found ${chalk.red.bold( | ||
errors.length.toString(), | ||
@@ -86,3 +115,3 @@ )} missing reference targets (used by ${referenceCount} links) while checking ${ | ||
.map(line => ` ${line}`), | ||
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`, | ||
` 🕑 Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`, | ||
]; | ||
@@ -98,3 +127,3 @@ message = output.join('\n'); | ||
console.log( | ||
`✅ All internal links are valid. (executed in ${performance[0]}s ${ | ||
` ✅ All tested links are valid. (executed in ${performance[0]}s ${ | ||
performance[1] / 1000000 | ||
@@ -101,0 +130,0 @@ }ms)`, |
@@ -18,3 +18,3 @@ import path from 'path'; | ||
output.push( | ||
`${number}. missing ${chalk.red.bold( | ||
` ${number}. missing ${chalk.red.bold( | ||
`id="${error.usage[0].anchor}"`, | ||
@@ -28,3 +28,3 @@ )} in ${chalk.cyanBright(filePath)}`, | ||
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`); | ||
output.push(` ${number}. missing ${title} ${chalk.red.bold(filePath)}`); | ||
} | ||
@@ -39,7 +39,7 @@ const usageLength = error.usage.length; | ||
const attributeEnd = chalk.gray('"'); | ||
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`); | ||
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`); | ||
} | ||
if (usageLength > 3) { | ||
const more = chalk.red((usageLength - 3).toString()); | ||
output.push(` ... ${more} more references to this target`); | ||
output.push(` ... ${more} more references to this target`); | ||
} | ||
@@ -46,0 +46,0 @@ output.push(''); |
@@ -6,6 +6,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ | ||
import { createRequire } from 'module'; | ||
import { listFiles } from './listFiles.js'; | ||
import path from 'path'; | ||
import slash from 'slash'; | ||
import { listFiles } from './listFiles.js'; | ||
import { checkLinks } from './checkLinks.js'; | ||
@@ -33,2 +33,5 @@ /** @typedef {import('../types/main').Link} Link */ | ||
/** @type {Error[]} */ | ||
let checkExternalLinks = []; | ||
/** @type {Error[]} */ | ||
let errors = []; | ||
@@ -157,2 +160,22 @@ | ||
/** | ||
* @param {string} filePath | ||
* @param {Usage} usageObj | ||
*/ | ||
function addExternalLink(filePath, usageObj) { | ||
const foundIndex = checkExternalLinks.findIndex(item => { | ||
return item.filePath === filePath; | ||
}); | ||
if (foundIndex === -1) { | ||
checkExternalLinks.push({ | ||
filePath, | ||
onlyAnchorMissing: false, | ||
usage: [usageObj], | ||
}); | ||
} else { | ||
checkExternalLinks[foundIndex].usage.push(usageObj); | ||
} | ||
} | ||
/** | ||
* @param {string} inValue | ||
@@ -206,8 +229,13 @@ */ | ||
* @param {string} options.rootDir | ||
* @param {string} options.absoluteBaseUrl | ||
* @param {function(string): boolean} options.ignoreUsage | ||
*/ | ||
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) { | ||
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage, absoluteBaseUrl }) { | ||
for (const hrefObj of links) { | ||
const { value, anchor } = getValueAndAnchor(hrefObj.value); | ||
const { value: rawValue, anchor } = getValueAndAnchor(hrefObj.value); | ||
const value = rawValue.startsWith(absoluteBaseUrl) | ||
? rawValue.substring(absoluteBaseUrl.length) | ||
: rawValue; | ||
const usageObj = { | ||
@@ -236,4 +264,3 @@ attribute: hrefObj.attribute, | ||
} else if (value.startsWith('//') || value.startsWith('http')) { | ||
// TODO: handle external urls | ||
// external url - we do not handle that (yet) | ||
addExternalLink(htmlFilePath, usageObj); | ||
} else if (value.startsWith('/')) { | ||
@@ -252,3 +279,3 @@ const filePath = path.join(rootDir, valueFile); | ||
return { checkLocalFiles: [...checkLocalFiles] }; | ||
return { checkLocalFiles: [...checkLocalFiles], checkExternalLinks: [...checkExternalLinks] }; | ||
} | ||
@@ -293,2 +320,18 @@ | ||
/** | ||
* | ||
* @param {Error[]} checkExternalLinks | ||
*/ | ||
async function validateExternalLinks(checkExternalLinks) { | ||
for await (const localFileObj of checkExternalLinks) { | ||
const links = localFileObj.usage.map(usage => usage.value); | ||
const results = await checkLinks(links); | ||
localFileObj.usage = localFileObj.usage.filter((link, index) => !results[index]); | ||
if (localFileObj.usage.length > 0) { | ||
errors.push(localFileObj); | ||
} | ||
} | ||
return errors; | ||
} | ||
/** | ||
* @param {string[]} files | ||
@@ -298,3 +341,3 @@ * @param {string} rootDir | ||
*/ | ||
export async function validateFiles(files, rootDir, opts) { | ||
export async function prepareFiles(files, rootDir, opts) { | ||
await parserReferences.prepareWasm(saxWasmBuffer); | ||
@@ -305,2 +348,3 @@ await parserIds.prepareWasm(saxWasmBuffer); | ||
checkLocalFiles = []; | ||
checkExternalLinks = []; | ||
idCache = new Map(); | ||
@@ -321,7 +365,23 @@ let numberLinks = 0; | ||
numberLinks += links.length; | ||
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }); | ||
await resolveLinks(links, { | ||
htmlFilePath, | ||
rootDir, | ||
ignoreUsage, | ||
absoluteBaseUrl: opts?.absoluteBaseUrl, | ||
}); | ||
} | ||
return { checkLocalFiles, checkExternalLinks, numberLinks }; | ||
} | ||
/** | ||
* @param {*} param0 | ||
* @returns | ||
*/ | ||
export async function validateFiles({ checkLocalFiles, validateExternals, checkExternalLinks }) { | ||
await validateLocalFiles(checkLocalFiles); | ||
if (validateExternals) { | ||
await validateExternalLinks(checkExternalLinks); | ||
} | ||
return { errors: errors, numberLinks: numberLinks }; | ||
return { errors }; | ||
} | ||
@@ -336,4 +396,12 @@ | ||
const files = await listFiles('**/*.html', rootDir); | ||
const { errors } = await validateFiles(files, rootDir, opts); | ||
const { checkLocalFiles, checkExternalLinks } = await prepareFiles(files, rootDir, opts); | ||
const { errors } = await validateFiles({ | ||
checkLocalFiles, | ||
validateExternals: opts?.validateExternals, | ||
checkExternalLinks, | ||
}); | ||
return errors; | ||
} |
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
Network access
Supply chain riskThis module accesses the network.
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
642
24281
7
11
1
+ Addednode-fetch@^3.0.0
+ Addeddata-uri-to-buffer@4.0.1(transitive)
+ Addedfetch-blob@3.2.0(transitive)
+ Addedformdata-polyfill@4.0.10(transitive)
+ Addednode-domexception@1.0.0(transitive)
+ Addednode-fetch@3.3.2(transitive)
+ Addedslash@4.0.0(transitive)
+ Addedweb-streams-polyfill@3.3.3(transitive)
- Removedslash@3.0.0(transitive)
Updatedslash@^4.0.0