Compare commits
46 Commits
@rocket/dr
...
check-html
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a4b80f1e | ||
|
|
f343c5030a | ||
|
|
a7b0dbbce0 | ||
|
|
eeb51c830c | ||
|
|
b968badf43 | ||
|
|
c92769a145 | ||
|
|
562e91fc43 | ||
|
|
ffd06fcee9 | ||
|
|
0eb507d7ef | ||
|
|
45cd7206f1 | ||
|
|
eb74110dd8 | ||
|
|
517c7780ab | ||
|
|
e4852db673 | ||
|
|
c6c564ede2 | ||
|
|
a498a5da44 | ||
|
|
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 | ||
|
|
fd4bc27f16 | ||
|
|
641c7e551c | ||
|
|
f9ae2b8208 | ||
|
|
a8c7173758 | ||
|
|
dd5c772ba3 |
@@ -1,2 +1,4 @@
|
||||
node_modules/**
|
||||
/docs/_assets/head.html
|
||||
/docs/_assets
|
||||
/docs/_includes
|
||||
/docs/_data
|
||||
|
||||
@@ -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
|
||||
|
||||
12
docs/blog/introducing-check-html-links.11tydata.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
const { createSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createSocialImage({
|
||||
title: 'Introducing',
|
||||
subTitle: 'check-html-links',
|
||||
footer: 'Rocket Blog',
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
206
docs/blog/introducing-check-html-links.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Introducing check html links - no more bad links
|
||||
published: true
|
||||
description: A fast link checker for static html
|
||||
tags: [html, javascript, webdev, node]
|
||||
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/an9z6f4hdll2jlne43u3.jpg
|
||||
---
|
||||
|
||||
**TL;DR : I created a standalone tool that can help you fix all the broken links in your websites/documentation. You can check it out [on npm as check-html-links](https://www.npmjs.com/package/check-html-links)**
|
||||
|
||||
In my developer career, I have put live multiple websites and honestly often within a few days, there was always this one issue raised. "This link on xxx is broken". 🤦♂️
|
||||
|
||||
Often these things happen as somewhere a page got moved or renamed and not every location got updated.
|
||||
It's really hard to catch especially if you have a dynamic page like with WordPress or an SPA. And for users, there is nothing worse than landing on your documentation only to find a 404 staring back at them.
|
||||
|
||||
Luckily, with the rise of SSG (Static Site Generators), this problem becomes easier to tackle and can be solved in large part. The reason for that is that with all HTML rendered upfront as static files we can read all of them and check every link.
|
||||
|
||||
## Evaluation and the decision for a new tool
|
||||
|
||||
Of course, I am not the first one to come up with that idea and there are multiple tools available on the market already.
|
||||
However, when checking existing solutions I found out that most of them didn't satisfy me in at least on way 😅. Things I noticed: slow to execute, deprecated, large dependency tree, confusing output for the user, ...
|
||||
|
||||
Reviewing these tools I decided to create my own, with the following requirements :
|
||||
|
||||
- Blazing fast
|
||||
- User-focused output
|
||||
- Few dependencies, to keep it lean
|
||||
- Preferably in the NodeJS ecosystem
|
||||
|
||||
## Focusing on Useful Output
|
||||
|
||||
Most tools evaluated check files individually and report on their findings individually. That means if you have a broken link in your header or footer, you will get one line (or even multiple lines) of an error message(s) for EVERY page.
|
||||
|
||||
I tested this on the [11ty-website](https://github.com/11ty/11ty-website) and there are currently 516 broken links in 501 files. However, **the source of those 516 broken links is just 13 missing pages/resources**.
|
||||
|
||||
In my implementation, I decided to switch from an "Error in File Focused" method to a "Missing File Focused". Let's see this with examples
|
||||
|
||||
### Error in File Focused
|
||||
|
||||
This is what a lot of current existing solutions implement. Here is part of the output that is being produced:
|
||||
|
||||
```
|
||||
[...]
|
||||
authors/ryzokuken/index.html
|
||||
target does not exist --- authors/ryzokuken/index.html --> /speedlify/
|
||||
authors/alex_kaul/index.html
|
||||
target does not exist --- authors/alex_kaul/index.html --> /speedlify/
|
||||
docs/config/index.html
|
||||
target does not exist --- docs/config/index.html --> /speedlify/
|
||||
hash does not exist --- docs/config/index.html --> /docs/copy/#disabling-passthrough-file-copy
|
||||
authors/cramforce/index.html
|
||||
target does not exist --- authors/cramforce/index.html --> /speedlify/
|
||||
authors/accudio/index.html
|
||||
target does not exist --- authors/accudio/index.html --> /speedlify/
|
||||
[...]
|
||||
```
|
||||
|
||||
We get ~2000 lines of errors for `/speedlify/` as it's not found ~500 times. In the middle of those errors, we also see some other broken links.
|
||||
Because the reporting is focusing first on the files, and then on the actual error **it is difficult to know where most errors originate from**.
|
||||
|
||||
### Missing File Focused
|
||||
|
||||
Let us turn that around and focus on missing references indeed. Here is the output for the same input website :
|
||||
|
||||
```
|
||||
[...]
|
||||
1. missing reference target _site/speedlify/index.html
|
||||
from _site/404.html:1942:13 via href="/speedlify/"
|
||||
from _site/authors/_amorgunov/index.html:2031:13 via href="/speedlify/"
|
||||
from _site/authors/_coolcut/index.html:2031:13 via href="/speedlify/"
|
||||
... 495 more references to this target
|
||||
|
||||
2. missing id="disabling-passthrough-file-copy" in _site/docs/copy/index.html
|
||||
from _site/docs/config/index.html:2527:267 via href="/docs/copy/#disabling-passthrough-file-copy"
|
||||
|
||||
3. missing reference target _site/authors/dkruythoff/github.com/dkruythoff/darius-codes
|
||||
from _site/authors/dkruythoff/index.html:2102:234 via href="github.com/dkruythoff/darius-codes"
|
||||
[...]
|
||||
```
|
||||
|
||||
We get one 5 line error for `/speedlify/` and it tells us it's missing 495 times + 3 examples usages.
|
||||
Afterward, we find very clearly more missing references and where they occurred.
|
||||
|
||||
### A clear winner
|
||||
|
||||
Comparing those two outputs makes it pretty clear to me that `Missing File Focused` will make more sense if there is a chance that some links will be broken everywhere. My implementation focuses on missing links in its output. This is crucial because it allows developers to know where to focus their efforts first to get the biggest wins.
|
||||
|
||||
## Focusing on Speed
|
||||
|
||||
Speed is always nice to have but in this case, it's probably vital. I need this to be fast so that I can run it potentially on every save. Speed is also very important in case the tool runs in a CI for example. For projects with extensive documentation, we don't want to hog the CI only to check for documentation.
|
||||
|
||||
Luckily HTML is an awesome language to analyze as it's declarative, which means you can read and analyze it at the same time. This may even mean that the HTML is already processed by the time the file is done reading.
|
||||
|
||||
With this knowledge I was hopeful - but reality didn't deliver 😅. The only tool that could keep up with the speed I needed was implemented in [Go](https://golang.org/).
|
||||
|
||||
It seems that most tools use sophisticated parsers meant to create full syntax trees of your HTML.
|
||||
In reality for link checking all you need to know are the _id_ and the _href_ attributes.
|
||||
|
||||
I have been using [sax-wasm](https://github.com/justinwilaby/sax-wasm) in a few situations before and I knew it supported streaming. I knew that way it could be FAST 🤞!
|
||||
|
||||
How fast are we talking about though?
|
||||
|
||||
As a rule of thumb, I decided that the analysis should be finished within 1s for a small site (up to 200 pages).
|
||||
The main reason is already listed above: To not disturb during writing/development as it will run on every save.
|
||||
For medium sites (200 - 1000 pages), it's reasonable if it takes a little longer - let's aim for less than 5 seconds. This will probably be a breaking point where you execute it only on-demand and in the CI instead of executing it on every save.
|
||||
|
||||
Results are gatherd on January 26, 2021:
|
||||
|
||||
| Website | Pages | Duration |
|
||||
| ----------- | ----- | -------- |
|
||||
| open-wc.org | 90 | ~0.4s |
|
||||
| 11ty.dev | 501 | ~2.5s |
|
||||
| web.dev | 830 | ~3.7s |
|
||||
| eslint.org | 3475 | ~12.4s |
|
||||
|
||||
## Being part of the NodeJS ecosystem
|
||||
|
||||
My daily workflow is hugely dominated by JavaScript, so it was only natural to want to stay in the same environment if I could reach my earlier requirements with it.
|
||||
On top of this, the end goal is to integrate it within a bigger WIP system called [Rocket](https://github.com/modernweb-dev/rocket) which is node-based so therefore it will need to at least support NodeJS. Having it standalone (usable via `npx`) also makes it more versatile and easier to maintain/test.
|
||||
|
||||
## Focusing on a small Dependency Tree
|
||||
|
||||
The JavaScript and NodeJs ecosystem is very active and constantly shifting. Lots of changes/improvements happen all the time. It's often hard to keep up. Therefore having a small dependency tree is something to always thrive for because it will reduce the maintenance burden down the line. And as an added benefit, it makes it smaller and easily embeddable as less stuff has to go down the wire. Lean is king 👑.
|
||||
|
||||
## Solution
|
||||
|
||||
As already mentioned I went on and implement a link checker myself 😅. So far it fits all my requirements so I call it a success 🎉! You can find it [on npm](https://www.npmjs.com/package/check-html-links).
|
||||
|
||||
I called it `check-html-links` and its slogan is "no more broken links or assets".
|
||||
|
||||
The features so far are:
|
||||
|
||||
- extracts every attribute value of id, href, src, srset
|
||||
- use a wasm parser (sax-wasm)
|
||||
- streams the html for performance
|
||||
- check if file or id within file exist
|
||||
- focus on missing references/sources
|
||||
|
||||
## Usage
|
||||
|
||||
It does check your final html output so you need to execute it after your Static Site Generator.
|
||||
|
||||
```
|
||||
npx check-html-links _site
|
||||
```
|
||||
|
||||
## Usage Github Action
|
||||
|
||||
[Julien](https://twitter.com/jlengrand) created a Github action available for the tool, so you can easily plug it in your existing CI. You can find it [on the GitHub Marketplace](https://github.com/marketplace/actions/check-html-links-action).
|
||||
|
||||
Here is a complete example workflow that will check the result of the folder `_site` in the root of your repository on each push:
|
||||
|
||||
```yml
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
check_html_links_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: A job to test check-html-links-action
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: check-html-links-action step
|
||||
id: check-links
|
||||
uses: modernweb-dev/check-html-links-action@v1
|
||||
with:
|
||||
doc-folder: '_site_'
|
||||
```
|
||||
|
||||
## Comparison
|
||||
|
||||
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 | Duration | 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 | - |
|
||||
|
||||
## Future
|
||||
|
||||
The basic functionality is finished and it's reasonabley fast.
|
||||
|
||||
Topic to work on:
|
||||
|
||||
- Allow to ignore folders (potentially via a cli parameter)
|
||||
- Support for `<base href="/">`
|
||||
- Big Sites Speed improvements (potentially running multiple parsers in parallel for 1000+ pages)
|
||||
- Speed improvements by introducing a "permanent cache" for the parse result (if file did not change, parse result will not change - we still check all links)
|
||||
- Memory consumption check (see if there is room for improvements)
|
||||
- Improve node api
|
||||
- Check external links
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thank you for following along on my journey on creating `check-html-links`. You can find the code on [Github](https://github.com/modernweb-dev/rocket/tree/main/packages/check-html-links).
|
||||
|
||||
Follow us on [Twitter](https://twitter.com/modern_web_dev), or follow me on my personal [Twitter](https://twitter.com/dakmor).
|
||||
|
||||
Thanks to [Julien](https://twitter.com/jlengrand) for feedback and helping turn my scribbles to a followable story.
|
||||
|
||||
If you think my open source work is valuable then I would like you to check out my personal [Github Sponsor Page](https://github.com/sponsors/daKmoR). Or you can support our whole group via the [Modern Web Open Collective](https://opencollective.com/modern-web).
|
||||
|
||||
---
|
||||
|
||||
<span>Photo by <a href="https://unsplash.com/@mihaiteslariu0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Teslariu Mihai</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: Introducing rocket - effective static content with some javascript
|
||||
published: true
|
||||
description: Write Interactive Demos Using Markdown and JavaScript
|
||||
tags: [markdown, javascript, webcomponents, demos]
|
||||
cover_image: /blog/introducing-rocket/images/blog-header.jpg
|
||||
socialMediaImage: /blog/introducing-rocket/images/social-media-image.jpg
|
||||
---
|
||||
|
||||
Welcome to the next level of content creation.
|
||||
|
||||
## Here comes the navigation
|
||||
|
||||
Stuff
|
||||
|
||||
## Another anchor
|
||||
@@ -9,11 +9,8 @@ import { rocketLaunch } from '@rocket/launch';
|
||||
|
||||
export default {
|
||||
presets: [rocketLaunch()],
|
||||
build: {
|
||||
emptyOutputDir: true,
|
||||
pathPrefix: 'subfolder-only-for-build',
|
||||
serviceWorkerFileName: 'service-worker.js',
|
||||
},
|
||||
emptyOutputDir: true,
|
||||
pathPrefix: 'subfolder-only-for-build',
|
||||
};
|
||||
```
|
||||
|
||||
@@ -23,11 +20,25 @@ New plugins can be added and all default plugins can be adjusted or even removed
|
||||
|
||||
```js
|
||||
export default {
|
||||
// add remark/unified plugin to the markdown processing (e.g. enable special code blocks)
|
||||
setupUnifiedPlugins: [],
|
||||
|
||||
// add a rollup plugins to the web dev server (will be wrapped with @web/dev-server-rollup) AND the rollup build (e.g. enable json importing)
|
||||
setupDevAndBuildPlugins: [],
|
||||
|
||||
// add a plugin to the web dev server (will not be wrapped) (e.g. esbuild for typescript)
|
||||
setupDevPlugins: [],
|
||||
|
||||
// add a plugin to the rollup build (e.g. optimization steps)
|
||||
setupBuildPlugins: [],
|
||||
|
||||
// add a plugin to eleventy (e.g. a filter packs)
|
||||
setupEleventyPlugins: [],
|
||||
|
||||
// add a computedConfig to eleventy (e.g. site wide default variables like socialMediaImage)
|
||||
setupEleventyComputedConfig: [],
|
||||
|
||||
// add a plugin to the cli (e.g. a new command like "rocket my-command")
|
||||
setupCliPlugins: [],
|
||||
};
|
||||
```
|
||||
|
||||
41
docs/docs/configuration/setupEleventyComputedConfig.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Configuration >> setupEleventyComputedConfig ||20
|
||||
|
||||
If you want to add data that depends on other data then you can do it via [11ty's computed data](https://www.11ty.dev/docs/data-computed/).
|
||||
|
||||
Rocket exposes it via `setupEleventyComputedConfig`.
|
||||
|
||||
## Set your own data
|
||||
|
||||
Let's say you want to add a `Welcome to the contact page` everyhwere. (a filter might be a better choise but it's a good example of the concept)
|
||||
|
||||
👉 `rocket.config.mjs` (or your theme config file)
|
||||
|
||||
```js
|
||||
import { addPlugin } from 'plugins-manager';
|
||||
|
||||
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
|
||||
const config = {
|
||||
setupEleventyComputedConfig: [
|
||||
addPlugin({ name: 'greeting', plugin: data => `Welcome to the ${data.title} page.` }),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
{% raw %}
|
||||
Now you can use everywhere {{ greeting }}.
|
||||
{% endraw %}
|
||||
And it will correctly replaced with a Welcome and the page title.
|
||||
|
||||
## Default Available Configs
|
||||
|
||||
```js
|
||||
[
|
||||
{ name: 'titleMeta', plugin: titleMetaPlugin },
|
||||
{ name: 'title', plugin: titlePlugin },
|
||||
{ name: 'eleventyNavigation', plugin: eleventyNavigationPlugin },
|
||||
{ name: 'section', plugin: sectionPlugin },
|
||||
{ name: 'socialMediaImage', plugin: socialMediaImagePlugin },
|
||||
];
|
||||
```
|
||||
@@ -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
@@ -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
|
After Width: | Height: | Size: 214 KiB |
@@ -216,7 +216,11 @@ const plugins = finalMetaPlugins.map(pluginObj => {
|
||||
|
||||
**Examples**
|
||||
|
||||
Rollup has a more specific helper
|
||||
Rollup has a more specific helper that handles
|
||||
|
||||
- `config.setupPlugins`
|
||||
|
||||
Note: if you provide `config.plugins` then it will return that directly ignoring `setupPlugins`
|
||||
|
||||
```js
|
||||
import { metaConfigToRollupConfig } from 'plugins-manager';
|
||||
@@ -224,14 +228,19 @@ import { metaConfigToRollupConfig } from 'plugins-manager';
|
||||
const finalConfig = metaConfigToRollupConfig(currentConfig, defaultMetaPlugins);
|
||||
```
|
||||
|
||||
Web Dev Server has a more specific helper
|
||||
Web Dev Server has a more specific helper that handles
|
||||
|
||||
- `config.setupPlugins`
|
||||
- `config.setupRollupPlugins`
|
||||
|
||||
Note: if you provide `config.plugins` then it will return that directly ignoring `setupPlugins` and `setupRollupPlugins`
|
||||
|
||||
```js
|
||||
import { metaConfigToWebDevServerConfig } from 'plugins-manager';
|
||||
import { fromRollup } from '@web/dev-server-rollup';
|
||||
|
||||
const finalConfig = metaConfigToWebDevServerConfig(currentConfig, defaultMetaPlugins, {
|
||||
wrapperFunction: fromRollup,
|
||||
rollupWrapperFunction: fromRollup,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -12,7 +12,6 @@ import { absoluteBaseUrlNetlify } from '@rocket/core/helpers';
|
||||
|
||||
export default /** @type {Partial<import('@rocket/cli').RocketCliOptions>} */ ({
|
||||
presets: [rocketLaunch(), rocketBlog(), rocketSearch()],
|
||||
emptyOutputDir: false,
|
||||
absoluteBaseUrl: absoluteBaseUrlNetlify('http://localhost:8080'),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -51,6 +51,15 @@ Rocket uses the .gitignore file to manage it's requirements. If you skip this st
|
||||
};
|
||||
```
|
||||
|
||||
5. (optionally) Create a file `.eleventyignore` (this file will be needed once you start customizing presets)
|
||||
|
||||
```
|
||||
node_modules/**
|
||||
/docs/_assets
|
||||
/docs/_includes
|
||||
/docs/_data
|
||||
```
|
||||
|
||||
<inline-notification type="warning" title="note">
|
||||
|
||||
All further pathes are relative to your project root (my-project in this case)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Go Live >> Overview
|
||||
# Go Live >> Overview ||10
|
||||
|
||||
A few things are usually needed before going live "for real".
|
||||
|
||||
|
||||
131
docs/guides/go-live/social-media.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
|
||||
Note: If your logo has an `<?xml>` tag it will throw an error as it will be inlined into this svg and nested xml tags are not allowed.
|
||||
|
||||
## 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 `createSocialImage` but provide your own values.
|
||||
|
||||
```js
|
||||
const { createSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createSocialImage({
|
||||
title: 'Learning Rocket',
|
||||
subTitle: 'Have a website',
|
||||
subTitle2: 'in 5 Minutes',
|
||||
footer: 'Rocket Guides',
|
||||
// you can also override the svg only for this page by providing
|
||||
// createSocialImageSvg: async () => '<svg>...</svg>'
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Override the default image
|
||||
|
||||
Often you want to have a unique style for your social media images.
|
||||
For that you can provide your own function which returns a string of an svg to render the image.
|
||||
|
||||
👉 `rocket.config.mjs`
|
||||
|
||||
```js
|
||||
import { adjustPluginOptions } from 'plugins-manager';
|
||||
|
||||
/** @type {Partial<import("@rocket/cli").RocketCliOptions>} */
|
||||
const config = {
|
||||
setupEleventyComputedConfig: [
|
||||
adjustPluginOptions('socialMediaImage', {
|
||||
createSocialImageSvg: async ({
|
||||
title = '',
|
||||
subTitle = '',
|
||||
subTitle2 = '',
|
||||
footer = '',
|
||||
logo = '',
|
||||
}) => {
|
||||
let svgStr = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" style="fill: #ecedef;">
|
||||
<defs/>
|
||||
<rect width="100%" height="100%" fill="#38393e"/>
|
||||
<g transform="matrix(0.45, 0, 0, 0.45, 300, 60)">${logo}</g>
|
||||
<g style="
|
||||
font-size: 70px;
|
||||
text-anchor: middle;
|
||||
font-family: 'Bitstream Vera Sans','Helvetica',sans-serif;
|
||||
font-weight: 700;
|
||||
">
|
||||
<text x="50%" y="470">
|
||||
${title}
|
||||
</text>
|
||||
<text x="50%" y="520" style="font-size: 30px;">
|
||||
${subTitle}
|
||||
</text>
|
||||
</g>
|
||||
<text x="10" y="620" style="font-size: 30px; fill: gray;">
|
||||
${footer}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
return svgStr;
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Using an svg file as a src with nunjucks
|
||||
|
||||
If you have multiple variations it may be easier to save them as svg files and using a template system
|
||||
|
||||
WARNING: Untested example
|
||||
|
||||
👉 `rocket.config.mjs`
|
||||
|
||||
{% raw %}
|
||||
|
||||
```js
|
||||
import { adjustPluginOptions } from 'plugins-manager';
|
||||
|
||||
/** @type {Partial<import("@rocket/cli").RocketCliOptions>} */
|
||||
const config = {
|
||||
setupEleventyComputedConfig: [
|
||||
adjustPluginOptions('socialMediaImage', {
|
||||
createSocialImageSvg: async (args = {}) => {
|
||||
// inside of the svg you can use {{ title }}
|
||||
const svgBuffer = await fs.promises.readFile('/path/to/your/svg/file');
|
||||
const svg = logoBuffer.toString();
|
||||
return nunjucks.renderString(svg, args);
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
13
docs/guides/index.11tydata.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
const { createSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createSocialImage({
|
||||
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,7 +1,106 @@
|
||||
# Presets >> Create your own || 90
|
||||
|
||||
All loaded presets will be combined but you can override each file.
|
||||
A preset is setup function and a folder including `_assets`, `_data` and `_includes` (all optional).
|
||||
|
||||
Take a look at `docs/_merged_includes` and override what you want to override by placing the same filename into `_includes`.
|
||||
To play around with a preset you can create a folder `fire-theme`.
|
||||
|
||||
Also works for `_assets`, `_data` ...
|
||||
You then create the setup function for it with only one property called `path` which will allow Rocket to properly resolve it.
|
||||
|
||||
## Create a Preset Config File
|
||||
|
||||
👉 `fire-theme/fireTheme.js`
|
||||
|
||||
```js
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function fireTheme() {
|
||||
return {
|
||||
path: path.resolve(__dirname),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Once you have that you can start filling in content you need.
|
||||
|
||||
For example a we could override the full `layout.css` by adding a it like so
|
||||
|
||||
👉 `fire-theme/layout.css`
|
||||
|
||||
```css
|
||||
body {
|
||||
background: hotpink;
|
||||
}
|
||||
```
|
||||
|
||||
Once you have that you can add it to your Rocket Config.
|
||||
|
||||
NOTE: The order of presets is important, as for example in this case we take everything from `rocketLaunch` but later override via `fireTheme`.
|
||||
|
||||
👉 `rocket-config.js`
|
||||
|
||||
```js
|
||||
import { rocketLaunch } from '@rocket/launch';
|
||||
import { fireTheme } from 'path/to/fire-theme/fireTheme.js';
|
||||
|
||||
export default {
|
||||
presets: [rocketLaunch(), fireTheme()],
|
||||
};
|
||||
```
|
||||
|
||||
## Publish a preset
|
||||
|
||||
If you would like to publish a preset to use it on multiple websites or share it with your friends you can do like so.
|
||||
|
||||
1. Pick a name for the package => for this example we take `fire-theme`.
|
||||
2. Create a new folder `fire-theme`
|
||||
3. Create a folder `fire-theme/preset` copy `fireTheme.js` from [above](#create-a-preset-config-file) into `preset/fireTheme.js`
|
||||
4. Add a 👉 `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "fire-theme",
|
||||
"version": "0.3.0",
|
||||
"description": "Fire Theme for Rocket",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./preset/": "./preset/"
|
||||
},
|
||||
"files": ["*.js", "preset"],
|
||||
"keywords": ["rocket", "preset"]
|
||||
}
|
||||
```
|
||||
|
||||
5. Add a 👉 `index.js`
|
||||
|
||||
```js
|
||||
export { fireTheme } from './preset/fireTheme.js';
|
||||
```
|
||||
|
||||
6. Add a 👉 `README.md`
|
||||
|
||||
````
|
||||
# FireTheme
|
||||
|
||||
This is a theme/preset for [Rocket](https://rocket.modern-web.dev/).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm i -D fire-theme
|
||||
```
|
||||
|
||||
Add it to your 👉 `rocket.config.js`
|
||||
|
||||
```js
|
||||
import { fireTheme } from 'fire-theme';
|
||||
|
||||
export default {
|
||||
presets: [fireTheme()],
|
||||
};
|
||||
```
|
||||
````
|
||||
|
||||
@@ -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
@@ -0,0 +1,13 @@
|
||||
const { createSocialImage } = require('@rocket/cli');
|
||||
|
||||
module.exports = async function () {
|
||||
const socialMediaImage = await createSocialImage({
|
||||
title: 'Rocket',
|
||||
subTitle: 'Static sites with',
|
||||
subTitle2: 'a sprinkle of JavaScript.',
|
||||
footer: 'A Modern Web Product',
|
||||
});
|
||||
return {
|
||||
socialMediaImage,
|
||||
};
|
||||
};
|
||||
26
package.json
@@ -21,13 +21,15 @@
|
||||
"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",
|
||||
"release": "changeset publish && yarn format",
|
||||
"rocket:build": "node packages/cli/src/cli.js build",
|
||||
"search": "node packages/cli/src/cli.js search",
|
||||
"setup": "npm run setup:ts-configs",
|
||||
"setup": "npm run setup:ts-configs && npm run build:packages",
|
||||
"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,6 +1,20 @@
|
||||
# @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
|
||||
|
||||
- Updated dependencies [a8c7173]
|
||||
- plugins-manager@0.2.0
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1971f5d: Initial Release
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/blog",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -38,6 +38,6 @@
|
||||
"testing"
|
||||
],
|
||||
"dependencies": {
|
||||
"plugins-manager": "^0.1.0"
|
||||
"plugins-manager": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
19
packages/check-html-links/CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# check-html-links
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f343c50: When reading bigger files, especially bigger files with all content on one line it could mean a read chunk is in the middle of a character. This can lead to strange symbols in the resulting string. The `hightWaterMark` is now increased from the node default of 16KB to 256KB. Additionally, the `hightWaterMark` is now synced for reading and parsing.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eb74110: Add info about how many files and links will be checked
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cd22231: Initial release
|
||||
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/).
|
||||
|
||||
## Comparison
|
||||
|
||||
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
@@ -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
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "check-html-links",
|
||||
"version": "0.1.2",
|
||||
"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"
|
||||
}
|
||||
55
packages/check-html-links/src/cli.js
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/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 filesOutput =
|
||||
files.length == 0
|
||||
? '🧐 No files to check. Did you select the correct folder?'
|
||||
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`;
|
||||
console.log(filesOutput);
|
||||
|
||||
const { errors, numberLinks } = await validateFiles(files, rootDir);
|
||||
|
||||
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
|
||||
|
||||
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
@@ -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
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
291
packages/check-html-links/src/validateFolder.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/* 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 streamOptions = { highWaterMark: 256 * 1024 };
|
||||
|
||||
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
|
||||
const saxWasmBuffer = fs.readFileSync(saxPath);
|
||||
const parserReferences = new SAXParser(SaxEventType.Attribute, streamOptions);
|
||||
const parserIds = new SAXParser(SaxEventType.Attribute, streamOptions);
|
||||
|
||||
/** @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, streamOptions);
|
||||
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, streamOptions);
|
||||
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();
|
||||
let numberLinks = 0;
|
||||
for (const htmlFilePath of files) {
|
||||
const { links } = await extractReferences(htmlFilePath);
|
||||
numberLinks += links.length;
|
||||
await resolveLinks(links, { htmlFilePath, rootDir });
|
||||
}
|
||||
await validateLocalFiles(checkLocalFiles);
|
||||
|
||||
return { errors: errors, numberLinks: numberLinks };
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,6 +1,100 @@
|
||||
# @rocket/cli
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c92769a: Processing links and asset urls to generate the final html output is now utf8 safe
|
||||
- 562e91f: Make sure logos do not have "<?xml" in their code
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 0eb507d: Adds the capability to configure the svg template for the social media images.
|
||||
- 0eb507d: Adds `setupEleventyComputedConfig` option to enable configuration of 11ty's `eleventyComputed`. The plugins-manager system is used for it.
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a498a5d: Make sure links to `*index.md` files are not treated as folder index files like `index.md`
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cd22231: Restructure and simplify Rocket Cli Plugin System
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cd22231: Add a default Rocket Cli Plugin which checks all links on every save (during start) and after a production build
|
||||
- Updated dependencies [cd22231]
|
||||
- Updated dependencies [cd22231]
|
||||
- @rocket/eleventy-plugin-mdjs-unified@0.3.0
|
||||
- check-html-links@0.1.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 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
|
||||
|
||||
- 641c7e5: Add a pathPrefix option to allow deployment to a subdirectory
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a8c7173: Changes to config:
|
||||
- Do not auto rollupWrap plugins added via `setupDevPlugins`.
|
||||
- If you provide `devServer.plugins` then it will return that directly ignoring `setupDevAndBuildPlugins` and `setupDevPlugins`
|
||||
- Updated dependencies [a8c7173]
|
||||
- plugins-manager@0.2.0
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1971f5d: Initial Release
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const { setComputedConfig, getComputedConfig } = require('./src/public/computedConfig.cjs');
|
||||
const { generateEleventyComputed } = require('./src/public/generateEleventyComputed.cjs');
|
||||
const { createSocialImage } = require('./src/public/createSocialImage.cjs');
|
||||
|
||||
module.exports = {
|
||||
setComputedConfig,
|
||||
getComputedConfig,
|
||||
generateEleventyComputed,
|
||||
createSocialImage,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/cli",
|
||||
"version": "0.1.0",
|
||||
"version": "0.4.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -38,6 +38,7 @@
|
||||
"*.mjs",
|
||||
"dist",
|
||||
"dist-types",
|
||||
"preset",
|
||||
"src"
|
||||
],
|
||||
"keywords": [
|
||||
@@ -50,20 +51,23 @@
|
||||
],
|
||||
"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.1",
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-usage": "^6.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"plugins-manager": "^0.1.0"
|
||||
"plugins-manager": "^0.2.0",
|
||||
"utf8": "^3.0.0"
|
||||
},
|
||||
"types": "dist-types/index.d.ts"
|
||||
}
|
||||
|
||||
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
@@ -0,0 +1,5 @@
|
||||
const { generateEleventyComputed } = require('@rocket/cli');
|
||||
|
||||
module.exports = {
|
||||
...generateEleventyComputed(),
|
||||
};
|
||||
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
@@ -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
@@ -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,10 +23,21 @@ async function buildAndWrite(config) {
|
||||
}
|
||||
|
||||
async function productionBuild(config) {
|
||||
// const serviceWorkerFileName =
|
||||
// config.build && config.build.serviceWorkerFileName
|
||||
// ? config.build.serviceWorkerFileName
|
||||
// : 'service-worker.js';
|
||||
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',
|
||||
@@ -38,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,
|
||||
],
|
||||
@@ -58,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +199,7 @@ export class RocketCli {
|
||||
setComputedConfig({});
|
||||
if (this.eleventy) {
|
||||
this.eleventy.finish();
|
||||
// this.eleventy.stopWatch();
|
||||
// await this.eleventy.stopWatch();
|
||||
}
|
||||
this.stop();
|
||||
}
|
||||
|
||||
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,14 +7,23 @@ import { metaConfigToWebDevServerConfig } from 'plugins-manager';
|
||||
/** @typedef {import('@web/dev-server').DevServerConfig} DevServerConfig */
|
||||
|
||||
export class RocketStart {
|
||||
static pluginName = 'RocketStart';
|
||||
commands = ['start'];
|
||||
|
||||
/**
|
||||
* @param {RocketCliOptions} config
|
||||
*/
|
||||
setupCommand(config) {
|
||||
delete config.pathPrefix;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {RocketCliOptions} options.config
|
||||
* @param {any} options.argv
|
||||
*/
|
||||
async setup({ config, argv }) {
|
||||
async setup({ config, argv, eleventy }) {
|
||||
this.__argv = argv;
|
||||
this.config = {
|
||||
...config,
|
||||
@@ -22,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(
|
||||
{
|
||||
@@ -39,10 +55,11 @@ export class RocketStart {
|
||||
clearTerminalOnReload: false,
|
||||
...this.config.devServer,
|
||||
|
||||
setupPlugins: [...this.config.setupDevAndBuildPlugins, ...this.config.setupDevPlugins],
|
||||
setupRollupPlugins: this.config.setupDevAndBuildPlugins,
|
||||
setupPlugins: this.config.setupDevPlugins,
|
||||
},
|
||||
[],
|
||||
{ wrapperFunction: fromRollup },
|
||||
{ rollupWrapperFunction: fromRollup },
|
||||
);
|
||||
|
||||
this.devServer = await startDevServer({
|
||||
|
||||
221
packages/cli/src/eleventy-plugins/processLocalReferences.cjs
Normal file
@@ -0,0 +1,221 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utf8 = require('utf8');
|
||||
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 isTemplateFile(href) {
|
||||
for (const templateEnding of templateEndings) {
|
||||
if (href.endsWith(templateEnding)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isIndexTemplateFile(href) {
|
||||
const hrefParsed = path.parse(href);
|
||||
const indexTemplateEndings = templateEndings.map(ending => `index${ending}`);
|
||||
|
||||
for (const indexTemplateEnding of indexTemplateEndings) {
|
||||
if (hrefParsed.base === indexTemplateEnding) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} inValue
|
||||
*/
|
||||
function getValueAndAnchor(inValue) {
|
||||
let value = inValue.replace(/&#/g, '--__check-html-links__--');
|
||||
let anchor = '';
|
||||
let suffix = '';
|
||||
|
||||
if (value.includes('#')) {
|
||||
[value, anchor] = value.split('#');
|
||||
suffix = `#${anchor}`;
|
||||
}
|
||||
if (value.includes('?')) {
|
||||
value = value.split('?')[0];
|
||||
}
|
||||
if (anchor.includes(':~:')) {
|
||||
anchor = anchor.split(':~:')[0];
|
||||
}
|
||||
if (value.includes(':~:')) {
|
||||
value = value.split(':~:')[0];
|
||||
}
|
||||
|
||||
value = value.replace(/--__check-html-links__--/g, '&#');
|
||||
anchor = anchor.replace(/--__check-html-links__--/g, '&#');
|
||||
suffix = suffix.replace(/--__check-html-links__--/g, '&#');
|
||||
value = value.trim();
|
||||
anchor = anchor.trim();
|
||||
|
||||
return {
|
||||
value,
|
||||
anchor,
|
||||
suffix,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateNewHrefs(hrefs, inputPath) {
|
||||
const newHrefs = [];
|
||||
for (const hrefObj of hrefs) {
|
||||
const newHrefObj = { ...hrefObj };
|
||||
const { value: href, suffix } = getValueAndAnchor(hrefObj.value);
|
||||
|
||||
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 content = utf8.encode(_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 utf8.decode(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
|
||||
@@ -26,13 +31,14 @@ export async function normalizeConfig(inConfig) {
|
||||
setupDevPlugins: [],
|
||||
setupBuildPlugins: [],
|
||||
setupEleventyPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
setupCliPlugins: [],
|
||||
eleventy: () => {},
|
||||
command: 'help',
|
||||
watch: true,
|
||||
inputDir: 'docs',
|
||||
outputDir: '_site',
|
||||
outputDevDir: path.resolve('_site-dev'),
|
||||
outputDevDir: '_site-dev',
|
||||
build: {},
|
||||
devServer: {},
|
||||
|
||||
@@ -75,7 +81,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);
|
||||
|
||||
@@ -100,6 +107,12 @@ export async function normalizeConfig(inConfig) {
|
||||
...preset.setupEleventyPlugins,
|
||||
];
|
||||
}
|
||||
if (preset.setupEleventyComputedConfig) {
|
||||
config.setupEleventyComputedConfig = [
|
||||
...config.setupEleventyComputedConfig,
|
||||
...preset.setupEleventyComputedConfig,
|
||||
];
|
||||
}
|
||||
if (preset.setupCliPlugins) {
|
||||
config.setupCliPlugins = [...config.setupCliPlugins, ...preset.setupCliPlugins];
|
||||
}
|
||||
@@ -109,8 +122,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)) {
|
||||
|
||||
45
packages/cli/src/public/createSocialImage.cjs
Normal file
@@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Image = require('@11ty/eleventy-img');
|
||||
const { getComputedConfig } = require('./computedConfig.cjs');
|
||||
const { createSocialImageSvg: defaultcreateSocialImageSvg } = require('./createSocialImageSvg.cjs');
|
||||
|
||||
async function createSocialImage(args) {
|
||||
const {
|
||||
title = '',
|
||||
subTitle = '',
|
||||
footer = '',
|
||||
createSocialImageSvg = defaultcreateSocialImageSvg,
|
||||
} = args;
|
||||
const cleanedUpArgs = { ...args };
|
||||
delete cleanedUpArgs.createSocialImageSvg;
|
||||
|
||||
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();
|
||||
|
||||
if (logo.includes('<?xml')) {
|
||||
throw new Error('You should not have an "<?xml" tag in your logo.svg');
|
||||
}
|
||||
|
||||
const svgStr = await createSocialImageSvg({ logo, ...args });
|
||||
|
||||
// TODO: cache images for 24h and not only for the given run (using @11ty/eleventy-cache-assets)
|
||||
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 = {
|
||||
createSocialImage,
|
||||
};
|
||||
33
packages/cli/src/public/createSocialImageSvg.cjs
Normal file
@@ -0,0 +1,33 @@
|
||||
async function createSocialImageSvg({
|
||||
title = '',
|
||||
subTitle = '',
|
||||
subTitle2 = '',
|
||||
footer = '',
|
||||
logo = '',
|
||||
}) {
|
||||
let svgStr = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630">
|
||||
<defs></defs>
|
||||
<rect width="100%" height="100%" fill="#fff" />
|
||||
<circle cx="1000" cy="230" r="530" fill="#ebebeb"></circle>
|
||||
<g transform="matrix(0.6, 0, 0, 0.6, 580, 100)">${logo}</g>
|
||||
<text x="70" y="200" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700" font-size="80">
|
||||
${title}
|
||||
</text>
|
||||
<text x="70" y="320" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700" font-size="60">
|
||||
${subTitle}
|
||||
</text>
|
||||
<text x="70" y="420" font-family="'Bitstream Vera Sans','Helvetica',sans-serif" font-weight="700" font-size="60">
|
||||
${subTitle2}
|
||||
</text>
|
||||
<text x="70" y="560" fill="gray" font-size="40">
|
||||
${footer}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
return svgStr;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSocialImageSvg,
|
||||
};
|
||||
116
packages/cli/src/public/generateEleventyComputed.cjs
Normal file
@@ -0,0 +1,116 @@
|
||||
const fs = require('fs');
|
||||
const { processContentWithTitle } = require('@rocket/core/title');
|
||||
const { createSocialImage: defaultcreateSocialImage } = require('./createSocialImage.cjs');
|
||||
const { getComputedConfig } = require('./computedConfig.cjs');
|
||||
const { executeSetupFunctions } = require('plugins-manager');
|
||||
|
||||
function titleMetaPlugin() {
|
||||
return 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 {};
|
||||
};
|
||||
}
|
||||
|
||||
function titlePlugin() {
|
||||
return async data => {
|
||||
if (data.title) {
|
||||
return data.title;
|
||||
}
|
||||
return data.titleMeta?.title;
|
||||
};
|
||||
}
|
||||
|
||||
function eleventyNavigationPlugin() {
|
||||
return async data => {
|
||||
if (data.eleventyNavigation) {
|
||||
return data.eleventyNavigation;
|
||||
}
|
||||
return data.titleMeta?.eleventyNavigation;
|
||||
};
|
||||
}
|
||||
|
||||
function sectionPlugin() {
|
||||
return 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];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function socialMediaImagePlugin(args = {}) {
|
||||
const { createSocialImage = defaultcreateSocialImage } = args;
|
||||
|
||||
const cleanedUpArgs = { ...args };
|
||||
delete cleanedUpArgs.createSocialImage;
|
||||
|
||||
return async data => {
|
||||
if (data.socialMediaImage) {
|
||||
return data.socialMediaImage;
|
||||
}
|
||||
if (!data.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = data.titleMeta.parts ? data.titleMeta.parts[0] : '';
|
||||
const subTitle =
|
||||
data.titleMeta.parts && data.titleMeta.parts[1] ? `in ${data.titleMeta.parts[1]}` : '';
|
||||
const section = data.section ? ' ' + data.section[0].toUpperCase() + data.section.slice(1) : '';
|
||||
const footer = `${data.site.name}${section}`;
|
||||
|
||||
const imgUrl = await createSocialImage({
|
||||
title,
|
||||
subTitle,
|
||||
footer,
|
||||
section,
|
||||
...cleanedUpArgs,
|
||||
});
|
||||
return imgUrl;
|
||||
};
|
||||
}
|
||||
|
||||
function generateEleventyComputed() {
|
||||
const rocketConfig = getComputedConfig();
|
||||
|
||||
let metaPlugins = [
|
||||
{ name: 'titleMeta', plugin: titleMetaPlugin },
|
||||
{ name: 'title', plugin: titlePlugin },
|
||||
{ name: 'eleventyNavigation', plugin: eleventyNavigationPlugin },
|
||||
{ name: 'section', plugin: sectionPlugin },
|
||||
{ name: 'socialMediaImage', plugin: socialMediaImagePlugin },
|
||||
];
|
||||
|
||||
const finalMetaPlugins = executeSetupFunctions(
|
||||
rocketConfig.setupEleventyComputedConfig,
|
||||
metaPlugins,
|
||||
);
|
||||
|
||||
const eleventyComputed = {};
|
||||
for (const pluginObj of finalMetaPlugins) {
|
||||
if (pluginObj.options) {
|
||||
eleventyComputed[pluginObj.name] = pluginObj.plugin(pluginObj.options);
|
||||
} else {
|
||||
eleventyComputed[pluginObj.name] = pluginObj.plugin();
|
||||
}
|
||||
}
|
||||
|
||||
return eleventyComputed;
|
||||
}
|
||||
|
||||
module.exports = { generateEleventyComputed };
|
||||
@@ -8,6 +8,7 @@ const rocketCollections = require('../eleventy-plugins/rocketCollections.cjs');
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
const config = getComputedConfig();
|
||||
|
||||
const { pathPrefix, _inputDirCwdRelative, outputDevDir } = config;
|
||||
|
||||
let metaPlugins = [
|
||||
@@ -46,12 +47,16 @@ module.exports = function (eleventyConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const pluginObj of metaPlugins) {
|
||||
if (pluginObj.options) {
|
||||
eleventyConfig.addPlugin(pluginObj.plugin, pluginObj.options);
|
||||
} else {
|
||||
eleventyConfig.addPlugin(pluginObj.plugin);
|
||||
try {
|
||||
for (const pluginObj of metaPlugins) {
|
||||
if (pluginObj.options) {
|
||||
eleventyConfig.addPlugin(pluginObj.plugin, pluginObj.options);
|
||||
} else {
|
||||
eleventyConfig.addPlugin(pluginObj.plugin);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('An eleventy plugin had an error', err);
|
||||
}
|
||||
|
||||
if (config.eleventy) {
|
||||
|
||||
182
packages/cli/test-node/RocketCli.computedConfig.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import chai from 'chai';
|
||||
import chalk from 'chalk';
|
||||
import { executeStart, readOutput, readStartOutput } from './helpers.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('RocketCli computedConfig', () => {
|
||||
let cli;
|
||||
|
||||
before(() => {
|
||||
// ignore colors in tests as most CIs won't support it
|
||||
chalk.level = 0;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (cli?.cleanup) {
|
||||
await cli.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('will extract a title from markdown and set first folder as section', async () => {
|
||||
cli = await executeStart('computed-config-fixtures/headlines/rocket.config.js');
|
||||
|
||||
const indexHtml = await readOutput(cli, 'index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
const [indexTitle, indexSection] = indexHtml.split('\n');
|
||||
expect(indexTitle).to.equal('Root');
|
||||
expect(indexSection).to.be.undefined;
|
||||
|
||||
const subHtml = await readOutput(cli, '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(cli, '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(cli, '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(cli, '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 = await executeStart('computed-config-fixtures/social-images/rocket.config.js');
|
||||
|
||||
const indexHtml = await readStartOutput(cli, 'index.html');
|
||||
expect(indexHtml).to.equal('/_merged_assets/11ty-img/c4c29ec7-1200.png');
|
||||
|
||||
const guidesHtml = await readStartOutput(cli, 'guides/index.html');
|
||||
expect(guidesHtml).to.equal('/_merged_assets/11ty-img/5e6f6f8c-1200.png');
|
||||
|
||||
const gettingStartedHtml = await readStartOutput(
|
||||
cli,
|
||||
'guides/first-pages/getting-started/index.html',
|
||||
);
|
||||
expect(gettingStartedHtml).to.equal('/_merged_assets/11ty-img/d989ab1a-1200.png');
|
||||
});
|
||||
|
||||
it('can override the svg function globally to adjust all social media image', async () => {
|
||||
cli = await executeStart('computed-config-fixtures/social-images-override/rocket.config.js');
|
||||
|
||||
const indexHtml = await readStartOutput(cli, 'index.html');
|
||||
expect(indexHtml).to.equal('/_merged_assets/11ty-img/d76265ed-1200.png');
|
||||
|
||||
const guidesHtml = await readStartOutput(cli, 'guides/index.html');
|
||||
expect(guidesHtml).to.equal('/_merged_assets/11ty-img/d76265ed-1200.png');
|
||||
|
||||
const gettingStartedHtml = await readStartOutput(
|
||||
cli,
|
||||
'guides/first-pages/getting-started/index.html',
|
||||
);
|
||||
expect(gettingStartedHtml).to.equal('/_merged_assets/11ty-img/d76265ed-1200.png');
|
||||
});
|
||||
|
||||
it('will add "../" for links and image urls only within named template files', async () => {
|
||||
cli = await executeStart('computed-config-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(cli, 'template/index.html');
|
||||
expect(templateHtml, 'template/index.html does not match').to.equal(
|
||||
namedHtmlContent.join('\n'),
|
||||
);
|
||||
|
||||
const guidesHtml = await readStartOutput(cli, 'guides/index.html');
|
||||
expect(guidesHtml, 'guides/index.html does not match').to.equal(
|
||||
[...namedMdContent, ...namedHtmlContent].join('\n'),
|
||||
);
|
||||
|
||||
const noAdjustHtml = await readStartOutput(cli, '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(cli, 'one-level/raw/index.html');
|
||||
expect(rawHtml, 'raw/index.html does not match').to.equal(
|
||||
[
|
||||
'<div>',
|
||||
' <a href="../../">Root</a>',
|
||||
' <a href="../../guides/#with-anchor">Guides</a>',
|
||||
' <img src="../../images/my-img.svg" alt="my-img">',
|
||||
' <img src="/images/my-img.svg" alt="absolute-img">',
|
||||
' <picture>',
|
||||
' <source media="(min-width:465px)" srcset="/images/picture-min-465.jpg">',
|
||||
' <img src="../../images/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
|
||||
' </picture>',
|
||||
'</div>',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
// for index files no '../' will be added
|
||||
const indexHtml = await readStartOutput(cli, '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>',
|
||||
'<a href="./rules/tabindex/">EndingIndex</a>',
|
||||
'<img src="./images/my-img.svg" alt="my-img">',
|
||||
'<img src="/images/my-img.svg" alt="absolute-img"></p>',
|
||||
'<div>',
|
||||
' <a href="./">Root</a>',
|
||||
' 👇<a href="guides/#with-anchor">Guides</a>',
|
||||
' 👉 <a href="./one-level/raw/">Raw</a>',
|
||||
' <a href="template/">Template</a>',
|
||||
' <a href="./rules/tabindex/">EndingIndex</a>',
|
||||
' <img src="./images/my-img.svg" alt="my-img">',
|
||||
' <img src="/images/my-img.svg" alt="absolute-img">',
|
||||
' <picture>',
|
||||
' <source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">',
|
||||
' <img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">',
|
||||
' </picture>',
|
||||
'</div>',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured via setupEleventyComputedConfig', async () => {
|
||||
cli = await executeStart('computed-config-fixtures/setup/addPlugin.rocket.config.js');
|
||||
|
||||
const indexHtml = await readOutput(cli, 'index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
expect(indexHtml).to.equal('test-value');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +71,18 @@ describe('RocketCli e2e', () => {
|
||||
await cli.run();
|
||||
}
|
||||
|
||||
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,12 +133,14 @@ describe('RocketCli e2e', () => {
|
||||
],
|
||||
});
|
||||
|
||||
await expectThrowsAsync(() => execute(), /Error in your Eleventy config file.*/);
|
||||
await expectThrowsAsync(() => execute(), {
|
||||
errorMatch: /Error in your Eleventy config file.*/,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupDevAndBuildPlugins in config', () => {
|
||||
it('can add a rollup plugin to build', async () => {
|
||||
it('can add a rollup plugin via setupDevAndBuildPlugins for build command', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'build',
|
||||
@@ -135,7 +153,7 @@ describe('RocketCli e2e', () => {
|
||||
expect(inlineModule).to.equal('var a={test:"data"};console.log(a);');
|
||||
});
|
||||
|
||||
it('can add a rollup plugin to dev', async () => {
|
||||
it('can add a rollup plugin via setupDevAndBuildPlugins for start command', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'start',
|
||||
@@ -199,39 +217,62 @@ 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.skip('can add a pathprefix for the build output', async () => {
|
||||
it('can add a pathPrefix that will not influence the start command', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'build',
|
||||
'start',
|
||||
'--config-file',
|
||||
path.join(__dirname, 'e2e-fixtures', 'content', 'eleventy.rocket.config.js'),
|
||||
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
|
||||
],
|
||||
});
|
||||
await execute();
|
||||
|
||||
// const indexHtml = await readOutput('index.html', {
|
||||
// type: 'start',
|
||||
// });
|
||||
// expect(indexHtml).to.equal("<p>Markdown in 'docs/page/index.md'</p>");
|
||||
const linkHtml = await readOutput('link/index.html', {
|
||||
type: 'start',
|
||||
});
|
||||
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.skip('works with an empty object in rocket.config.js', async () => {
|
||||
it('can add a pathPrefix that will be used in the build command', async () => {
|
||||
cli = new RocketCli({
|
||||
argv: [
|
||||
'build',
|
||||
'--config-file',
|
||||
path.join(__dirname, 'e2e-fixtures', 'content', 'empty.rocket.config.js'),
|
||||
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
|
||||
],
|
||||
});
|
||||
await execute();
|
||||
|
||||
// const indexHtml = await readOutput('index.html', {
|
||||
// type: 'start',
|
||||
// });
|
||||
// expect(indexHtml).to.equal("<p>Markdown in 'docs/page/index.md'</p>");
|
||||
const linkHtml = await readOutput('link/index.html', {
|
||||
stripServiceWorker: true,
|
||||
stripToBody: true,
|
||||
});
|
||||
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('smoke test for link checking', async () => {
|
||||
await expectThrowsAsync(() => executeLint('e2e-fixtures/lint-links/rocket.config.js'), {
|
||||
errorMatch: /Found 1 missing reference targets/,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -0,0 +1,21 @@
|
||||
[Root](./)
|
||||
[Guides](./guides.md#with-anchor)
|
||||
[Raw](./one-level/raw.html)
|
||||
[Template](./template.njk)
|
||||
[EndingIndex](./rules/tabindex.md)
|
||||

|
||||

|
||||
|
||||
<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>
|
||||
<a href="./rules/tabindex.md">EndingIndex</a>
|
||||
<img src="./images/my-img.svg" alt="my-img">
|
||||
<img src="/images/my-img.svg" alt="absolute-img">
|
||||
<picture>
|
||||
<source media="(min-width:465px)" srcset="./images/picture-min-465.jpg">
|
||||
<img src="./images/picture-fallback.jpg" alt="Fallback" style="width:auto;">
|
||||
</picture>
|
||||
</div>
|
||||