Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
c6c564ede2 Version Packages 2021-01-18 12:14:13 +01:00
Thomas Allmer
a498a5da44 fix(cli): *index.md should not be treated as folder index files 2021-01-18 12:11:46 +01:00
github-actions[bot]
eb6a23dc6a Version Packages 2021-01-17 19:40:29 +01:00
Thomas Allmer
23027bb684 chore: adjust launch release message 2021-01-17 19:20:50 +01:00
Thomas Allmer
cd22231806 chore: adjustments for the restructured CLI Plugin System 2021-01-17 19:09:27 +01:00
Thomas Allmer
b1f61c7759 feat(cli): restructure CLI plugin system - add rocket lint 2021-01-17 19:09:27 +01:00
Thomas Allmer
741f695106 feat: new package check-html-links 2021-01-17 19:09:27 +01:00
68 changed files with 1414 additions and 166 deletions

View File

@@ -22,7 +22,7 @@ module.exports = function (eleventyConfig) {
By providing a `setupUnifiedPlugins` function as an option to `eleventy-plugin-mdjs` you can set options for all unified/remark plugins.
We do use [plugins-manager](../plugins-manager/overview.md).
We do use [plugins-manager](../tools/plugins-manager.md).
This example adds a CSS class to the `htmlHeading` plugin so heading links can be selected in CSS.

View File

@@ -0,0 +1,38 @@
# Tools >> Check Html Links ||30
A fast checker for broken links/references in html.
## Features
- Checks all html files for broken local links/references (in href, src, srcset)
- Focuses on the broken reference targets and groups references to it
- Fast (can process 500-1000 documents in ~2-3 seconds)
- Has only 3 dependencies (and 19 in the full tree)
- Uses [sax-wasm](https://github.com/justinwilaby/sax-wasm) for parsing streamed html
## Installation
```
npm i -D check-html-links
```
## Usage
```
npx check-html-links _site
```
## Example Output
![Test Run Screenshot](./images/check-html-links-screenshot.png)
## Comparision
Checking the output of [11ty-website](https://github.com/11ty/11ty-website) with 13 missing reference targets (used by 516 links) while checking 501 files. (on January 17, 2021)
| Tool | Lines printed | Times | Lang | Dependency Tree |
| ---------------- | ------------- | ------ | ---- | --------------- |
| check-html-links | 38 | ~2.5s | node | 19 |
| link-checker | 3000+ | ~11s | node | 106 |
| hyperlink | 68 | 4m 20s | node | 481 |
| htmltest | 1000+ | ~0.7s | GO | - |

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -1,4 +1,4 @@
# Tools >> Rollup Config
# Tools >> Rollup Config ||20
Rollup configuration to help you get started building modern web applications.
You write modern javascript using the latest browser-features, rollup will optimize your code for production and ensure it runs on all supported browsers.
@@ -52,7 +52,7 @@ You write modern javascript using the latest browser-features, rollup will optim
Our config sets you up with good defaults for most projects. Additionally you can add more plugins and adjust predefined plugins or even remove them if needed.
We use the [plugins-manager](../plugins-manager/overview.md) for it.
We use the [plugins-manager](./plugins-manager.md) for it.
### Customizing the babel config
@@ -83,7 +83,7 @@ SPA and MPA plugins:
- [polyfills-loader](https://modern-web.dev/docs/building/rollup-plugin-polyfills-loader/)
- [workbox](https://www.npmjs.com/package/rollup-plugin-workbox)
You can customize options for these plugins by using [adjustPluginOptions](../plugins-manager/overview.md#adjusting-plugin-options).
You can customize options for these plugins by using [adjustPluginOptions](./plugins-manager.md#adjusting-plugin-options).
```js
import { createSpaConfig } from '@rocket/building-rollup';

View File

@@ -21,6 +21,7 @@
"lint": "run-p lint:*",
"lint:eslint": "eslint --ext .ts,.js,.mjs,.cjs .",
"lint:prettier": "node node_modules/prettier/bin-prettier.js \"**/*.{ts,js,mjs,cjs,md}\" --check --ignore-path .eslintignore",
"lint:rocket": "rocket lint",
"lint:types": "npm run types",
"lint:versions": "node scripts/lint-versions.js",
"postinstall": "npm run setup",

View File

@@ -0,0 +1,6 @@
# check-html-links
## 0.1.0
### Minor Changes
- cd22231: Initial release

View File

@@ -0,0 +1,28 @@
# Check Html Links
A fast checker for broken links/references in html.
## Installation
```
npm i -D check-html-links
```
## Usage
```
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/).
## Comparision
Checking the output of [11ty-website](https://github.com/11ty/11ty-website) with 13 missing reference targets (used by 516 links) while checking 501 files. (on January 17, 2021)
| Tool | Lines printed | Times | Lang | Dependency Tree |
| ---------------- | ------------- | ------ | ---- | --------------- |
| check-html-links | 38 | ~2.5s | node | 19 |
| link-checker | 3000+ | ~11s | node | 106 |
| hyperlink | 68 | 4m 20s | node | 481 |
| htmltest | 1000+ | ~0.7s | GO | - |

View File

@@ -0,0 +1,2 @@
export { validateFolder } from './src/validateFolder.js';
export { formatErrors } from './src/formatErrors.js';

View File

@@ -0,0 +1,43 @@
{
"name": "check-html-links",
"version": "0.1.0",
"publishConfig": {
"access": "public"
},
"description": "A fast low dependency checker of html links/references",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/modernweb-dev/rocket.git",
"directory": "packages/check-html-links"
},
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
"homepage": "https://rocket.modern-web.dev/docs/tools/check-html-links/",
"bin": {
"check-html-links": "src/cli.js"
},
"type": "module",
"exports": {
".": "./index.js"
},
"scripts": {
"test": "mocha --timeout 5000 test-node/**/*.test.{js,cjs} test-node/*.test.{js,cjs}",
"test:watch": "onchange 'src/**/*.{js,cjs}' 'test-node/**/*.{js,cjs}' -- npm test",
"types:copy": "copyfiles \"./types/**/*.d.ts\" dist-types/"
},
"files": [
"*.js",
"dist",
"dist-types",
"src"
],
"dependencies": {
"chalk": "^4.0.0",
"glob": "^7.0.0",
"sax-wasm": "^2.0.0"
},
"devDependencies": {
"@types/glob": "^7.0.0"
},
"types": "dist-types/index.d.ts"
}

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env node
import path from 'path';
import chalk from 'chalk';
import { validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
async function main() {
const userRootDir = process.argv[2];
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);
const errors = await validateFiles(files, rootDir);
const performance = process.hrtime(performanceStart);
if (errors.length > 0) {
let referenceCount = 0;
for (const error of errors) {
referenceCount += error.usage.length;
}
const output = [
`❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links) while checking ${
files.length
} files:`,
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
console.error(output.join('\n'));
process.exit(1);
} else {
console.log(
`✅ All internal links are valid. (executed in %ds %dms)`,
performance[0],
performance[1] / 1000000,
);
}
}
main();

View File

@@ -0,0 +1,46 @@
import path from 'path';
import chalk from 'chalk';
/** @typedef {import('../types/main').Error} Error */
/**
* @param {Error[]} errors
* @param {*} relativeFrom
*/
export function formatErrors(errors, relativeFrom = process.cwd()) {
let output = [];
let number = 0;
for (const error of errors) {
number += 1;
const filePath = path.relative(relativeFrom, error.filePath);
if (error.onlyAnchorMissing === true) {
output.push(
`${number}. missing ${chalk.red.bold(
`id="${error.usage[0].anchor}"`,
)} in ${chalk.cyanBright(filePath)}`,
);
} else {
const firstAttribute = error.usage[0].attribute;
const title =
firstAttribute === 'src' || firstAttribute === 'srcset' ? 'file' : 'reference target';
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`);
}
const usageLength = error.usage.length;
for (let i = 0; i < 3 && i < usageLength; i += 1) {
const usage = error.usage[i];
const usagePath = path.relative(relativeFrom, usage.file);
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}`);
}
if (usageLength > 3) {
const more = chalk.red((usageLength - 3).toString());
output.push(` ... ${more} more references to this target`);
}
output.push('');
}
return output.join('\n');
}

View File

@@ -0,0 +1,35 @@
import fs from 'fs';
import path from 'path';
import glob from 'glob';
/**
* Lists all files using the specified glob, starting from the given root directory.
*
* Will return all matching file paths fully resolved.
*
* @param {string} fromGlob
* @param {string} rootDir
*/
export function listFiles(fromGlob, rootDir) {
return new Promise(resolve => {
glob(
fromGlob,
{ cwd: rootDir },
/**
* @param {Error | null} er
* @param {string[]} files
*/
(er, files) => {
// remember, each filepath returned is relative to rootDir
resolve(
files
// fully resolve the filename relative to rootDir
.map(filePath => path.resolve(rootDir, filePath))
// filter out directories
.filter(filePath => !fs.lstatSync(filePath).isDirectory()),
);
},
);
});
}

View File

@@ -0,0 +1,287 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import fs from 'fs';
import saxWasm from 'sax-wasm';
import { createRequire } from 'module';
import { listFiles } from './listFiles.js';
import path from 'path';
/** @typedef {import('../types/main').Link} Link */
/** @typedef {import('../types/main').LocalFile} LocalFile */
/** @typedef {import('../types/main').Usage} Usage */
/** @typedef {import('../types/main').Error} Error */
/** @typedef {import('sax-wasm').Attribute} Attribute */
const require = createRequire(import.meta.url);
const { SaxEventType, SAXParser } = saxWasm;
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
const saxWasmBuffer = fs.readFileSync(saxPath);
const parserReferences = new SAXParser(SaxEventType.Attribute);
const parserIds = new SAXParser(SaxEventType.Attribute /*, { highWaterMark: 256 * 1024 } */);
/** @type {Error[]} */
let checkLocalFiles = [];
/** @type {Error[]} */
let errors = [];
/** @type {Map<string, string[]>} */
let idCache = new Map();
/**
* @param {string} htmlFilePath
*/
function extractReferences(htmlFilePath) {
/** @type {Link[]} */
const links = [];
/** @type {string[]} */
const ids = [];
parserReferences.eventHandler = (ev, _data) => {
if (ev === SaxEventType.Attribute) {
const data = /** @type {Attribute} */ (/** @type {any} */ (_data));
const attributeName = data.name.toString();
const value = data.value.toString();
const entry = {
attribute: attributeName,
value,
htmlFilePath,
...data.value.start,
};
if (attributeName === 'href' || attributeName === 'src') {
links.push(entry);
}
if (attributeName === 'srcset') {
if (value.includes(',')) {
const srcsets = value.split(',').map(el => el.trim());
for (const srcset of srcsets) {
if (srcset.includes(' ')) {
const srcsetParts = srcset.split(' ');
links.push({ ...entry, value: srcsetParts[0] });
} else {
links.push({ ...entry, value: srcset });
}
}
} else if (value.includes(' ')) {
const srcsetParts = value.split(' ');
links.push({ ...entry, value: srcsetParts[0] });
} else {
links.push(entry);
}
}
if (attributeName === 'id') {
ids.push(value);
}
}
};
return new Promise(resolve => {
const readable = fs.createReadStream(htmlFilePath);
readable.on('data', chunk => {
// @ts-expect-error
parserReferences.write(chunk);
});
readable.on('end', () => {
parserReferences.end();
idCache.set(htmlFilePath, ids);
resolve({ links });
});
});
}
/**
* @param {string} filePath
* @param {string} id
*/
function idExists(filePath, id) {
if (idCache.has(filePath)) {
const cachedIds = idCache.get(filePath);
// return cachedIds.includes(id);
return new Promise(resolve => resolve(cachedIds?.includes(id)));
}
/** @type {string[]} */
const ids = [];
parserIds.eventHandler = (ev, _data) => {
const data = /** @type {Attribute} */ (/** @type {any} */ (_data));
if (ev === SaxEventType.Attribute) {
if (data.name.toString() === 'id') {
ids.push(data.value.toString());
}
}
};
return new Promise(resolve => {
const readable = fs.createReadStream(filePath);
readable.on('data', chunk => {
// @ts-expect-error
parserIds.write(chunk);
});
readable.on('end', () => {
parserIds.end();
idCache.set(filePath, ids);
resolve(ids.includes(id));
});
});
}
/**
* @param {string} filePath
* @param {string} anchor
* @param {Usage} usageObj
*/
function addLocalFile(filePath, anchor, usageObj) {
const foundIndex = checkLocalFiles.findIndex(item => {
return item.filePath === filePath;
});
if (foundIndex === -1) {
checkLocalFiles.push({
filePath,
onlyAnchorMissing: false,
usage: [usageObj],
});
} else {
checkLocalFiles[foundIndex].usage.push(usageObj);
}
}
/**
* @param {string} inValue
*/
function getValueAndAnchor(inValue) {
let value = inValue.replace(/&#/g, '--__check-html-links__--');
let anchor = '';
if (value.includes('#')) {
[value, anchor] = value.split('#');
}
if (value.includes('?')) {
value = value.split('?')[0];
}
if (anchor.includes(':~:')) {
anchor = anchor.split(':~:')[0];
}
if (value.includes(':~:')) {
value = value.split(':~:')[0];
}
value = value.replace(/--__check-html-links__--/g, '&#');
anchor = anchor.replace(/--__check-html-links__--/g, '&#');
value = value.trim();
anchor = anchor.trim();
return {
value,
anchor,
};
}
/**
*
* @param {Link[]} links
* @param {object} options
* @param {string} options.htmlFilePath
* @param {string} options.rootDir
*/
async function resolveLinks(links, { htmlFilePath, rootDir }) {
for (const hrefObj of links) {
const { value, anchor } = getValueAndAnchor(hrefObj.value);
const usageObj = {
attribute: hrefObj.attribute,
value: hrefObj.value,
file: htmlFilePath,
line: hrefObj.line,
character: hrefObj.character,
anchor,
};
let valueFile = value.endsWith('/') ? path.join(value, 'index.html') : value;
if (value.includes('mailto:')) {
// ignore for now - could add a check to validate if the email address is valid
} 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)
} else if (value.startsWith('/')) {
const filePath = path.join(rootDir, valueFile);
addLocalFile(filePath, anchor, usageObj);
} else if (value === '' && anchor === '') {
// no need to check it
} else {
const filePath = path.join(path.dirname(htmlFilePath), valueFile);
addLocalFile(filePath, anchor, usageObj);
}
}
return { checkLocalFiles: [...checkLocalFiles] };
}
/**
*
* @param {Error[]} checkLocalFiles
*/
async function validateLocalFiles(checkLocalFiles) {
for (const localFileObj of checkLocalFiles) {
if (
!fs.existsSync(localFileObj.filePath) ||
fs.lstatSync(localFileObj.filePath).isDirectory()
) {
errors.push(localFileObj);
} else {
for (let i = 0; i < localFileObj.usage.length; i += 1) {
const usage = localFileObj.usage[i];
if (usage.anchor === '') {
localFileObj.usage.splice(i, 1);
i -= 1;
} else {
const isValidAnchor = await idExists(localFileObj.filePath, usage.anchor);
if (isValidAnchor) {
localFileObj.usage.splice(i, 1);
i -= 1;
}
}
}
if (localFileObj.usage.length > 0) {
if (localFileObj.usage.length === 1 && localFileObj.usage[0].anchor) {
localFileObj.onlyAnchorMissing = true;
}
errors.push(localFileObj);
}
}
}
return errors;
}
/**
* @param {string[]} files
* @param {string} rootDir
*/
export async function validateFiles(files, rootDir) {
await parserReferences.prepareWasm(saxWasmBuffer);
await parserIds.prepareWasm(saxWasmBuffer);
errors = [];
checkLocalFiles = [];
idCache = new Map();
for (const htmlFilePath of files) {
const { links } = await extractReferences(htmlFilePath);
await resolveLinks(links, { htmlFilePath, rootDir });
}
await validateLocalFiles(checkLocalFiles);
return errors;
}
/**
* @param {string} inRootDir
*/
export async function validateFolder(inRootDir) {
const rootDir = path.resolve(inRootDir);
const files = await listFiles('**/*.html', rootDir);
const errors = await validateFiles(files, rootDir);
return errors;
}

View File

@@ -0,0 +1,11 @@
<img src="/missing.png" alt="" />
<img src="./missing.png" alt="" />
<img src="/absolute/missing.png" alt="" />
<img src="./relative/missing.png" alt="" />
<!-- valid -->
<img src="/empty.png" alt="" />
<img src="./empty.png" alt="" />
<img src="./empty.png " alt="" />
<img src=" ./empty.png " alt="" />
<img src="./empty.png?data=in&query=params" alt="" />

View File

@@ -0,0 +1,16 @@
<a href="./page.html"></a>
<a href="./page.html#first-headline"></a>
<a href="./page.html#missing-headline"></a>
<a href="./missing-page.html#missing-headline"></a>
<a href="#local"></a>
<a href="#local-missing"></a>
<p id="local"></p>
<a href="#audit-your-angular-app&#39;s-accessibility-with-codelyzer">
Audit your Angular app's accessibility with codelyzer
</a>
<h1 id="audit-your-angular-app&#39;s-accessibility-with-codelyzer">Audit your Angular app's accessibility with codelyzer</h1>
<a href="#local:~:text=put%20your%20labels%20above%20your%20inputs">Sign-in form best practices</a>

View File

@@ -0,0 +1 @@
<h1 id="first-headline">First Headline</h1>

View File

@@ -0,0 +1,4 @@
<a href="./page/index.html"></a>
<a href="./page/#my-anchor"></a>
<a href="./missing-folder/"></a>
<a href="./missing-folder/#my-anchor"></a>

View File

@@ -0,0 +1 @@
<p id="my-anchor"></p>

View File

@@ -0,0 +1,12 @@
<a href="/absolute/index.html"></a>
<a href="./relative/index.html"></a>
<!-- valid -->
<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>

View File

@@ -0,0 +1,2 @@
<a href="/absolute-page/index.html">absolute page</a>
<a href="./relative-page/index.html">relative page</a>

View File

@@ -0,0 +1,3 @@
<a href="/foo"></a>
<a href="./foo"></a>
<a href="./foo#my-anchor"></a>

View File

@@ -0,0 +1,9 @@
<picture>
<source srcset="/images/empty-300.png 300w, /images/empty-600.png?data=in&query=params 600w" type="image/jpeg" sizes="(min-width: 62.5em) 25vw, (min-width: 30.625em) 50vw, 100vw">
<img src="/images/empty.png" alt="Empty" width="300" height="225">
</picture>
<picture>
<source srcset="/images/missing-300.png 300w, /images/missing-600.png 600w" type="image/jpeg" sizes="(min-width: 62.5em) 25vw, (min-width: 30.625em) 50vw, 100vw">
<img src="/images/missing.png" alt="Empty" width="300" height="225">
</picture>

View File

@@ -0,0 +1 @@
<a href="mailto:foo@bar.com"></a>

View File

@@ -0,0 +1,7 @@
<a href="../price/"></a>
<img src="./images/team.png" />
<footer>
<a href="/aboot"></a>
</footer>

View File

@@ -0,0 +1,5 @@
<a href="/price/#my-teams"></a>
<footer>
<a href="/aboot"></a>
</footer>

View File

@@ -0,0 +1,5 @@
<a href="./prce"></a>
<footer>
<a href="/aboot"></a>
</footer>

View File

@@ -0,0 +1,3 @@
<footer>
<a href="/aboot"></a>
</footer>

View File

@@ -0,0 +1,7 @@
<h1 id="overview"></h1>
<h2 id="teams"></h2>
<footer>
<a href="/aboot"></a>
</footer>

View File

@@ -0,0 +1,38 @@
import chai from 'chai';
import chalk from 'chalk';
import { execute } from './test-helpers.js';
import { formatErrors } from 'check-html-links';
const { expect } = chai;
async function executeAndFormat(inPath) {
const { errors, cleanup } = await execute(inPath);
return formatErrors(cleanup(errors));
}
describe('formatErrors', () => {
before(() => {
// ignore colors in tests as most CIs won't support it
chalk.level = 0;
});
it('prints a nice summery', async () => {
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"',
'',
'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',
'',
'4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
]);
});
});

View File

@@ -0,0 +1,28 @@
import path from 'path';
import { fileURLToPath } from 'url';
import { validateFolder } from 'check-html-links';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export async function execute(inPath) {
const testDir = path.join(__dirname, inPath.split('/').join(path.sep));
const errors = await validateFolder(testDir);
return {
cleanup: items => {
const newItems = [];
for (const item of items) {
newItems.push({
...item,
filePath: path.relative(__dirname, item.filePath),
usage: item.usage.map(usageObj => ({
...usageObj,
file: path.relative(__dirname, usageObj.file),
})),
});
}
return newItems;
},
errors,
};
}

View File

@@ -0,0 +1,289 @@
import chai from 'chai';
import { execute } from './test-helpers.js';
const { expect } = chai;
describe('validateFolder', () => {
it('validates internal links', async () => {
const { errors, cleanup } = await execute('fixtures/internal-link');
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-link/absolute/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
value: '/absolute/index.html',
file: 'fixtures/internal-link/index.html',
line: 0,
character: 9,
},
],
},
{
filePath: 'fixtures/internal-link/relative/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
value: './relative/index.html',
file: 'fixtures/internal-link/index.html',
line: 1,
character: 9,
},
],
},
{
filePath: 'fixtures/internal-link/absolute-page/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
value: '/absolute-page/index.html',
file: 'fixtures/internal-link/page.html',
line: 0,
character: 9,
},
],
},
{
filePath: 'fixtures/internal-link/relative-page/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
value: './relative-page/index.html',
file: 'fixtures/internal-link/page.html',
line: 1,
character: 9,
},
],
},
]);
});
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([
{
filePath: 'fixtures/internal-links-to-same-file/foo',
onlyAnchorMissing: false,
usage: [
{
attribute: 'href',
value: '/foo',
anchor: '',
file: 'fixtures/internal-links-to-same-file/index.html',
line: 0,
character: 9,
},
{
attribute: 'href',
value: './foo',
anchor: '',
file: 'fixtures/internal-links-to-same-file/index.html',
line: 1,
character: 9,
},
{
attribute: 'href',
value: './foo#my-anchor',
anchor: 'my-anchor',
file: 'fixtures/internal-links-to-same-file/index.html',
line: 2,
character: 9,
},
],
},
]);
});
it('validates that ids of anchors exist', async () => {
const { errors, cleanup } = await execute('fixtures/internal-link-anchor');
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-link-anchor/page.html',
onlyAnchorMissing: true,
usage: [
{
attribute: 'href',
value: './page.html#missing-headline',
anchor: 'missing-headline',
file: 'fixtures/internal-link-anchor/index.html',
line: 2,
character: 9,
},
],
},
{
filePath: 'fixtures/internal-link-anchor/missing-page.html',
onlyAnchorMissing: false,
usage: [
{
attribute: 'href',
value: './missing-page.html#missing-headline',
anchor: 'missing-headline',
file: 'fixtures/internal-link-anchor/index.html',
line: 3,
character: 9,
},
],
},
{
filePath: 'fixtures/internal-link-anchor/index.html',
onlyAnchorMissing: true,
usage: [
{
attribute: 'href',
value: '#local-missing',
anchor: 'local-missing',
file: 'fixtures/internal-link-anchor/index.html',
line: 5,
character: 9,
},
],
},
]);
});
it('can handle urls that end with a /', async () => {
const { errors, cleanup } = await execute('fixtures/internal-link-folder');
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-link-folder/missing-folder/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
value: './missing-folder/',
file: 'fixtures/internal-link-folder/index.html',
line: 2,
character: 9,
},
{
anchor: 'my-anchor',
attribute: 'href',
value: './missing-folder/#my-anchor',
file: 'fixtures/internal-link-folder/index.html',
line: 3,
character: 9,
},
],
},
]);
});
it('ignores mailto links', async () => {
const { errors, cleanup } = await execute('fixtures/mailto');
expect(cleanup(errors)).to.deep.equal([]);
});
it('can handle img src', async () => {
const { errors, cleanup } = await execute('fixtures/internal-images');
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-images/missing.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'src',
value: '/missing.png',
file: 'fixtures/internal-images/index.html',
line: 0,
character: 10,
},
{
anchor: '',
attribute: 'src',
character: 10,
file: 'fixtures/internal-images/index.html',
line: 1,
value: './missing.png',
},
],
},
{
filePath: 'fixtures/internal-images/absolute/missing.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'src',
character: 10,
file: 'fixtures/internal-images/index.html',
line: 2,
value: '/absolute/missing.png',
},
],
},
{
filePath: 'fixtures/internal-images/relative/missing.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'src',
character: 10,
file: 'fixtures/internal-images/index.html',
line: 3,
value: './relative/missing.png',
},
],
},
]);
});
it('can handle picture source srcset', async () => {
const { errors, cleanup } = await execute('fixtures/internal-pictures');
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-pictures/images/missing-300.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'srcset',
value: '/images/missing-300.png',
file: 'fixtures/internal-pictures/index.html',
line: 6,
character: 18,
},
],
},
{
filePath: 'fixtures/internal-pictures/images/missing-600.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'srcset',
value: '/images/missing-600.png',
file: 'fixtures/internal-pictures/index.html',
line: 6,
character: 18,
},
],
},
{
filePath: 'fixtures/internal-pictures/images/missing.png',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'src',
value: '/images/missing.png',
file: 'fixtures/internal-pictures/index.html',
line: 7,
character: 12,
},
],
},
]);
});
});

