mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-21 15:54:57 +00:00
Compare commits
26 Commits
@rocket/cl
...
@rocket/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb6a23dc6a | ||
|
|
23027bb684 | ||
|
|
cd22231806 | ||
|
|
b1f61c7759 | ||
|
|
741f695106 | ||
|
|
156719f977 | ||
|
|
295cfbdbd8 | ||
|
|
7dd6f4c64f | ||
|
|
b68923b608 | ||
|
|
86c3a4b0e8 | ||
|
|
897892d6f9 | ||
|
|
7b2dc6430f | ||
|
|
2a400e09da | ||
|
|
25adb741d8 | ||
|
|
485827127d | ||
|
|
ef3b846bb9 | ||
|
|
6922161429 | ||
|
|
06843f3fa9 | ||
|
|
496a1b0974 | ||
|
|
13c15346b1 | ||
|
|
8eeebbc978 | ||
|
|
32f39ae96a | ||
|
|
579ecfde50 | ||
|
|
9aa3265ebb | ||
|
|
d955b436b6 | ||
|
|
3468ff9fc2 |
@@ -15,7 +15,7 @@ git clone git@github.com:modernweb-dev/rocket.git
|
||||
Once cloning is complete, change directory to the repo.
|
||||
|
||||
```sh
|
||||
cd web
|
||||
cd rocket
|
||||
```
|
||||
|
||||
Now add your fork as a remote
|
||||
@@ -89,7 +89,7 @@ Exceptions:
|
||||
|
||||
## Committing Your Changes
|
||||
|
||||
Commit messages must follow the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0-beta.2/)
|
||||
Commit messages must follow the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
Modern-web uses package name as scope. So for example if you fix a _terrible bug_ in the package `@web/test-runner`, the commit message should look like this:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -18,43 +18,11 @@ module.exports = function (eleventyConfig) {
|
||||
};
|
||||
```
|
||||
|
||||
As mdjs does return html AND javascript at the same time we need to have a template that can understand it. For that we create a layout file.
|
||||
|
||||
👉 `_includes/layout.njk`
|
||||
|
||||
{% raw %}
|
||||
|
||||
```js
|
||||
<main>
|
||||
{{ content.html | safe }}
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
{{ content.jsCode | safe }}
|
||||
</script>
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
And in our content we then need to make sure to use that template.
|
||||
|
||||
👉 `index.md`
|
||||
|
||||
```
|
||||
---
|
||||
layout: layout.njk
|
||||
---
|
||||
|
||||
# Hello World
|
||||
```
|
||||
|
||||
You can see a minimal setup in the [examples repo](https://github.com/daKmoR/rocket-example-projects/tree/master/eleventy-and-mdjs).
|
||||
|
||||
## Configure a unified or remark plugin with mdjs
|
||||
|
||||
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.
|
||||
|
||||
|
||||
38
docs/docs/tools/check-html-links.md
Normal file
38
docs/docs/tools/check-html-links.md
Normal 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
|
||||
|
||||

|
||||
|
||||
## 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 | - |
|
||||
BIN
docs/docs/tools/images/check-html-links-screenshot.png
Normal file
BIN
docs/docs/tools/images/check-html-links-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Go Live >> Overview
|
||||
# Go Live >> Overview ||10
|
||||
|
||||
A few things are usually needed before going live "for real".
|
||||
|
||||
|
||||
44
docs/guides/go-live/social-media.md
Normal file
44
docs/guides/go-live/social-media.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Go Live >> Social Media ||20
|
||||
|
||||
Having a nice preview image for social media can be very helpful.
|
||||
For that reason Rocket creates those automatically with the title, parent title, section and your logo.
|
||||
|
||||
It will look like this but with your logo
|
||||
|
||||
<img src="{{ socialMediaImage }}" width="1200" height="630" alt="Social Media Image of this page" style="border: 1px solid #000" />
|
||||
|
||||
There are multiple ways you can modify it.
|
||||
|
||||
## Setting it via frontMatter
|
||||
|
||||
You can create your own image and link it with something like this
|
||||
|
||||
```
|
||||
---
|
||||
socialMediaImage: path/to/my/image.png
|
||||
---
|
||||
```
|
||||
|
||||
## Providing your own text
|
||||
|
||||
Sometimes extracting the title + title of parent is not enough but you still want to use the "default image".
|
||||
|
||||
You can create an `11tydata.cjs` file next to your page. If your page is `docs/guides/overview.md` then you create a `docs/guides/overview.11tydata.cjs`.
|
||||
|
||||
In there you can use the default `createPageSocialImage` but provide your own values.
|
||||
|
||||
```js
|
||||
const { createPageSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createPageSocialImage({
|
||||
title: 'Learning Rocket',
|
||||
subTitle: 'Have a website',
|
||||
subTitle2: 'in 5 Minutes',
|
||||
footer: 'Rocket Guides',
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
```
|
||||
13
docs/guides/index.11tydata.cjs
Normal file
13
docs/guides/index.11tydata.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
const { createPageSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createPageSocialImage({
|
||||
title: 'Learning Rocket',
|
||||
subTitle: 'Have a website',
|
||||
subTitle2: 'in 5 Minutes',
|
||||
footer: 'Rocket Guides',
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Learning Rocket
|
||||
description: 'foo'
|
||||
eleventyNavigation:
|
||||
key: Guides
|
||||
order: 10
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Presets >> Overriding presets ||20
|
||||
# Presets >> Overriding ||20
|
||||
|
||||
All loaded presets will be combined but you can override each file.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Presets >> Using preset templates ||30
|
||||
# Presets >> Using templates ||30
|
||||
|
||||
Most presetse have specific entry files you can override...
|
||||
13
docs/index.11tydata.cjs
Normal file
13
docs/index.11tydata.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
const { createPageSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createPageSocialImage({
|
||||
title: 'Rocket',
|
||||
subTitle: 'Static sites with',
|
||||
subTitle2: 'a sprinkle of JavaScript.',
|
||||
footer: 'A Modern Web Product',
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
24
package.json
24
package.json
@@ -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",
|
||||
@@ -28,6 +29,7 @@
|
||||
"rocket:build": "node packages/cli/src/cli.js build",
|
||||
"search": "node packages/cli/src/cli.js search",
|
||||
"setup": "npm run setup:ts-configs",
|
||||
"setup:patches": "npx patch-package",
|
||||
"setup:ts-configs": "node scripts/generate-ts-configs.mjs",
|
||||
"start": "node packages/cli/src/cli.js start",
|
||||
"test": "yarn test:node && yarn test:web",
|
||||
@@ -50,22 +52,22 @@
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/mocha": "^8.2.0",
|
||||
"@types/node": "^14.14.16",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/sinon": "^9.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.11.1",
|
||||
"@typescript-eslint/parser": "^4.11.1",
|
||||
"@web/test-runner": "^0.11.5",
|
||||
"@web/test-runner-commands": "^0.3.0",
|
||||
"@web/test-runner-playwright": "^0.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.13.0",
|
||||
"@typescript-eslint/parser": "^4.13.0",
|
||||
"@web/test-runner": "^0.12.2",
|
||||
"@web/test-runner-commands": "^0.4.0",
|
||||
"@web/test-runner-playwright": "^0.8.0",
|
||||
"chai": "^4.2.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"esbuild": "^0.8.26",
|
||||
"eslint": "^7.16.0",
|
||||
"esbuild": "^0.8.31",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"hanbi": "^0.4.1",
|
||||
"husky": "^4.3.6",
|
||||
"husky": "^4.3.7",
|
||||
"lint-staged": "^10.5.3",
|
||||
"mocha": "^8.2.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
@@ -76,9 +78,9 @@
|
||||
"puppeteer": "^5.5.0",
|
||||
"remark-emoji": "^2.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.35.1",
|
||||
"rollup": "^2.36.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sinon": "^9.2.2",
|
||||
"sinon": "^9.2.3",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @rocket/blog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4858271: Adjust templates for change in `@rocket/eleventy-plugin-mdjs-unified` as it now returns html directly instead of an object with html, js, stories
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/blog",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{% endif %}
|
||||
{% include 'partials/addTitleHeadline.njk' %}
|
||||
|
||||
{{ content.html | safe }}
|
||||
{{ content | safe }}
|
||||
|
||||
{% include 'partials/previousNext.njk' %}
|
||||
{% include 'partials/blog-content-footer.njk' %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block main %}
|
||||
<main class="markdown-body">
|
||||
{% include 'partials/addTitleHeadline.njk' %}
|
||||
{{ content.html | safe }}
|
||||
{{ content | safe }}
|
||||
<div class="articles">
|
||||
{% for post in posts %}
|
||||
{% if post.data.published %}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
# @rocket/building-rollup
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 897892d: bump dependencies
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3468ff9: Update rollup-plugin-html to support `absolutePathPrefix` option
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1971f5d: Initial Release
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/building-rollup",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -54,10 +54,10 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@rollup/plugin-babel": "^5.2.2",
|
||||
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||
"@web/rollup-plugin-html": "^1.3.3",
|
||||
"@web/rollup-plugin-html": "^1.4.0",
|
||||
"@web/rollup-plugin-import-meta-assets": "^1.0.4",
|
||||
"@web/rollup-plugin-polyfills-loader": "^1.0.3",
|
||||
"browserslist": "^4.16.0",
|
||||
"browserslist": "^4.16.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-workbox": "^6.1.0"
|
||||
}
|
||||
|
||||
6
packages/check-html-links/CHANGELOG.md
Normal file
6
packages/check-html-links/CHANGELOG.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# check-html-links
|
||||
|
||||
## 0.1.0
|
||||
### Minor Changes
|
||||
|
||||
- cd22231: Initial release
|
||||
28
packages/check-html-links/README.md
Normal file
28
packages/check-html-links/README.md
Normal 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 | - |
|
||||
2
packages/check-html-links/index.js
Normal file
2
packages/check-html-links/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { validateFolder } from './src/validateFolder.js';
|
||||
export { formatErrors } from './src/formatErrors.js';
|
||||
43
packages/check-html-links/package.json
Normal file
43
packages/check-html-links/package.json
Normal 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"
|
||||
}
|
||||
45
packages/check-html-links/src/cli.js
Executable file
45
packages/check-html-links/src/cli.js
Executable 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();
|
||||
46
packages/check-html-links/src/formatErrors.js
Normal file
46
packages/check-html-links/src/formatErrors.js
Normal 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');
|
||||
}
|
||||
35
packages/check-html-links/src/listFiles.js
Normal file
35
packages/check-html-links/src/listFiles.js
Normal 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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
287
packages/check-html-links/src/validateFolder.js
Normal file
287
packages/check-html-links/src/validateFolder.js
Normal 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;
|
||||
}
|
||||
@@ -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="" />
|
||||
@@ -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's-accessibility-with-codelyzer">
|
||||
Audit your Angular app's accessibility with codelyzer
|
||||
</a>
|
||||
|
||||
<h1 id="audit-your-angular-app'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>
|
||||
@@ -0,0 +1 @@
|
||||
<h1 id="first-headline">First Headline</h1>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
<p id="my-anchor"></p>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
<a href="/absolute-page/index.html">absolute page</a>
|
||||
<a href="./relative-page/index.html">relative page</a>
|
||||
@@ -0,0 +1,3 @@
|
||||
<a href="/foo"></a>
|
||||
<a href="./foo"></a>
|
||||
<a href="./foo#my-anchor"></a>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="mailto:foo@bar.com"></a>
|
||||
@@ -0,0 +1,7 @@
|
||||
<a href="../price/"></a>
|
||||
|
||||
<img src="./images/team.png" />
|
||||
|
||||
<footer>
|
||||
<a href="/aboot"></a>
|
||||
</footer>
|
||||
@@ -0,0 +1,5 @@
|
||||
<a href="/price/#my-teams"></a>
|
||||
|
||||
<footer>
|
||||
<a href="/aboot"></a>
|
||||
</footer>
|
||||
@@ -0,0 +1,5 @@
|
||||
<a href="./prce"></a>
|
||||
|
||||
<footer>
|
||||
<a href="/aboot"></a>
|
||||
</footer>
|
||||
@@ -0,0 +1,3 @@
|
||||
<footer>
|
||||
<a href="/aboot"></a>
|
||||
</footer>
|
||||
@@ -0,0 +1,7 @@
|
||||
<h1 id="overview"></h1>
|
||||
|
||||
<h2 id="teams"></h2>
|
||||
|
||||
<footer>
|
||||
<a href="/aboot"></a>
|
||||
</footer>
|
||||
38
packages/check-html-links/test-node/formatErrors.test.js
Normal file
38
packages/check-html-links/test-node/formatErrors.test.js
Normal 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"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
28
packages/check-html-links/test-node/test-helpers.js
Normal file
28
packages/check-html-links/test-node/test-helpers.js
Normal 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,
|
||||
};
|
||||
}
|
||||
289
packages/check-html-links/test-node/validateFolder.test.js
Normal file
289
packages/check-html-links/test-node/validateFolder.test.js
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
24
packages/check-html-links/tsconfig.json
Normal file
24
packages/check-html-links/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
27
packages/check-html-links/types/main.d.ts
vendored
Normal file
27
packages/check-html-links/types/main.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
@@ -1,5 +1,62 @@
|
||||
# @rocket/cli
|
||||
|
||||
## 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
|
||||
|
||||
- 897892d: bump dependencies
|
||||
- 295cfbd: Better support for windows paths
|
||||
- Updated dependencies [897892d]
|
||||
- @rocket/building-rollup@0.1.2
|
||||
- @rocket/eleventy-rocket-nav@0.2.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ef3b846: Add a default "core" preset to the cli package which provides fundaments like eleventConfig data, eleventyComputed data, logo, site name, simple layout, ...
|
||||
- 4858271: Process local relative links and images via html (11ty transform) to support all 11ty template systems
|
||||
- 4858271: Adjust templates for change in `@rocket/eleventy-plugin-mdjs-unified` as it now returns html directly instead of an object with html, js, stories
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ef3b846: Move setting of title, eleventyNavigation and section page meta data to eleventyComputed
|
||||
- ef3b846: Auto create social media images for every page
|
||||
- Updated dependencies [ef3b846]
|
||||
- Updated dependencies [4858271]
|
||||
- Updated dependencies [4858271]
|
||||
- @rocket/core@0.1.1
|
||||
- @rocket/eleventy-plugin-mdjs-unified@0.2.0
|
||||
- @rocket/eleventy-rocket-nav@0.2.0
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 32f39ae: An updated triggered via watch should not hide the main navgiation.
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3468ff9: Pass prefix to rollup-plugin-html so assets can still be extracted
|
||||
- Updated dependencies [3468ff9]
|
||||
- @rocket/building-rollup@0.1.1
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const { setComputedConfig, getComputedConfig } = require('./src/public/computedConfig.cjs');
|
||||
const rocketEleventyComputed = require('./src/public/rocketEleventyComputed.cjs');
|
||||
const { createPageSocialImage } = require('./src/public/createPageSocialImage.cjs');
|
||||
|
||||
module.exports = {
|
||||
setComputedConfig,
|
||||
getComputedConfig,
|
||||
rocketEleventyComputed,
|
||||
createPageSocialImage,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/cli",
|
||||
"version": "0.1.2",
|
||||
"version": "0.3.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -38,6 +38,7 @@
|
||||
"*.mjs",
|
||||
"dist",
|
||||
"dist-types",
|
||||
"preset",
|
||||
"src"
|
||||
],
|
||||
"keywords": [
|
||||
@@ -50,16 +51,18 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^0.11.1",
|
||||
"@rocket/building-rollup": "^0.1.0",
|
||||
"@rocket/core": "^0.1.0",
|
||||
"@rocket/eleventy-plugin-mdjs-unified": "^0.1.0",
|
||||
"@rocket/eleventy-rocket-nav": "^0.1.0",
|
||||
"@11ty/eleventy-img": "^0.7.4",
|
||||
"@rocket/building-rollup": "^0.1.2",
|
||||
"@rocket/core": "^0.1.1",
|
||||
"@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",
|
||||
"@web/config-loader": "^0.1.3",
|
||||
"@web/dev-server": "^0.1.2",
|
||||
"@web/dev-server-rollup": "^0.3.0",
|
||||
"@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",
|
||||
|
||||
33
packages/cli/preset/_assets/logo.svg
Normal file
33
packages/cli/preset/_assets/logo.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<svg fill="#e63946" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 511.998 511.998" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M98.649,430.256c-46.365,28.67-71.17,30.939-78.916,23.51c-7.75-7.433-6.519-32.307,20.182-79.832
|
||||
c24.953-44.412,65.374-96.693,113.818-147.211l-11.279-10.817C93.124,267.348,51.871,320.751,26.291,366.279
|
||||
c-19.228,34.22-37.848,79.134-17.375,98.766c5.84,5.6,13.599,7.935,22.484,7.935c22.269,0,51.606-14.677,75.469-29.432
|
||||
c44.416-27.464,96.044-70.919,145.373-122.362l-11.279-10.817C192.517,360.888,141.976,403.464,98.649,430.256z"/>
|
||||
|
||||
<rect x="238.112" y="272.64" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 237.9094 656.5383)" width="25.589" height="15.628"/>
|
||||
|
||||
<rect x="268.895" y="302.163" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 270.4774 728.6761)" width="25.589" height="15.628"/>
|
||||
|
||||
<rect x="232.827" y="268.929" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 297.4719 673.0591)" width="102.364" height="15.628"/>
|
||||
<path d="M500.916,41.287c-7.769,1.59-76.412,16.062-93.897,34.294l-50.728,52.899l-114.703-3.629l-39.198,40.876l79.28,40.569
|
||||
l-21.755,22.687l72.848,69.858l21.755-22.687l43.857,77.51l39.197-40.876l-8.433-114.451l50.727-52.899
|
||||
c17.485-18.234,29.067-87.422,30.331-95.251l1.801-11.169L500.916,41.287z M228.209,161.383l19.842-20.692l93.688,2.964
|
||||
l-48.775,50.864L228.209,161.383z M401.632,327.686l-35.822-63.308l48.776-50.865l6.886,93.482L401.632,327.686z
|
||||
M332.298,276.743l-50.287-48.223L412.89,92.037l50.288,48.223L332.298,276.743z M473.009,128.036l-48.316-46.334
|
||||
c14.54-8.427,44.787-17.217,68.076-22.632C488.336,82.567,480.82,113.155,473.009,128.036z"/>
|
||||
|
||||
<rect x="302.369" y="231.988" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 384.0262 633.9694)" width="34.12" height="15.628"/>
|
||||
|
||||
<rect x="411.311" y="127.35" transform="matrix(-0.6921 0.7218 -0.7218 -0.6921 807.9747 -74.331)" width="17.061" height="15.628"/>
|
||||
|
||||
<rect x="394.288" y="145.087" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 586.0206 542.7934)" width="15.628" height="17.06"/>
|
||||
|
||||
<rect x="376.571" y="163.565" transform="matrix(-0.7218 -0.6921 0.6921 -0.7218 542.7271 562.3462)" width="15.628" height="17.06"/>
|
||||
|
||||
<rect x="161.111" y="185.158" transform="matrix(0.7071 0.7071 -0.7071 0.7071 192.1943 -60.3323)" width="15.628" height="33.35"/>
|
||||
|
||||
<rect x="184.683" y="172.695" transform="matrix(0.707 0.7072 -0.7072 0.707 182.4625 -83.9076)" width="15.628" height="11.118"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
5
packages/cli/preset/_data/eleventyComputed.cjs
Normal file
5
packages/cli/preset/_data/eleventyComputed.cjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const { rocketEleventyComputed } = require('@rocket/cli');
|
||||
|
||||
module.exports = {
|
||||
...rocketEleventyComputed,
|
||||
};
|
||||
1
packages/cli/preset/_data/layout.cjs
Normal file
1
packages/cli/preset/_data/layout.cjs
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'layout.njk';
|
||||
8
packages/cli/preset/_data/site.cjs
Normal file
8
packages/cli/preset/_data/site.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = function () {
|
||||
return {
|
||||
dir: 'ltr',
|
||||
lang: 'en',
|
||||
name: 'Rocket',
|
||||
description: 'Rocket is the way to build fast static websites with a sprinkle of javascript',
|
||||
};
|
||||
};
|
||||
1
packages/cli/preset/_includes/layout.njk
Normal file
1
packages/cli/preset/_includes/layout.njk
Normal file
@@ -0,0 +1 @@
|
||||
{{ content | safe }}
|
||||
@@ -1,13 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { rollup } from 'rollup';
|
||||
import fs from 'fs-extra';
|
||||
import { copy } from '@web/rollup-plugin-copy';
|
||||
|
||||
import { createMpaConfig } from '@rocket/building-rollup';
|
||||
import { addPlugin } from 'plugins-manager';
|
||||
import { addPlugin, adjustPluginOptions } from 'plugins-manager';
|
||||
|
||||
/**
|
||||
* @param {object} config
|
||||
@@ -24,6 +23,22 @@ async function buildAndWrite(config) {
|
||||
}
|
||||
|
||||
async function productionBuild(config) {
|
||||
const defaultSetupPlugins = [
|
||||
addPlugin({
|
||||
name: 'copy',
|
||||
plugin: copy,
|
||||
options: {
|
||||
patterns: ['!(*.md|*.html)*', '_merged_assets/_static/**/*.{png,gif,jpg,json,css,svg,ico}'],
|
||||
rootDir: config.outputDevDir,
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (config.pathPrefix) {
|
||||
defaultSetupPlugins.push(
|
||||
adjustPluginOptions('html', { absolutePathPrefix: config.pathPrefix }),
|
||||
);
|
||||
}
|
||||
|
||||
const mpaConfig = createMpaConfig({
|
||||
input: '**/*.html',
|
||||
output: {
|
||||
@@ -33,17 +48,7 @@ async function productionBuild(config) {
|
||||
rootDir: config.outputDevDir,
|
||||
absoluteBaseUrl: config.absoluteBaseUrl,
|
||||
setupPlugins: [
|
||||
addPlugin({
|
||||
name: 'copy',
|
||||
plugin: copy,
|
||||
options: {
|
||||
patterns: [
|
||||
'!(*.md|*.html)*',
|
||||
'_merged_assets/_static/**/*.{png,gif,jpg,json,css,svg,ico}',
|
||||
],
|
||||
rootDir: config.outputDevDir,
|
||||
},
|
||||
}),
|
||||
...defaultSetupPlugins,
|
||||
...config.setupDevAndBuildPlugins,
|
||||
...config.setupBuildPlugins,
|
||||
],
|
||||
@@ -53,37 +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, argv }) {
|
||||
const buildDefinitions = [
|
||||
{
|
||||
name: 'mode',
|
||||
alias: 'm',
|
||||
type: String,
|
||||
defaultValue: 'full',
|
||||
description: 'What build to run [full, site, optimize]',
|
||||
},
|
||||
{ name: 'help', type: Boolean, description: 'See all options' },
|
||||
];
|
||||
const buildOptions = commandLineArgs(buildDefinitions, { argv });
|
||||
|
||||
async setup({ config, eleventy }) {
|
||||
this.config = {
|
||||
...config,
|
||||
emptyOutputDir: true,
|
||||
build: {
|
||||
...config.build,
|
||||
...buildOptions,
|
||||
},
|
||||
...config,
|
||||
};
|
||||
this.eleventy = eleventy;
|
||||
}
|
||||
|
||||
async build() {
|
||||
async buildCommand() {
|
||||
await this.eleventy.write();
|
||||
if (this.config.emptyOutputDir) {
|
||||
await fs.emptyDir(this.config.outputDir);
|
||||
}
|
||||
|
||||
@@ -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,35 +64,18 @@ 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?
|
||||
const rel = path.relative(process.cwd(), path.join(__dirname));
|
||||
const relCwdPathToConfig = path.join(rel, 'shared', '.eleventy.cjs');
|
||||
elev.setConfigPathOverride(relCwdPathToConfig);
|
||||
// elev.setDryRun(true); // do not write to file system
|
||||
await elev.init();
|
||||
|
||||
if (this.config.watch) {
|
||||
elev.watch();
|
||||
}
|
||||
|
||||
// // 11ty will bind this hook to itself
|
||||
// const that = this;
|
||||
// elev.config.filters['hook-for-rocket'] = async function hook(html, outputPath) {
|
||||
// // that.requestUpdate();
|
||||
// // const data = await this.getData();
|
||||
// // const { layout, title, inputPath } = data;
|
||||
// // const url = data.page.url;
|
||||
// // for (const plugin of that.plugins) {
|
||||
// // if (typeof plugin.transformHtml === 'function') {
|
||||
// // await plugin.transformHtml({ html, inputPath, outputPath, layout, title, url });
|
||||
// // }
|
||||
// // }
|
||||
// return html;
|
||||
// };
|
||||
|
||||
this.eleventy = elev;
|
||||
}
|
||||
}
|
||||
@@ -132,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.setup === 'function') {
|
||||
await plugin.setup({ config: this.config, argv: this.subArgv });
|
||||
}
|
||||
|
||||
if (typeof plugin.setupCommand === 'function') {
|
||||
this.config = plugin.setupCommand(this.config);
|
||||
}
|
||||
}
|
||||
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
91
packages/cli/src/RocketLint.js
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
183
packages/cli/src/eleventy-plugins/processLocalReferences.cjs
Normal file
183
packages/cli/src/eleventy-plugins/processLocalReferences.cjs
Normal file
@@ -0,0 +1,183 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { SaxEventType, SAXParser } = require('sax-wasm');
|
||||
|
||||
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
|
||||
const saxWasmBuffer = fs.readFileSync(saxPath);
|
||||
|
||||
/** @typedef {import('./types').NavigationNode} NavigationNode */
|
||||
/** @typedef {import('./types').Heading} Heading */
|
||||
/** @typedef {import('./types').SaxData} SaxData */
|
||||
|
||||
// Instantiate
|
||||
const parser = new SAXParser(
|
||||
SaxEventType.Attribute,
|
||||
{ highWaterMark: 256 * 1024 }, // 256k chunks
|
||||
);
|
||||
parser.prepareWasm(saxWasmBuffer);
|
||||
|
||||
/**
|
||||
* @param {string} link
|
||||
*/
|
||||
function isRelativeLink(link) {
|
||||
if (link.startsWith('http') || link.startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const templateEndings = [
|
||||
'.html',
|
||||
'.md',
|
||||
'.11ty.js',
|
||||
'.liquid',
|
||||
'.njk',
|
||||
'.hbs',
|
||||
'.mustache',
|
||||
'.ejs',
|
||||
'.haml',
|
||||
'.pug',
|
||||
];
|
||||
|
||||
function endsWithAny(string, suffixes) {
|
||||
for (let suffix of suffixes) {
|
||||
if (string.endsWith(suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isTemplateFile(href) {
|
||||
return endsWithAny(href, templateEndings);
|
||||
}
|
||||
|
||||
function isIndexTemplateFile(href) {
|
||||
const indexTemplateEndings = templateEndings.map(ending => `index${ending}`);
|
||||
return endsWithAny(href, indexTemplateEndings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
function extractReferences(html, inputPath) {
|
||||
const _html = html.replace(/\n/g, 'XXXRocketProcessLocalReferencesXXX');
|
||||
const hrefs = [];
|
||||
const assets = [];
|
||||
parser.eventHandler = (ev, _data) => {
|
||||
const data = /** @type {SaxData} */ (/** @type {any} */ (_data));
|
||||
if (ev === SaxEventType.Attribute) {
|
||||
const attributeName = data.name.toString();
|
||||
const value = data.value.toString();
|
||||
const entry = {
|
||||
value,
|
||||
startCharacter: data.value.start.character,
|
||||
};
|
||||
if (attributeName === 'href') {
|
||||
if (isRelativeLink(value)) {
|
||||
hrefs.push(entry);
|
||||
}
|
||||
}
|
||||
if (attributeName === 'src' || attributeName === 'srcset') {
|
||||
if (isRelativeLink(value) && !isIndexTemplateFile(inputPath)) {
|
||||
assets.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
parser.write(Buffer.from(_html));
|
||||
parser.end();
|
||||
|
||||
return { hrefs, assets };
|
||||
}
|
||||
|
||||
function calculateNewHrefs(hrefs, inputPath) {
|
||||
const newHrefs = [];
|
||||
for (const hrefObj of hrefs) {
|
||||
const newHrefObj = { ...hrefObj };
|
||||
const [href, anchor] = newHrefObj.value.split('#');
|
||||
const suffix = anchor ? `#${anchor}` : '';
|
||||
|
||||
if (isRelativeLink(href) && isTemplateFile(href)) {
|
||||
const hrefParsed = path.parse(href);
|
||||
const dirPart = hrefParsed.dir.length > 1 ? `${hrefParsed.dir}/` : '';
|
||||
newHrefObj.newValue = isIndexTemplateFile(href)
|
||||
? `${dirPart}${suffix}`
|
||||
: `${dirPart}${hrefParsed.name}/${suffix}`;
|
||||
|
||||
if (isTemplateFile(inputPath)) {
|
||||
if (isIndexTemplateFile(inputPath)) {
|
||||
// nothing
|
||||
} else {
|
||||
newHrefObj.newValue = path.join('../', newHrefObj.newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newHrefObj.newValue) {
|
||||
newHrefs.push(newHrefObj);
|
||||
}
|
||||
}
|
||||
return newHrefs;
|
||||
}
|
||||
|
||||
function calculateNewAssets(assets) {
|
||||
const newAssets = [...assets];
|
||||
return newAssets.map(assetObj => {
|
||||
assetObj.newValue = path.join('../', assetObj.value);
|
||||
return assetObj;
|
||||
});
|
||||
}
|
||||
|
||||
function replaceContent(hrefObj, content) {
|
||||
const upToChange = content.slice(0, hrefObj.startCharacter);
|
||||
const afterChange = content.slice(hrefObj.startCharacter + hrefObj.value.length);
|
||||
|
||||
return `${upToChange}${hrefObj.newValue}${afterChange}`;
|
||||
}
|
||||
|
||||
function sortByStartCharacter(a, b) {
|
||||
if (a.startCharacter > b.startCharacter) {
|
||||
return 1;
|
||||
}
|
||||
if (a.startCharacter < b.startCharacter) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function applyChanges(_changes, _content) {
|
||||
// make sure changes are sorted as changes affect all other changes afterwards
|
||||
let changes = [..._changes].sort(sortByStartCharacter);
|
||||
|
||||
let content = _content.replace(/\n/g, 'XXXRocketProcessLocalReferencesXXX');
|
||||
|
||||
while (changes.length > 0) {
|
||||
const hrefObj = changes.shift();
|
||||
const diff = hrefObj.newValue.length - hrefObj.value.length;
|
||||
|
||||
content = replaceContent(hrefObj, content);
|
||||
|
||||
changes = changes.map(href => {
|
||||
href.startCharacter = href.startCharacter + diff;
|
||||
return href;
|
||||
});
|
||||
}
|
||||
|
||||
return content.replace(/XXXRocketProcessLocalReferencesXXX/g, '\n');
|
||||
}
|
||||
|
||||
async function processLocalReferences(content) {
|
||||
const inputPath = this.inputPath;
|
||||
const { hrefs, assets } = extractReferences(content, inputPath);
|
||||
const newHrefs = calculateNewHrefs(hrefs, inputPath);
|
||||
const newAssets = calculateNewAssets(assets, inputPath);
|
||||
|
||||
const newContent = applyChanges([...newHrefs, ...newAssets], content);
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processLocalReferences,
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { readdirSync } = require('fs');
|
||||
const { processContentWithTitle } = require('@rocket/core/title');
|
||||
|
||||
function getDirectories(source) {
|
||||
return readdirSync(source, { withFileTypes: true })
|
||||
@@ -9,32 +8,6 @@ function getDirectories(source) {
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
let needSetForAll = true;
|
||||
|
||||
/**
|
||||
* adds title from markdown headline to all pages
|
||||
*
|
||||
* @param collection
|
||||
*/
|
||||
function setTitleForAll(collection) {
|
||||
if (needSetForAll) {
|
||||
const all = collection.getAll();
|
||||
all.forEach(page => {
|
||||
page.data.addTitleHeadline = true;
|
||||
const titleData = processContentWithTitle(
|
||||
page.template.inputContent,
|
||||
page.template._templateRender._engineName,
|
||||
);
|
||||
if (titleData) {
|
||||
page.data.title = titleData.title;
|
||||
page.data.eleventyNavigation = { ...titleData.eleventyNavigation };
|
||||
page.data.addTitleHeadline = false;
|
||||
}
|
||||
});
|
||||
needSetForAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
const rocketCollections = {
|
||||
configFunction: (eleventyConfig, { _inputDirCwdRelative }) => {
|
||||
const sectionNames = getDirectories(_inputDirCwdRelative);
|
||||
@@ -50,14 +23,8 @@ const rocketCollections = {
|
||||
let docs = [
|
||||
...collection.getFilteredByGlob(`${_inputDirCwdRelative}/${section}/**/*.md`),
|
||||
];
|
||||
docs.forEach(page => {
|
||||
page.data.section = section;
|
||||
});
|
||||
docs = docs.filter(page => page.inputPath !== `./${indexSection}`);
|
||||
|
||||
// docs = addPrevNextUrls(docs);
|
||||
|
||||
setTitleForAll(collection);
|
||||
return docs;
|
||||
});
|
||||
}
|
||||
@@ -76,6 +43,7 @@ const rocketCollections = {
|
||||
(b.data && b.data.eleventyNavigation && b.data.eleventyNavigation.order) || 0;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
return headers;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { processLocalReferences } = require('./processLocalReferences.cjs');
|
||||
|
||||
function inlineFilePath(filePath) {
|
||||
let data = fs.readFileSync(filePath, function (err, contents) {
|
||||
@@ -22,6 +23,8 @@ const rocketFilters = {
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('inlineFilePath', inlineFilePath);
|
||||
|
||||
eleventyConfig.addTransform('processLocalReferences', processLocalReferences);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ 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';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* @param {Partial<RocketCliOptions>} inConfig
|
||||
@@ -32,7 +37,7 @@ export async function normalizeConfig(inConfig) {
|
||||
watch: true,
|
||||
inputDir: 'docs',
|
||||
outputDir: '_site',
|
||||
outputDevDir: path.resolve('_site-dev'),
|
||||
outputDevDir: '_site-dev',
|
||||
build: {},
|
||||
devServer: {},
|
||||
|
||||
@@ -75,7 +80,8 @@ export async function normalizeConfig(inConfig) {
|
||||
const _configDirCwdRelative = path.relative(process.cwd(), path.resolve(__configDir));
|
||||
const _inputDirCwdRelative = path.join(_configDirCwdRelative, config.inputDir);
|
||||
|
||||
config._presetPathes = [];
|
||||
// cli core preset is always first
|
||||
config._presetPathes = [path.join(__dirname, '..', 'preset')];
|
||||
for (const preset of config.presets) {
|
||||
config._presetPathes.push(preset.path);
|
||||
|
||||
@@ -109,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)) {
|
||||
|
||||
73
packages/cli/src/public/createPageSocialImage.cjs
Normal file
73
packages/cli/src/public/createPageSocialImage.cjs
Normal file
@@ -0,0 +1,73 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Image = require('@11ty/eleventy-img');
|
||||
const { getComputedConfig } = require('./computedConfig.cjs');
|
||||
|
||||
async function createPageSocialImage({ title = '', subTitle = '', subTitle2 = '', footer = '' }) {
|
||||
const rocketConfig = getComputedConfig();
|
||||
const outputDir = path.join(rocketConfig.outputDevDir, '_merged_assets', '11ty-img');
|
||||
|
||||
const logoPath = path.join(rocketConfig._inputDirCwdRelative, '_merged_assets', 'logo.svg');
|
||||
|
||||
const logoBuffer = await fs.promises.readFile(logoPath);
|
||||
const logo = logoBuffer.toString();
|
||||
|
||||
let svgStr = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#4a4a4a" font-family="sans-serif" font-size="80" style="background-color:#fff" viewBox="0 0 1200 630">
|
||||
<defs></defs>
|
||||
<rect width="100%" height="100%" fill="#fff" />
|
||||
<circle cx="1000" cy="230" r="530" fill="#ebebeb"></circle>
|
||||
`;
|
||||
|
||||
if (logo) {
|
||||
svgStr += `<g transform="matrix(0.7, 0, 0, 0.7, 500, 100)">${logo}</g>`;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
svgStr += `
|
||||
<text x="70" y="200" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700">
|
||||
${title}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
|
||||
if (subTitle) {
|
||||
svgStr += `
|
||||
<text x="70" y="320" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700" font-size="60">
|
||||
${subTitle}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
|
||||
if (subTitle2) {
|
||||
svgStr += `
|
||||
<text x="70" y="420" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700" font-size="60">
|
||||
${subTitle2}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
|
||||
if (footer) {
|
||||
svgStr += `
|
||||
<text x="70" y="560" fill="gray" font-size="40">
|
||||
${footer}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
|
||||
svgStr += '</svg>';
|
||||
|
||||
let stats = await Image(Buffer.from(svgStr), {
|
||||
widths: [1200], // Facebook Opengraph image is 1200 x 630
|
||||
formats: ['png'],
|
||||
outputDir,
|
||||
urlPath: '/_merged_assets/11ty-img/',
|
||||
sourceUrl: `${title}${subTitle}${footer}${logo}`, // This is only used to generate the output filename hash
|
||||
});
|
||||
|
||||
return stats['png'][0].url;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPageSocialImage,
|
||||
};
|
||||
63
packages/cli/src/public/rocketEleventyComputed.cjs
Normal file
63
packages/cli/src/public/rocketEleventyComputed.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const fs = require('fs');
|
||||
const { processContentWithTitle } = require('@rocket/core/title');
|
||||
const { createPageSocialImage } = require('./createPageSocialImage.cjs');
|
||||
|
||||
module.exports = {
|
||||
titleMeta: async data => {
|
||||
if (data.titleMeta) {
|
||||
return data.titleMeta;
|
||||
}
|
||||
let text = await fs.promises.readFile(data.page.inputPath);
|
||||
text = text.toString();
|
||||
const titleMetaFromContent = processContentWithTitle(text, 'md');
|
||||
if (titleMetaFromContent) {
|
||||
return titleMetaFromContent;
|
||||
}
|
||||
return {};
|
||||
},
|
||||
title: async data => {
|
||||
if (data.title) {
|
||||
return data.title;
|
||||
}
|
||||
return data.titleMeta?.title;
|
||||
},
|
||||
eleventyNavigation: async data => {
|
||||
if (data.eleventyNavigation) {
|
||||
return data.eleventyNavigation;
|
||||
}
|
||||
return data.titleMeta?.eleventyNavigation;
|
||||
},
|
||||
section: async data => {
|
||||
if (data.section) {
|
||||
return data.section;
|
||||
}
|
||||
|
||||
if (data.page.filePathStem) {
|
||||
// filePathStem: '/sub/subsub/index'
|
||||
// filePathStem: '/index',
|
||||
const parts = data.page.filePathStem.split('/');
|
||||
if (parts.length > 2) {
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
socialMediaImage: async data => {
|
||||
if (data.socialMediaImage) {
|
||||
return data.socialMediaImage;
|
||||
}
|
||||
if (!data.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const section = data.section ? ' ' + data.section[0].toUpperCase() + data.section.slice(1) : '';
|
||||
const footer = `${data.site.name}${section}`;
|
||||
|
||||
const imgUrl = await createPageSocialImage({
|
||||
title: data.titleMeta.parts ? data.titleMeta.parts[0] : '',
|
||||
subTitle:
|
||||
data.titleMeta.parts && data.titleMeta.parts[1] ? `in ${data.titleMeta.parts[1]}` : '',
|
||||
footer,
|
||||
});
|
||||
return imgUrl;
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +61,11 @@ describe('RocketCli e2e', () => {
|
||||
return text;
|
||||
}
|
||||
|
||||
function readStartOutput(fileName, options = {}) {
|
||||
options.type = 'start';
|
||||
return readOutput(fileName, options);
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
await cli.setup();
|
||||
cli.config.outputDevDir = path.join(__dirname, 'e2e-fixtures', '__output-dev');
|
||||
@@ -67,6 +76,29 @@ describe('RocketCli e2e', () => {
|
||||
await cli.run();
|
||||
}
|
||||
|
||||
async function executeStart(pathToConfig) {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'start',
|
||||
'--config-file',
|
||||
path.join(__dirname, pathToConfig.split('/').join(path.sep)),
|
||||
],
|
||||
});
|
||||
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();
|
||||
@@ -117,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.*/,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,26 +233,30 @@ describe('RocketCli e2e', () => {
|
||||
type: 'start',
|
||||
});
|
||||
expect(indexHtml).to.equal(
|
||||
'<p>You can show rocket config data like rocketConfig.absoluteBaseUrl = http://test-domain.com/</p>',
|
||||
'<p>You can show rocket config data like rocketConfig.absoluteBaseUrl = <a href="http://test-domain.com/">http://test-domain.com/</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
const indexHtml = await readOutput('link/index.html', {
|
||||
const linkHtml = await readOutput('link/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
expect(indexHtml).to.equal(
|
||||
['<p><a href="../../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'),
|
||||
expect(linkHtml).to.equal(
|
||||
['<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',
|
||||
});
|
||||
expect(assetHtml).to.equal('<link rel="stylesheet" href="/_merged_assets/some.css">');
|
||||
});
|
||||
|
||||
it('can add a pathPrefix that will be used in the build command', async () => {
|
||||
@@ -231,15 +269,173 @@ describe('RocketCli e2e', () => {
|
||||
});
|
||||
await execute();
|
||||
|
||||
const indexHtml = await readOutput('link/index.html', {
|
||||
const linkHtml = await readOutput('link/index.html', {
|
||||
stripServiceWorker: true,
|
||||
stripToBody: true,
|
||||
});
|
||||
expect(indexHtml).to.equal(
|
||||
expect(linkHtml).to.equal(
|
||||
['<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,
|
||||
});
|
||||
expect(assetHtml).to.equal(
|
||||
'<html><head><link rel="stylesheet" href="../41297ffa.css">\n\n</head><body>\n\n</body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('will extract a title from markdown and set first folder as section', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'start',
|
||||
'--config-file',
|
||||
path.join(__dirname, 'e2e-fixtures', 'headlines', 'rocket.config.js'),
|
||||
],
|
||||
});
|
||||
await execute();
|
||||
|
||||
const indexHtml = await readOutput('index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [indexTitle, indexSection] = indexHtml.split('\n');
|
||||
expect(indexTitle).to.equal('Root');
|
||||
expect(indexSection).to.be.undefined;
|
||||
|
||||
const subHtml = await readOutput('sub/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [subTitle, subSection] = subHtml.split('\n');
|
||||
expect(subTitle).to.equal('Root: Sub');
|
||||
expect(subSection).to.equal('sub');
|
||||
|
||||
const subSubHtml = await readOutput('sub/subsub/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [subSubTitle, subSubSection] = subSubHtml.split('\n');
|
||||
expect(subSubTitle).to.equal('Sub: SubSub');
|
||||
expect(subSubSection).to.equal('sub');
|
||||
|
||||
const sub2Html = await readOutput('sub2/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [sub2Title, sub2Section] = sub2Html.split('\n');
|
||||
expect(sub2Title).to.equal('Root: Sub2');
|
||||
expect(sub2Section).to.equal('sub2');
|
||||
|
||||
const withDataHtml = await readOutput('with-data/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [withDataTitle, withDataSection] = withDataHtml.split('\n');
|
||||
expect(withDataTitle).to.equal('Set via data');
|
||||
expect(withDataSection).be.undefined;
|
||||
});
|
||||
|
||||
it('will create a social media image for every page', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'start',
|
||||
'--config-file',
|
||||
path.join(__dirname, 'e2e-fixtures', 'social-images', 'rocket.config.js'),
|
||||
],
|
||||
});
|
||||
await execute();
|
||||
|
||||
const indexHtml = await readOutput('index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
expect(indexHtml).to.equal('/_merged_assets/11ty-img/c0a892f2-1200.png');
|
||||
|
||||
const guidesHtml = await readOutput('guides/first-pages/getting-started/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
expect(guidesHtml).to.equal('/_merged_assets/11ty-img/58b7e437-1200.png');
|
||||
});
|
||||
|
||||
it.only('will add "../" for links and image urls only within named template files', async () => {
|
||||
await executeStart('e2e-fixtures/image-link/rocket.config.js');
|
||||
|
||||
const namedMdContent = [
|
||||
'<p><a href="../">Root</a>',
|
||||
'<a href="../one-level/raw/">Raw</a>',
|
||||
'<img src="../images/my-img.svg" alt="my-img">',
|
||||
'<img src="/images/my-img.svg" alt="absolute-img"></p>',
|
||||
];
|
||||
|
||||
const namedHtmlContent = [
|
||||
'<div id="with-anchor">',
|
||||
' <a href="../">Root</a>',
|
||||
' <a href="../one-level/raw/">Raw</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>',
|
||||
];
|
||||
|
||||
const templateHtml = await readStartOutput('template/index.html');
|
||||
expect(templateHtml, 'template/index.html does not match').to.equal(
|
||||
namedHtmlContent.join('\n'),
|
||||
);
|
||||
|
||||
const guidesHtml = await readStartOutput('guides/index.html');
|
||||
expect(guidesHtml, 'guides/index.html does not match').to.equal(
|
||||
[...namedMdContent, ...namedHtmlContent].join('\n'),
|
||||
);
|
||||
|
||||
const noAdjustHtml = await readStartOutput('no-adjust/index.html');
|
||||
expect(noAdjustHtml, 'no-adjust/index.html does not match').to.equal(
|
||||
'<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(
|
||||
[
|
||||
'<p><a href="../../">home</a></p>',
|
||||
'<p><a href="/my-sub-folder/">absolute home</a></p>',
|
||||
'<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(
|
||||
[
|
||||
'<p><a href="./">Root</a>',
|
||||
'<a href="guides/#with-anchor">Guides</a>',
|
||||
'<a href="./one-level/raw/">Raw</a>',
|
||||
'<a href="template/">Template</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="template/">Template</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'),
|
||||
);
|
||||
});
|
||||
|
||||
it('smoke test for link checking', async () => {
|
||||
await expectThrowsAsync(() => executeLint('e2e-fixtures/lint-links/rocket.config.js'), {
|
||||
errorMatch: /Found 1 missing reference targets/,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ content.html | safe }}
|
||||
|
||||
{% if content.jsCode %}
|
||||
<script type="module">
|
||||
{{ content.jsCode | safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,5 +1 @@
|
||||
---
|
||||
layout: layout.njk
|
||||
---
|
||||
|
||||
Markdown in `docs/page/index.md`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
body { background: green; }
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ content.html | safe }}
|
||||
|
||||
{% if content.jsCode %}
|
||||
<script type="module">
|
||||
{{ content.jsCode | safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -2,6 +2,6 @@
|
||||
layout: layout.njk
|
||||
---
|
||||
|
||||
[home](../index.md)
|
||||
[home](./index.md)
|
||||
|
||||
<a href="{{ '/' | url }}">absolute home</a>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: layout.njk
|
||||
---
|
||||
|
||||
<link rel="stylesheet" href="{{ '/_assets/some.css' | asset | url }}">
|
||||
@@ -0,0 +1 @@
|
||||
**/*.njk
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = 'do-not-generate-it';
|
||||
@@ -0,0 +1,2 @@
|
||||
{{ title }}
|
||||
{{ section }}
|
||||
@@ -0,0 +1 @@
|
||||
# Root
|
||||
@@ -0,0 +1 @@
|
||||
# Root >> Sub
|
||||
@@ -0,0 +1 @@
|
||||
# Root >> Sub >> SubSub ||10
|
||||
@@ -0,0 +1 @@
|
||||
# Root >> Sub2
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Set via data
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
|
||||
const config = {};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,15 @@
|
||||
[Root](./index.md)
|
||||
[Raw](./one-level/raw.html)
|
||||

|
||||

|
||||
|
||||
<div id="with-anchor">
|
||||
<a href="./index.md">Root</a>
|
||||
<a href="./one-level/raw.html">Raw</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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg height="100" width="100">
|
||||
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 118 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
19
packages/cli/test-node/e2e-fixtures/image-link/docs/index.md
Normal file
19
packages/cli/test-node/e2e-fixtures/image-link/docs/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
[Root](./)
|
||||
[Guides](./guides.md#with-anchor)
|
||||
[Raw](./one-level/raw.html)
|
||||
[Template](./template.njk)
|
||||

|
||||

|
||||
|
||||
<div>
|
||||
<a href="./">Root</a>
|
||||
<a href="./guides.md#with-anchor">Guides</a>
|
||||
<a href="./one-level/raw.html">Raw</a>
|
||||
<a href="./template.njk">Template</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>
|
||||
@@ -0,0 +1 @@
|
||||
Nothing to adjust in here
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div id="with-anchor">
|
||||
<a href="./index.md">Root</a>
|
||||
<a href="./one-level/raw.html">Raw</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>
|
||||
@@ -0,0 +1,3 @@
|
||||
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
|
||||
const config = {};
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<a href="./foo"></a>
|
||||
@@ -0,0 +1,3 @@
|
||||
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
|
||||
const config = {};
|
||||
export default config;
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ content.html | safe }}
|
||||
|
||||
{% if content.jsCode %}
|
||||
<script type="module">
|
||||
{{ content.jsCode | safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,5 +1 @@
|
||||
---
|
||||
layout: layout.njk
|
||||
---
|
||||
|
||||
You can show rocket config data like rocketConfig.absoluteBaseUrl = {{rocketConfig.absoluteBaseUrl}}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ content.html | safe }}
|
||||
|
||||
{% if content.jsCode %}
|
||||
<script type="module">
|
||||
{{ content.jsCode | safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user