mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-21 08:51:18 +00:00
Compare commits
4 Commits
@rocket/la
...
@rocket/cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8082fbac8 | ||
|
|
68e05f4d4a | ||
|
|
660f64c320 | ||
|
|
97d5fb2040 |
@@ -81,7 +81,7 @@
|
||||
"husky": "^4.3.7",
|
||||
"lint-staged": "^10.5.3",
|
||||
"mocha": "^9.1.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^3.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"onchange": "^7.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -14,7 +14,7 @@ npm i -D check-html-links
|
||||
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/).
|
||||
|
||||
## Comparison
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "check-html-links",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -37,6 +37,7 @@
|
||||
"command-line-args": "^5.1.1",
|
||||
"glob": "^7.0.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^3.0.0",
|
||||
"sax-wasm": "^2.0.0",
|
||||
"slash": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { validateFiles } from './validateFolder.js';
|
||||
import { prepareFiles, validateFiles } from './validateFolder.js';
|
||||
import { formatErrors } from './formatErrors.js';
|
||||
import { listFiles } from './listFiles.js';
|
||||
|
||||
@@ -18,7 +18,9 @@ export class CheckHtmlLinksCli {
|
||||
const mainDefinitions = [
|
||||
{ name: 'ignore-link-pattern', type: String, multiple: true },
|
||||
{ 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 },
|
||||
];
|
||||
const options = commandLineArgs(mainDefinitions, {
|
||||
stopAtFirstUnknown: true,
|
||||
@@ -29,6 +31,8 @@ export class CheckHtmlLinksCli {
|
||||
continueOnError: options['continue-on-error'],
|
||||
rootDir: options['root-dir'],
|
||||
ignoreLinkPatterns: options['ignore-link-pattern'],
|
||||
validateExternals: options['validate-externals'],
|
||||
absoluteBaseUrl: options['absolute-base-url'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,22 +47,47 @@ export class CheckHtmlLinksCli {
|
||||
}
|
||||
|
||||
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);
|
||||
/** @type {string[]} */
|
||||
@@ -70,7 +99,7 @@ export class CheckHtmlLinksCli {
|
||||
referenceCount += error.usage.length;
|
||||
}
|
||||
output = [
|
||||
`❌ Found ${chalk.red.bold(
|
||||
` ❌ Found ${chalk.red.bold(
|
||||
errors.length.toString(),
|
||||
)} missing reference targets (used by ${referenceCount} links) while checking ${
|
||||
files.length
|
||||
@@ -78,7 +107,7 @@ export class CheckHtmlLinksCli {
|
||||
...formatErrors(errors)
|
||||
.split('\n')
|
||||
.map(line => ` ${line}`),
|
||||
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
|
||||
` 🕑 Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
|
||||
];
|
||||
message = output.join('\n');
|
||||
if (this.options.printOnError === true) {
|
||||
@@ -89,7 +118,7 @@ export class CheckHtmlLinksCli {
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}ms)`,
|
||||
);
|
||||
|
||||
63
packages/check-html-links/src/checkLinks.js
Normal file
63
packages/check-html-links/src/checkLinks.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
/**
|
||||
* @type {Map<string,boolean>}
|
||||
*/
|
||||
const resultsMap = new Map();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {boolean} result
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const memorizeCheckup = (url, result) => {
|
||||
resultsMap.set(url, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @returns
|
||||
*/
|
||||
const fetchWrap = async (url, method = 'GET') => {
|
||||
return Promise.race([
|
||||
fetch(url, { method })
|
||||
.then(response => response.ok)
|
||||
.catch(() => false),
|
||||
new Promise(resolve => setTimeout(resolve, 10000, false)),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const fetchHead = async url => fetchWrap(url, 'HEAD');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url - URL object to check
|
||||
* @returns {Promise<boolean>} true if url is alive or false if not
|
||||
*/
|
||||
const checkUrl = async url =>
|
||||
(fetchHead(url) || fetchWrap(url)).then(result => memorizeCheckup(url, result));
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} link - link string to check
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const checkLink = async link => {
|
||||
const url = link.startsWith('//') ? `https:${link}` : link;
|
||||
return resultsMap.get(url) ?? checkUrl(url);
|
||||
};
|
||||
/**
|
||||
* Check an array of links and return an object with
|
||||
*
|
||||
* @param {string[]} links Links to check
|
||||
*/
|
||||
export const checkLinks = async links => Promise.all(links.map(checkLink));
|
||||
@@ -15,7 +15,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const filePath = path.relative(relativeFrom, error.filePath);
|
||||
if (error.onlyAnchorMissing === true) {
|
||||
output.push(
|
||||
`${number}. missing ${chalk.red.bold(
|
||||
` ${number}. missing ${chalk.red.bold(
|
||||
`id="${error.usage[0].anchor}"`,
|
||||
)} in ${chalk.cyanBright(filePath)}`,
|
||||
);
|
||||
@@ -24,7 +24,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const title =
|
||||
firstAttribute === 'src' || firstAttribute === 'srcset' ? 'file' : 'reference target';
|
||||
|
||||
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`);
|
||||
output.push(` ${number}. missing ${title} ${chalk.red.bold(filePath)}`);
|
||||
}
|
||||
const usageLength = error.usage.length;
|
||||
|
||||
@@ -34,11 +34,11 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const clickAbleLink = chalk.cyanBright(`${usagePath}:${usage.line + 1}:${usage.character}`);
|
||||
const attributeStart = chalk.gray(`${usage.attribute}="`);
|
||||
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`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import fs from 'fs';
|
||||
import saxWasm from 'sax-wasm';
|
||||
import minimatch from 'minimatch';
|
||||
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';
|
||||
|
||||
/** @typedef {import('../types/main').Link} Link */
|
||||
/** @typedef {import('../types/main').LocalFile} LocalFile */
|
||||
@@ -28,6 +28,9 @@ const parserIds = new SAXParser(SaxEventType.Attribute, streamOptions);
|
||||
/** @type {Error[]} */
|
||||
let checkLocalFiles = [];
|
||||
|
||||
/** @type {Error[]} */
|
||||
let checkExternalLinks = [];
|
||||
|
||||
/** @type {Error[]} */
|
||||
let errors = [];
|
||||
|
||||
@@ -151,6 +154,26 @@ function addLocalFile(filePath, anchor, usageObj) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
@@ -200,11 +223,16 @@ function isNonHttpSchema(url) {
|
||||
* @param {object} options
|
||||
* @param {string} options.htmlFilePath
|
||||
* @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 = {
|
||||
attribute: hrefObj.attribute,
|
||||
@@ -229,8 +257,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
|
||||
} else if (valueFile === '' && anchor !== '') {
|
||||
addLocalFile(htmlFilePath, anchor, usageObj);
|
||||
} 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('/')) {
|
||||
const filePath = path.join(rootDir, valueFile);
|
||||
addLocalFile(filePath, anchor, usageObj);
|
||||
@@ -244,7 +271,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
|
||||
}
|
||||
}
|
||||
|
||||
return { checkLocalFiles: [...checkLocalFiles] };
|
||||
return { checkLocalFiles: [...checkLocalFiles], checkExternalLinks: [...checkExternalLinks] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,17 +310,34 @@ async function validateLocalFiles(checkLocalFiles) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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
|
||||
* @param {string} rootDir
|
||||
* @param {Options} opts?
|
||||
*/
|
||||
export async function validateFiles(files, rootDir, opts) {
|
||||
export async function prepareFiles(files, rootDir, opts) {
|
||||
await parserReferences.prepareWasm(saxWasmBuffer);
|
||||
await parserIds.prepareWasm(saxWasmBuffer);
|
||||
|
||||
errors = [];
|
||||
checkLocalFiles = [];
|
||||
checkExternalLinks = [];
|
||||
idCache = new Map();
|
||||
let numberLinks = 0;
|
||||
|
||||
@@ -309,11 +353,27 @@ export async function validateFiles(files, rootDir, opts) {
|
||||
for (const htmlFilePath of files) {
|
||||
const { links } = await extractReferences(htmlFilePath);
|
||||
numberLinks += links.length;
|
||||
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
|
||||
await resolveLinks(links, {
|
||||
htmlFilePath,
|
||||
rootDir,
|
||||
ignoreUsage,
|
||||
absoluteBaseUrl: opts?.absoluteBaseUrl,
|
||||
});
|
||||
}
|
||||
await validateLocalFiles(checkLocalFiles);
|
||||
return { checkLocalFiles, checkExternalLinks, numberLinks };
|
||||
}
|
||||
|
||||
return { errors: errors, numberLinks: numberLinks };
|
||||
/**
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export async function validateFiles({ checkLocalFiles, validateExternals, checkExternalLinks }) {
|
||||
await validateLocalFiles(checkLocalFiles);
|
||||
if (validateExternals) {
|
||||
await validateExternalLinks(checkExternalLinks);
|
||||
}
|
||||
|
||||
return { errors };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +383,14 @@ export async function validateFiles(files, rootDir, opts) {
|
||||
export async function validateFolder(inRootDir, opts) {
|
||||
const rootDir = path.resolve(inRootDir);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!-- ignore known subsystems -->
|
||||
<a href="/docs/"></a>
|
||||
<a href="/developer/getting-started.html#js"></a>
|
||||
<a href="/developer/language-guides/"></a>
|
||||
<a href="/developer/javascript.html"></a>
|
||||
<!-- valid -->
|
||||
<a href="//rocket.modern-web.dev/"></a>
|
||||
<a href="http://rocket.modern-web.dev/"></a>
|
||||
<a href="https://rocket.modern-web.dev/"></a>
|
||||
|
||||
<!-- invalid -->
|
||||
<a href="//rocket.modern-web.dev/unexists-page/"></a>
|
||||
<a href="http://rocket.modern-web.dev/unexists-page/"></a>
|
||||
<a href="https://rocket.modern-web.dev/unexists-page/"></a>
|
||||
|
||||
@@ -5,8 +5,5 @@
|
||||
<a href="./page.html"></a>
|
||||
<a href=" ./page.html "></a>
|
||||
<a href=" /page.html "></a>
|
||||
<a href="//domain.com/something/"></a>
|
||||
<a href="http://domain.com/something/"></a>
|
||||
<a href="https://domain.com/something/"></a>
|
||||
<a href=""></a>
|
||||
<a href=":~:text=put%20your%20labels%20above%20your%20inputs">Sign-in form best practices</a>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
About
|
||||
@@ -0,0 +1,2 @@
|
||||
<a href="about.html">About</a>
|
||||
<a href="http://localhost/about.html">About Absolute</a>
|
||||
@@ -20,19 +20,19 @@ describe('formatErrors', () => {
|
||||
const result = await executeAndFormat('fixtures/test-case');
|
||||
expect(result.trim().split('\n')).to.deep.equal([
|
||||
'1. missing id="my-teams" in fixtures/test-case/price/index.html',
|
||||
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
|
||||
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
|
||||
'',
|
||||
'2. missing file fixtures/test-case/about/images/team.png',
|
||||
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
|
||||
' 2. missing file fixtures/test-case/about/images/team.png',
|
||||
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
|
||||
'',
|
||||
'3. missing reference target fixtures/test-case/aboot',
|
||||
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
|
||||
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
|
||||
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
|
||||
' ... 2 more references to this target',
|
||||
' 3. missing reference target fixtures/test-case/aboot',
|
||||
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
|
||||
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
|
||||
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
|
||||
' ... 2 more references to this target',
|
||||
'',
|
||||
'4. missing reference target fixtures/test-case/prce',
|
||||
' from fixtures/test-case/index.html:1:9 via href="./prce"',
|
||||
' 4. missing reference target fixtures/test-case/prce',
|
||||
' from fixtures/test-case/index.html:1:9 via href="./prce"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,74 @@ describe('validateFolder', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates external links', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/external-link', {
|
||||
validateExternals: true,
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
{
|
||||
filePath: 'fixtures/external-link/index.html',
|
||||
onlyAnchorMissing: false,
|
||||
usage: [
|
||||
{
|
||||
attribute: 'href',
|
||||
value: '//rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 6,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
{
|
||||
attribute: 'href',
|
||||
value: 'http://rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 7,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
{
|
||||
attribute: 'href',
|
||||
value: 'https://rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 8,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates links with own absolute base url as internal', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
|
||||
validateExternals: true,
|
||||
absoluteBaseUrl: 'http://localhost',
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('validates all full urls if there is no absoluteBaseUrl provided', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
|
||||
validateExternals: true,
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
{
|
||||
filePath: 'fixtures/internal-own-absolute-base-path/index.html',
|
||||
onlyAnchorMissing: false,
|
||||
usage: [
|
||||
{
|
||||
anchor: '',
|
||||
attribute: 'href',
|
||||
character: 9,
|
||||
file: 'fixtures/internal-own-absolute-base-path/index.html',
|
||||
line: 1,
|
||||
value: 'http://localhost/about.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups multiple usage of the same missing file', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-links-to-same-file');
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
|
||||
8
packages/check-html-links/types/main.d.ts
vendored
8
packages/check-html-links/types/main.d.ts
vendored
@@ -26,13 +26,15 @@ export interface Error {
|
||||
usage: Usage[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @rocket/cli
|
||||
|
||||
## 0.20.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 68e05f4: `rocket lint` can now validate external links.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
rocket lint --validate-externals
|
||||
```
|
||||
|
||||
- Updated dependencies [97d5fb2]
|
||||
- check-html-links@0.2.4
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/cli",
|
||||
"version": "0.20.3",
|
||||
"version": "0.20.4",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -54,7 +54,7 @@
|
||||
"dependencies": {
|
||||
"@rocket/building-rollup": "^0.4.0",
|
||||
"@rocket/engine": "^0.2.7",
|
||||
"check-html-links": "^0.2.3",
|
||||
"check-html-links": "^0.2.4",
|
||||
"colorette": "^2.0.16",
|
||||
"commander": "^9.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
|
||||
@@ -24,6 +24,7 @@ export class RocketLint {
|
||||
.command('lint')
|
||||
.option('-i, --input-dir <path>', 'path to where to search for source files')
|
||||
.option('-b, --build-html', 'do a quick html only build and then check links')
|
||||
.option('-e, --validate-externals', 'validate external links')
|
||||
.action(async options => {
|
||||
const { cliOptions, ...lintOptions } = options;
|
||||
cli.setOptions({
|
||||
@@ -77,6 +78,7 @@ export class RocketLint {
|
||||
rootDir: folderToCheck,
|
||||
printOnError: true,
|
||||
continueOnError: false,
|
||||
absoluteBaseUrl: this.cli.options.absoluteBaseUrl,
|
||||
...userCheckHtmlLinksOptions,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @rocket/launch
|
||||
|
||||
## 0.21.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 660f64c: Change the default gitBranch to main
|
||||
- Updated dependencies [68e05f4]
|
||||
- @rocket/cli@0.20.4
|
||||
|
||||
## 0.21.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/launch",
|
||||
"version": "0.21.2",
|
||||
"version": "0.21.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -46,7 +46,7 @@
|
||||
"preset"
|
||||
],
|
||||
"dependencies": {
|
||||
"@rocket/cli": "^0.20.3",
|
||||
"@rocket/cli": "^0.20.4",
|
||||
"@rocket/components": "^0.2.0",
|
||||
"@rocket/engine": "^0.2.7",
|
||||
"@webcomponents/template-shadowroot": "^0.1.0",
|
||||
|
||||
@@ -57,7 +57,7 @@ export class LayoutMain extends Layout {
|
||||
</picture>
|
||||
`,
|
||||
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
|
||||
gitBranch: 'next',
|
||||
gitBranch: 'main',
|
||||
description: 'A static site generator for modern web development',
|
||||
socialLinks: [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,6 @@ export const defaultGlobalOptions = {
|
||||
logoSrc: '/icon.svg',
|
||||
logoAlt: 'Rocket Logo',
|
||||
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
|
||||
gitBranch: 'master',
|
||||
gitBranch: 'main',
|
||||
description: 'A static site generator for modern web development',
|
||||
};
|
||||
|
||||
@@ -68,6 +68,18 @@ rocket lint --build-html
|
||||
|
||||
Note: We can do this as 2-4 generally does not impact links/references (as long as the optimizations scripts do not have related bugs)
|
||||
|
||||
### Check external links
|
||||
|
||||
`rocket lint` can also check external links.
|
||||
This will do a HEAD/GET request to every external link which means if all is green that the website only links to still active websites.
|
||||
It however also means that it will do a lot of network requests which will take a while.
|
||||
|
||||
Use with care.
|
||||
|
||||
```bash
|
||||
rocket lint --validate-externals
|
||||
```
|
||||
|
||||
## Add a Not Found Page
|
||||
|
||||
When a user enters a URL that does not exist, a "famous" 404 Page Not Found error occurs.
|
||||
|
||||
@@ -60,6 +60,8 @@ npm i -D check-html-links
|
||||
| root-dir | string | the root directory to serve files from. Defaults to the current working directory |
|
||||
| ignore-link-pattern | string | do not check links matching the pattern |
|
||||
| continue-on-error | boolean | if present it will not exit with an error code - useful while writing or for temporary passing a ci |
|
||||
| validate-externals | boolean | if present it will validate external links making a request to the linked url |
|
||||
| absolute-base-url | string | the urls of your website - if provided it will handle absolute links that start with it as internal |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -72,6 +74,12 @@ npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
|
||||
|
||||
# ignore all links like <a href="/users/123"> & <a href="/users/123/details">
|
||||
npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
@@ -1373,6 +1373,11 @@
|
||||
"id": "fixing-broken-links",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"text": "Check external links",
|
||||
"id": "check-external-links",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"text": "Add a Not Found Page",
|
||||
"id": "add-a-not-found-page",
|
||||
|
||||
@@ -6109,14 +6109,14 @@ node-emoji@^1.10.0:
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
node-fetch@2.6.7, node-fetch@^2.6.7:
|
||||
node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^3.2.8:
|
||||
node-fetch@^3.0.0, node-fetch@^3.2.8:
|
||||
version "3.2.10"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
||||
integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
|
||||
|
||||
Reference in New Issue
Block a user