View File

@@ -0,0 +1,24 @@
// Don't edit this file directly. It is generated by /scripts/update-package-configs.ts
{
"extends": "../../tsconfig.node-base.json",
"compilerOptions": {
"module": "ESNext",
"outDir": "./dist-types",
"rootDir": ".",
"composite": true,
"allowJs": true,
"checkJs": true,
"emitDeclarationOnly": true
},
"references": [],
"include": [
"src",
"*.js",
"types"
],
"exclude": [
"dist",
"dist-types"
]
}

View File

@@ -0,0 +1,27 @@
export interface Link {
value: string;
attribute: string;
htmlFilePath: string;
line: number;
character: number;
}
export interface Usage {
attribute: string;
value: string;
anchor: string;
file: string;
line: number;
character: number;
}
export interface LocalFile {
filePath: string;
usage: Usage[];
}
export interface Error {
filePath: string;
onlyAnchorMissing: boolean;
usage: Usage[];
}

View File

@@ -1,5 +1,25 @@
# @rocket/cli
## 0.3.1
### Patch Changes
- a498a5d: Make sure links to `*index.md` files are not treated as folder index files like `index.md`
## 0.3.0
### Minor Changes
- cd22231: Restructure and simplify Rocket Cli Plugin System
### Patch Changes
- cd22231: Add a default Rocket Cli Plugin which checks all links on every save (during start) and after a production build
- Updated dependencies [cd22231]
- Updated dependencies [cd22231]
- @rocket/eleventy-plugin-mdjs-unified@0.3.0
- check-html-links@0.1.0
## 0.2.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/cli",
"version": "0.2.1",
"version": "0.3.1",
"publishConfig": {
"access": "public"
},
@@ -54,7 +54,7 @@
"@11ty/eleventy-img": "^0.7.4",
"@rocket/building-rollup": "^0.1.2",
"@rocket/core": "^0.1.1",
"@rocket/eleventy-plugin-mdjs-unified": "^0.2.0",
"@rocket/eleventy-plugin-mdjs-unified": "^0.3.0",
"@rocket/eleventy-rocket-nav": "^0.2.1",
"@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-node-resolve": "^11.0.1",
@@ -62,6 +62,7 @@
"@web/dev-server": "^0.1.4",
"@web/dev-server-rollup": "^0.3.2",
"@web/rollup-plugin-copy": "^0.2.0",
"check-html-links": "^0.1.0",
"command-line-args": "^5.1.1",
"command-line-usage": "^6.1.1",
"fs-extra": "^9.0.1",

View File

@@ -58,21 +58,28 @@ async function productionBuild(config) {
}
export class RocketBuild {
static pluginName = 'RocketBuild';
commands = ['build'];
/**
* @param {RocketCliOptions} config
*/
setupCommand(config) {
config.watch = false;
config.lintInputDir = config.outputDir;
return config;
}
async setup({ config }) {
async setup({ config, eleventy }) {
this.config = {
emptyOutputDir: true,
...config,
};
this.eleventy = eleventy;
}
async build() {
async buildCommand() {
await this.eleventy.write();
if (this.config.emptyOutputDir) {
await fs.emptyDir(this.config.outputDir);
}

View File

@@ -28,23 +28,15 @@ export class RocketEleventy extends Eleventy {
}
async write() {
/** @type {function} */
let finishBuild;
this.__rocketCli.updateComplete = new Promise(resolve => {
finishBuild = resolve;
});
await this.__rocketCli.mergePresets();
await super.write();
await this.__rocketCli.update();
// @ts-ignore
finishBuild();
}
}
export class RocketCli {
updateComplete = new Promise(resolve => resolve(null));
/** @type {string[]} */
errors = [];
constructor({ argv } = { argv: undefined }) {
const mainDefinitions = [
@@ -72,7 +64,10 @@ export class RocketCli {
if (!this.eleventy) {
const { _inputDirCwdRelative, outputDevDir } = this.config;
await fs.emptyDir(outputDevDir);
// We need to merge before we setup 11ty as the write phase is too late for _data
// TODO: find a way so we don't need to double merge
await this.mergePresets();
const elev = new RocketEleventy(_inputDirCwdRelative, outputDevDir, this);
elev.isVerbose = false;
// 11ty always wants a relative path to cwd - why?
@@ -81,10 +76,6 @@ export class RocketCli {
elev.setConfigPathOverride(relCwdPathToConfig);
await elev.init();
if (this.config.watch) {
elev.watch();
}
this.eleventy = elev;
}
}
@@ -116,48 +107,43 @@ export class RocketCli {
async run() {
await this.setup();
if (this.config) {
for (const plugin of this.config.plugins) {
if (this.considerPlugin(plugin)) {
if (typeof plugin.setupCommand === 'function') {
this.config = plugin.setupCommand(this.config);
}
if (typeof plugin.setup === 'function') {
await plugin.setup({ config: this.config, argv: this.subArgv });
}
}
for (const plugin of this.config.plugins) {
if (this.considerPlugin(plugin) && typeof plugin.setupCommand === 'function') {
this.config = plugin.setupCommand(this.config);
}
}
await this.mergePresets();
await fs.emptyDir(this.config.outputDevDir);
await this.setupEleventy();
if (this.config) {
await this.updateComplete;
for (const plugin of this.config.plugins) {
if (this.considerPlugin(plugin) && typeof plugin.execute === 'function') {
await plugin.execute();
}
for (const plugin of this.config.plugins) {
if (typeof plugin.setup === 'function') {
await plugin.setup({ config: this.config, argv: this.subArgv, eleventy: this.eleventy });
}
}
if (this.config.watch === false && this.eleventy) {
await this.eleventy.write();
// execute the actual command
let executedAtLeastOneCommand = false;
const commandFn = `${this.config.command}Command`;
for (const plugin of this.config.plugins) {
if (this.considerPlugin(plugin) && typeof plugin[commandFn] === 'function') {
console.log(`Rocket executes ${commandFn} of ${plugin.constructor.pluginName}`);
executedAtLeastOneCommand = true;
await plugin[commandFn]();
}
}
if (executedAtLeastOneCommand === false) {
throw new Error(`No Rocket Cli Plugin had a ${commandFn} function.`);
}
// Build Phase
if (this.config.command === 'build') {
for (const plugin of this.config.plugins) {
if (typeof plugin.build === 'function') {
await plugin.build();
}
}
for (const plugin of this.config.plugins) {
if (this.considerPlugin(plugin) && typeof plugin.postCommand === 'function') {
await plugin.postCommand();
}
}
if (this.config.command === 'help') {
console.log('Help is here: use build or start');
}
if (this.config.command === 'help') {
console.log('Help is here: use build or start');
}
}

91
packages/cli/src/RocketLint.js Executable file
View File

@@ -0,0 +1,91 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */
import chalk from 'chalk';
import { validateFolder, formatErrors } from 'check-html-links';
export class RocketLint {
static pluginName = 'RocketLint';
commands = ['start', 'build', 'lint'];
/**
* @param {RocketCliOptions} config
*/
setupCommand(config) {
if (config.command === 'lint') {
config.watch = false;
}
return config;
}
/**
* @param {object} options
* @param {RocketCliOptions} options.config
* @param {any} options.argv
*/
async setup({ config, argv, eleventy }) {
this.__argv = argv;
this.config = {
lintInputDir: config.outputDevDir,
lintExecutesEleventyBefore: true,
...config,
};
this.eleventy = eleventy;
}
async lintCommand() {
if (this.config.lintExecutesEleventyBefore) {
await this.eleventy.write();
// updated will trigger linting
} else {
await this.__lint();
}
}
async __lint() {
if (this.config?.pathPrefix) {
console.log('INFO: RocketLint currently does not support being used with a pathPrefix');
return;
}
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const errors = await validateFolder(this.config.lintInputDir);
const performance = process.hrtime(performanceStart);
if (errors.length > 0) {
let referenceCount = 0;
for (const error of errors) {
referenceCount += error.usage.length;
}
const output = [
`❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links):`,
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
if (this.config.watch) {
console.log(output.join('\n'));
} else {
throw new Error(output.join('\n'));
}
} else {
console.log('✅ All internal links are valid.');
}
}
async postCommand() {
if (this.config.watch === false) {
await this.__lint();
}
}
async updated() {
if (this.config.watch === true) {
await this.__lint();
}
}
}

View File

@@ -7,6 +7,7 @@ import { metaConfigToWebDevServerConfig } from 'plugins-manager';
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
export class RocketStart {
static pluginName = 'RocketStart';
commands = ['start'];
/**
@@ -22,7 +23,7 @@ export class RocketStart {
* @param {RocketCliOptions} options.config
* @param {any} options.argv
*/
async setup({ config, argv }) {
async setup({ config, argv, eleventy }) {
this.__argv = argv;
this.config = {
...config,
@@ -30,13 +31,20 @@ export class RocketStart {
...config.devServer,
},
};
this.eleventy = eleventy;
}
async execute() {
async startCommand() {
if (!this.config) {
return;
}
if (this.config.watch) {
await this.eleventy.watch();
} else {
await this.eleventy.write();
}
/** @type {DevServerConfig} */
const devServerConfig = metaConfigToWebDevServerConfig(
{

View File

@@ -39,22 +39,25 @@ const templateEndings = [
'.pug',
];
function endsWithAny(string, suffixes) {
for (let suffix of suffixes) {
if (string.endsWith(suffix)) {
function isTemplateFile(href) {
for (const templateEnding of templateEndings) {
if (href.endsWith(templateEnding)) {
return true;
}
}
return false;
}
function isTemplateFile(href) {
return endsWithAny(href, templateEndings);
}
function isIndexTemplateFile(href) {
const hrefParsed = path.parse(href);
const indexTemplateEndings = templateEndings.map(ending => `index${ending}`);
return endsWithAny(href, indexTemplateEndings);
for (const indexTemplateEnding of indexTemplateEndings) {
if (hrefParsed.base === indexTemplateEnding) {
return true;
}
}
return false;
}
/**
@@ -91,12 +94,46 @@ function extractReferences(html, inputPath) {
return { hrefs, assets };
}
/**
* @param {string} inValue
*/
function getValueAndAnchor(inValue) {
let value = inValue.replace(/&#/g, '--__check-html-links__--');
let anchor = '';
let suffix = '';
if (value.includes('#')) {
[value, anchor] = value.split('#');
suffix = `#${anchor}`;
}
if (value.includes('?')) {
value = value.split('?')[0];
}
if (anchor.includes(':~:')) {
anchor = anchor.split(':~:')[0];
}
if (value.includes(':~:')) {
value = value.split(':~:')[0];
}
value = value.replace(/--__check-html-links__--/g, '&#');
anchor = anchor.replace(/--__check-html-links__--/g, '&#');
suffix = suffix.replace(/--__check-html-links__--/g, '&#');
value = value.trim();
anchor = anchor.trim();
return {
value,
anchor,
suffix,
};
}
function calculateNewHrefs(hrefs, inputPath) {
const newHrefs = [];
for (const hrefObj of hrefs) {
const newHrefObj = { ...hrefObj };
const [href, anchor] = newHrefObj.value.split('#');
const suffix = anchor ? `#${anchor}` : '';
const { value: href, suffix } = getValueAndAnchor(hrefObj.value);
if (isRelativeLink(href) && isTemplateFile(href)) {
const hrefParsed = path.parse(href);

View File

@@ -13,6 +13,7 @@ import { readConfig } from '@web/config-loader';
import { RocketStart } from './RocketStart.js';
import { RocketBuild } from './RocketBuild.js';
import { RocketLint } from './RocketLint.js';
import { fileURLToPath } from 'url';
@@ -114,8 +115,9 @@ export async function normalizeConfig(inConfig) {
/** @type {MetaPlugin[]} */
let pluginsMeta = [
{ name: 'rocket-start', plugin: RocketStart },
{ name: 'rocket-build', plugin: RocketBuild },
{ name: 'RocketStart', plugin: RocketStart },
{ name: 'RocketBuild', plugin: RocketBuild },
{ name: 'RocketLint', plugin: RocketLint },
];
if (Array.isArray(config.setupCliPlugins)) {

View File

@@ -4,6 +4,7 @@ import { RocketCli } from '../src/RocketCli.js';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
import chalk from 'chalk';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -13,7 +14,7 @@ const { expect } = chai;
* @param {function} method
* @param {string} errorMessage
*/
async function expectThrowsAsync(method, errorMessage) {
async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) {
let error = null;
try {
await method();
@@ -21,8 +22,11 @@ async function expectThrowsAsync(method, errorMessage) {
error = err;
}
expect(error).to.be.an('Error', 'No error was thrown');
if (errorMatch) {
expect(error.message).to.match(errorMatch);
}
if (errorMessage) {
expect(error.message).to.match(errorMessage);
expect(error.message).to.equal(errorMessage);
}
}
@@ -83,6 +87,18 @@ describe('RocketCli e2e', () => {
await execute();
}
async function executeLint(pathToConfig) {
cli = new RocketCli({
argv: ['lint', '--config-file', path.join(__dirname, pathToConfig.split('/').join(path.sep))],
});
await execute();
}
before(() => {
// ignore colors in tests as most CIs won't support it
chalk.level = 0;
});
afterEach(async () => {
if (cli?.cleanup) {
await cli.cleanup();
@@ -133,7 +149,9 @@ describe('RocketCli e2e', () => {
],
});
await expectThrowsAsync(() => execute(), /Error in your Eleventy config file.*/);
await expectThrowsAsync(() => execute(), {
errorMatch: /Error in your Eleventy config file.*/,
});
});
});
@@ -219,12 +237,12 @@ describe('RocketCli e2e', () => {
);
});
it('can add a pathprefix that will not influence the start command', async () => {
it('can add a pathPrefix that will not influence the start command', async () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'pathprefix.rocket.config.js'),
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
],
});
await execute();
@@ -233,7 +251,7 @@ describe('RocketCli e2e', () => {
type: 'start',
});
expect(linkHtml).to.equal(
['<p><a href="../../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'),
['<p><a href="../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'),
);
const assetHtml = await readOutput('use-assets/index.html', {
type: 'start',
@@ -256,10 +274,9 @@ describe('RocketCli e2e', () => {
stripToBody: true,
});
expect(linkHtml).to.equal(
[
'<p><a href="../../">home</a></p>',
'<p><a href="/my-sub-folder/">absolute home</a></p>',
].join('\n'),
['<p><a href="../">home</a></p>', '<p><a href="/my-sub-folder/">absolute home</a></p>'].join(
'\n',
),
);
const assetHtml = await readOutput('use-assets/index.html', {
stripServiceWorker: true,
@@ -341,31 +358,24 @@ describe('RocketCli e2e', () => {
const namedMdContent = [
'<p><a href="../">Root</a>',
'<a href="../guides/#with-anchor">Guides</a>',
'<a href="../one-level/raw/">Raw</a>',
'<a href="../../up-one-level/template/">Template</a>',
'<img src="../images-one-level/my-img.svg" alt="my-img">',
'<img src="/absolute-img.svg" alt="absolute-img"></p>',
'<img src="../images/my-img.svg" alt="my-img">',
'<img src="/images/my-img.svg" alt="absolute-img"></p>',
];
const namedHtmlContent = [
'<div>',
'<div id="with-anchor">',
' <a href="../">Root</a>',
' <a href="../guides/#with-anchor">Guides</a>',
' <a href="../one-level/raw/">Raw</a>',
' <a href="../../up-one-level/template/">Template</a>',
' <img src="../images-one-level/my-img.svg" alt="my-img">',
' <img src="/absolute-img.svg" alt="absolute-img">',
' <img src="../images/my-img.svg" alt="my-img">',
' <img src="/images/my-img.svg" alt="absolute-img">',
' <picture>',
' <source media="(min-width:465px)" srcset="../picture-min-465.jpg">',
' <img src="../../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
' <source media="(min-width:465px)" srcset="../images/picture-min-465.jpg">',
' <img src="../images/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
' </picture>',
'</div>',
];
const rawHtml = await readStartOutput('raw/index.html');
expect(rawHtml, 'raw/index.html does not match').to.equal(namedHtmlContent.join('\n'));
const templateHtml = await readStartOutput('template/index.html');
expect(templateHtml, 'template/index.html does not match').to.equal(
namedHtmlContent.join('\n'),
@@ -381,6 +391,22 @@ describe('RocketCli e2e', () => {
'<p>Nothing to adjust in here</p>',
);
const rawHtml = await readStartOutput('one-level/raw/index.html');
expect(rawHtml, 'raw/index.html does not match').to.equal(
[
'<div>',
' <a href="../../">Root</a>',
' <a href="../../guides/#with-anchor">Guides</a>',
' <img src="../../images/my-img.svg" alt="my-img">',
' <img src="/images/my-img.svg" alt="absolute-img">',
' <picture>',
' <source media="(min-width:465px)" srcset="/images/picture-min-465.jpg">',
' <img src="../../images/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
' </picture>',
'</div>',
].join('\n'),
);
// for index files no '../' will be added
const indexHtml = await readStartOutput('index.html');
expect(indexHtml, 'index.html does not match').to.equal(
@@ -388,22 +414,30 @@ describe('RocketCli e2e', () => {
'<p><a href="./">Root</a>',
'<a href="guides/#with-anchor">Guides</a>',
'<a href="./one-level/raw/">Raw</a>',
'<a href="../up-one-level/template/">Template</a>',
'<img src="./images-one-level/my-img.svg" alt="my-img">',
'<img src="/absolute-img.svg" alt="absolute-img"></p>',
'<a href="template/">Template</a>',
'<a href="./rules/tabindex/">EndingIndex</a>',
'<img src="./images/my-img.svg" alt="my-img">',
'<img src="/images/my-img.svg" alt="absolute-img"></p>',
'<div>',
' <a href="./">Root</a>',
' <a href="guides/#with-anchor">Guides</a>',
' <a href="./one-level/raw/">Raw</a>',
' <a href="../up-one-level/template/">Template</a>',
' <img src="./images-one-level/my-img.svg" alt="my-img">',
' <img src="/absolute-img.svg" alt="absolute-img">',
' <a href="template/">Template</a>',
' <a href="./rules/tabindex/">EndingIndex</a>',
' <img src="./images/my-img.svg" alt="my-img">',
' <img src="/images/my-img.svg" alt="absolute-img">',
' <picture>',
' <source media="(min-width:465px)" srcset="./picture-min-465.jpg">',
' <img src="../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
' <source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">',
' <img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
' </picture>',
'</div>',
].join('\n'),
);
});
it('smoke test for link checking', async () => {
await expectThrowsAsync(() => executeLint('e2e-fixtures/lint-links/rocket.config.js'), {
errorMatch: /Found 1 missing reference targets/,
});
});
});

View File

@@ -2,6 +2,6 @@
layout: layout.njk
---
[home](../index.md)
[home](./index.md)
<a href="{{ '/' | url }}">absolute home</a>

View File

@@ -1,19 +1,15 @@
[Root](./index.md)
[Guides](./guides.md#with-anchor)
[Raw](./one-level/raw.html)
[Template](../up-one-level/template.njk)
![my-img](./images-one-level/my-img.svg)
![absolute-img](/absolute-img.svg)
![my-img](./images/my-img.svg)
![absolute-img](/images/my-img.svg)
<div>
<div id="with-anchor">
<a href="./index.md">Root</a>
<a href="./guides.md#with-anchor">Guides</a>
<a href="./one-level/raw.html">Raw</a>
<a href="../up-one-level/template.njk">Template</a>
<img src="./images-one-level/my-img.svg" alt="my-img">
<img src="/absolute-img.svg" alt="absolute-img">
<img src="./images/my-img.svg" alt="my-img">
<img src="/images/my-img.svg" alt="absolute-img">
<picture>
<source media="(min-width:465px)" srcset="./picture-min-465.jpg">
<img src="../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">
<source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">
<img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">
</picture>
</div>

View File

@@ -1,19 +1,21 @@
[Root](./)
[Guides](./guides.md#with-anchor)
[Raw](./one-level/raw.html)
[Template](../up-one-level/template.njk)
![my-img](./images-one-level/my-img.svg)
![absolute-img](/absolute-img.svg)
[Template](./template.njk)
[EndingIndex](./rules/tabindex.md)
![my-img](./images/my-img.svg)
![absolute-img](/images/my-img.svg)
<div>
<a href="./">Root</a>
<a href="./guides.md#with-anchor">Guides</a>
<a href="./one-level/raw.html">Raw</a>
<a href="../up-one-level/template.njk">Template</a>
<img src="./images-one-level/my-img.svg" alt="my-img">
<img src="/absolute-img.svg" alt="absolute-img">
<a href="./template.njk">Template</a>
<a href="./rules/tabindex.md">EndingIndex</a>
<img src="./images/my-img.svg" alt="my-img">
<img src="/images/my-img.svg" alt="absolute-img">
<picture>
<source media="(min-width:465px)" srcset="./picture-min-465.jpg">
<img src="../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">
<source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">
<img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">
</picture>
</div>

View File

@@ -0,0 +1,10 @@
<div>
<a href="../index.md">Root</a>
<a href="../guides.md#with-anchor">Guides</a>
<img src="../images/my-img.svg" alt="my-img">
<img src="/images/my-img.svg" alt="absolute-img">
<picture>
<source media="(min-width:465px)" srcset="/images/picture-min-465.jpg">
<img src="../images/picture-fallback.jpg" alt="Fallback" style="width:auto;">
</picture>
</div>

View File

@@ -1,12 +0,0 @@
<div>
<a href="./index.md">Root</a>
<a href="./guides.md#with-anchor">Guides</a>
<a href="./one-level/raw.html">Raw</a>
<a href="../up-one-level/template.njk">Template</a>
<img src="./images-one-level/my-img.svg" alt="my-img">
<img src="/absolute-img.svg" alt="absolute-img">
<picture>
<source media="(min-width:465px)" srcset="./picture-min-465.jpg">
<img src="../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">
</picture>
</div>

View File

@@ -1,12 +1,10 @@
<div>
<div id="with-anchor">
<a href="./index.md">Root</a>
<a href="./guides.md#with-anchor">Guides</a>
<a href="./one-level/raw.html">Raw</a>
<a href="../up-one-level/template.njk">Template</a>
<img src="./images-one-level/my-img.svg" alt="my-img">
<img src="/absolute-img.svg" alt="absolute-img">
<img src="./images/my-img.svg" alt="my-img">
<img src="/images/my-img.svg" alt="absolute-img">
<picture>
<source media="(min-width:465px)" srcset="./picture-min-465.jpg">
<img src="../images-up-one-level/picture-fallback.jpg" alt="Fallback" style="width:auto;">
<source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">
<img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">
</picture>
</div>

View File

@@ -0,0 +1 @@
<a href="./foo"></a>

View File

@@ -0,0 +1,3 @@
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
const config = {};
export default config;

View File

@@ -39,7 +39,11 @@ describe('normalizeConfig', () => {
setupEleventyPlugins: [],
setupCliPlugins: [],
presets: [],
plugins: [{ commands: ['start'] }, { commands: ['build'] }],
plugins: [
{ commands: ['start'] },
{ commands: ['build'] },
{ commands: ['start', 'build', 'lint'] },
],
inputDir: 'docs',
outputDir: '_site',
});
@@ -68,7 +72,11 @@ describe('normalizeConfig', () => {
setupEleventyPlugins: [],
setupCliPlugins: [],
presets: [],
plugins: [{ commands: ['start'] }, { commands: ['build'] }],
plugins: [
{ commands: ['start'] },
{ commands: ['build'] },
{ commands: ['start', 'build', 'lint'] },
],
inputDir: 'docs',
outputDir: '_site',
});
@@ -94,7 +102,11 @@ describe('normalizeConfig', () => {
setupEleventyPlugins: [],
setupCliPlugins: [],
presets: [],
plugins: [{ commands: ['start'] }, { commands: ['build'] }],
plugins: [
{ commands: ['start'] },
{ commands: ['build'] },
{ commands: ['start', 'build', 'lint'] },
],
inputDir: 'docs',
outputDir: '_site',
});
@@ -123,7 +135,11 @@ describe('normalizeConfig', () => {
setupEleventyPlugins: [],
setupCliPlugins: [],
presets: [],
plugins: [{ commands: ['start'] }, { commands: ['build'] }],
plugins: [
{ commands: ['start'] },
{ commands: ['build'] },
{ commands: ['start', 'build', 'lint'] },
],
inputDir: 'docs',
outputDir: '_site',
});

View File

@@ -1,5 +1,11 @@
# @rocket/eleventy-plugin-mdjs-unified
## 0.3.0
### Minor Changes
- cd22231: Adjustments to work with the restructured CLI Plugin System
## 0.2.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/eleventy-plugin-mdjs-unified",
"version": "0.2.0",
"version": "0.3.0",
"publishConfig": {
"access": "public"
},
@@ -31,7 +31,7 @@
"mdjs"
],
"dependencies": {
"@mdjs/core": "^0.6.1",
"@mdjs/core": "^0.6.2",
"es-module-lexer": "^0.3.26",
"unist-util-visit": "^2.0.3"
},

View File

@@ -1,5 +1,15 @@
# @rocket/launch
## 0.3.0
### Minor Changes
- cd22231: Adjustments to work with the restructured CLI Plugin System
### Patch Changes
- cd22231: Move `noscript.css` into `_assets/_static` as it does not get detected/moved automatically by `@web/rollup-plugin-html`.
## 0.2.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/launch",
"version": "0.2.1",
"version": "0.3.0",
"publishConfig": {
"access": "public"
},

View File

@@ -42,6 +42,6 @@
<link rel="stylesheet" href="{{ '/_assets/layout.css' | asset | url }}">
<link rel="stylesheet" href="{{ '/_assets/markdown.css' | asset | url }}">
<link rel="stylesheet" href="{{ '/_assets/style.css' | asset | url }}">
<noscript><link rel="stylesheet" href="{{ '/_assets/noscript.css' | asset | url }}"/></noscript>
<noscript><link rel="stylesheet" href="{{ '/_assets/_static/noscript.css' | asset | url }}"/></noscript>
{% include 'partials/head.njk' %}

View File

@@ -1,5 +1,11 @@
# @rocket/search
## 0.2.0
### Minor Changes
- cd22231: Adjustments to work with the restructured CLI Plugin System
## 0.1.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/search",
"version": "0.1.2",
"version": "0.2.0",
"publishConfig": {
"access": "public"
},
@@ -42,6 +42,7 @@
"dependencies": {
"@lion/combobox": "^0.1.20",
"@open-wc/scoped-elements": "^1.3.2",
"chalk": "^4.0.0",
"minisearch": "^3.0.2",
"plugins-manager": "^0.2.0",
"sax-wasm": "^2.0.0"

View File

@@ -12,8 +12,8 @@ export class RocketSearchPlugin {
excludeLayouts = ['with-index.njk'];
documents = [];
async setup({ config, argv }) {
const searcDefinitions = [
async setup({ config, argv, eleventy }) {
const searchDefinitions = [
{
name: 'mode',
alias: 'm',
@@ -24,25 +24,13 @@ export class RocketSearchPlugin {
{ name: 'term', type: String, defaultOption: true, defaultValue: '' },
{ name: 'help', type: Boolean, description: 'See all options' },
];
const searchOptions = commandLineArgs(searcDefinitions, { argv });
const searchOptions = commandLineArgs(searchDefinitions, { argv });
this.config = {
...config,
search: searchOptions,
};
}
async execute() {
if (this.config.command !== 'search') {
return;
}
const { mode } = this.config.search;
switch (mode) {
case 'search':
await this.search();
break;
/* no default */
}
this.eleventy = eleventy;
}
async inspectRenderedHtml({ html, url, layout, title, data, eleventy }) {
@@ -67,7 +55,8 @@ export class RocketSearchPlugin {
}
}
async search() {
async searchCommand() {
await this.eleventy.write();
await this.setupIndex();
process.stderr.write('\u001B[?25l'); // hide default cursor

View File

@@ -19,6 +19,9 @@
{
"path": "./packages/core/tsconfig.json"
},
{
"path": "./packages/check-html-links/tsconfig.json"
},
{
"path": "./packages/eleventy-plugin-mdjs-unified/tsconfig.json"
},

View File

@@ -2,6 +2,7 @@ const packages = [
// { name: 'cli', type: 'js', environment: 'node-esm' },
{ name: 'plugins-manager', type: 'js', environment: 'node-esm' },
{ name: 'core', type: 'js', environment: 'node' },
{ name: 'check-html-links', type: 'js', environment: 'node-esm' },
{ name: 'eleventy-plugin-mdjs-unified', type: 'js', environment: 'node' },
{ name: 'eleventy-rocket-nav', type: 'js', environment: 'node' },
{ name: 'drawer', type: 'js', environment: 'browser' },

View File

@@ -1510,6 +1510,14 @@
dependencies:
"@types/node" "*"
"@types/glob@^7.0.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
"@types/hast@^2.0.0":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9"
@@ -1575,7 +1583,7 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
"@types/minimatch@^3.0.3":
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==