Compare commits

...

49 Commits

Author SHA1 Message Date
github-actions[bot]
14721d1e0f Version Packages 2021-04-20 12:58:41 +02:00
Thomas Allmer
0f6709ac4b fix(mdjs-preview): initial setting should come from the element 2021-04-20 12:55:32 +02:00
Thomas Allmer
ed86ff2346 fix(cli): do not set data-localize-lang in the simulator iframe html tag 2021-04-20 12:55:32 +02:00
Mathieu Puech
c675820163 fix: windows path issue avoid filtering of index section of collections 2021-04-20 12:28:19 +02:00
Konstantinos Norgias
f4a0ab559f fix: add changeset & update drawer 2021-04-20 12:27:12 +02:00
Konstantinos Norgias
a8cdaebab1 fix(simulator): document shadowRoot n/a in iframe 2021-04-20 12:27:12 +02:00
github-actions[bot]
22393dd172 Version Packages 2021-04-20 06:50:18 +02:00
Thomas Allmer
a6fdb31f1e fix(mdjs-preview): do not restory empty values 2021-04-19 23:55:52 +02:00
Thomas Allmer
dd15d4fc65 chore: fix tests 2021-04-19 23:55:52 +02:00
Thomas Allmer
edb1abf82b feat(mdjs-preview): rework preview and add a simulation mode 2021-04-19 23:55:52 +02:00
Thomas Allmer
0b6411661e chore: update lion dependencies for drawer and search 2021-04-19 23:55:52 +02:00
Thomas Allmer
604a80e6cb feat(mdjs-story): force /define entrypoint 2021-04-19 23:55:52 +02:00
Thomas Allmer
fe6a929f1e feat(mdjs-core): keep the original code block and wrap it for preview story 2021-04-19 23:55:52 +02:00
Thomas Allmer
2267e728cf feat(eleventy-plugin-mdjs-unified): write mdjs javascript to file instead of inline 2021-04-19 23:55:52 +02:00
Thomas Allmer
abc8a02b72 fix(cli): supporting an absolute path for the rootDir 2021-04-19 23:55:52 +02:00
Thomas Allmer
2270887faf chore: format package.json 2021-04-19 23:55:52 +02:00
Thomas Allmer
bad4686506 feat(building-rollup): preserve export names & attributes on script tags, 2021-04-19 23:55:52 +02:00
Thomas Allmer
818caad7cb Create chilled-turkeys-help.md 2021-04-04 18:01:06 +02:00
Konstantinos Norgias
672b7e893e chore: generalize label & add alt when no img 2021-04-04 18:01:06 +02:00
Thomas Allmer
a8e66d84f4 feat(mdjs-core): extract mdjsSetupCode function which support a highlightCode fn 2021-04-04 18:00:26 +02:00
github-actions[bot]
e9090d64b9 Version Packages 2021-04-01 20:01:47 +02:00
Benny Powers
728a205b7b chore: add changeset 2021-04-01 19:44:43 +02:00
Benny Powers
67ba29d45a feat(navigation): add no-redirects attribute
By default, the sidebar nav redirects clicks on category headings to
their first child.

To disable those redirects, override
`_includes/_joiningBlocks/_layoutSidebar/sidebar/20-navigation.njk`
and add the `no-redirects` attribute to the `<rocket-navigation>`
element.
2021-04-01 19:44:43 +02:00
github-actions[bot]
758caffdf9 Version Packages 2021-03-25 07:14:15 +01:00
qa46hx
302227e8a9 feat(search): add variable for border-radius of SearchCombobox 2021-03-24 23:20:56 +01:00
Thomas Allmer
04a31220fb chore: align versions across the mono repo 2021-03-15 21:03:07 +01:00
Benny Powers
d44a23af0c Merge pull request #83 from modernweb-dev/changeset-release/main
Version Packages
2021-03-07 10:14:39 +02:00
github-actions[bot]
18a79589c2 Version Packages 2021-03-06 19:28:35 +00:00
Thomas Allmer
b7727b0e10 chore: add rocket nav upgrade to cli 2021-03-06 20:26:44 +01:00
Thomas Allmer
5edc40fed5 feat(cli): make sure each instance has its own eleventy config 2021-03-06 19:58:09 +01:00
Amin Yahyaabadi
be0d0b3ca1 fix: add missing main entry to the packages (#81)
This allows the tools to work properly. For example, eslint-plugin-import, TypeScript LSP hyperclick, and many other tools rely on main.
2021-03-06 19:10:49 +01:00
Thomas Allmer
ef8ebb0098 feat(eleventy-rocket-nav): support dynamically created content 2021-03-06 19:05:00 +01:00
djlauk
2fa61e1377 chore: tiny fixes to the README (#74) 2021-02-23 21:45:41 +01:00
Matsuuu
b23e37f38e feat(search): Precache search results to service worker 2021-02-23 21:44:53 +01:00
Matsuuu
cf45e32702 feat(search): Add ellipsis as prefix when truncating 2021-02-23 21:44:53 +01:00
Matsuuu
b5965c6c37 feat(search): Set cursor to pointer on result hover 2021-02-23 21:44:53 +01:00
Matsuuu
e39cc45d23 fix(search): Center search icon 2021-02-23 21:44:53 +01:00
Matsuuu
f0434cb12c feat(search): Add feedback when no results found 2021-02-23 21:44:53 +01:00
Matsuuu
c87caaed2d feat: Allow overlay query modification in Drawer (#73) 2021-02-23 21:31:12 +01:00
Thomas Allmer
04af7ecf53 chore: align dependency versions 2021-02-23 20:39:37 +01:00
github-actions[bot]
98d6aad12a Version Packages 2021-02-05 13:30:10 +01:00
Thomas Allmer
ee6b404aaa fix: mdjs element pass on shadowRoot to story function 2021-02-05 13:26:42 +01:00
Thomas Allmer
8ba8939c67 chore: use test-helper everywhere 2021-02-04 20:48:08 +01:00
Thomas Allmer
8e095b792e fix(cli): watching user preset files to trigger updates automatically 2021-02-04 19:56:56 +01:00
github-actions[bot]
b58ac27658 Version Packages 2021-02-04 09:51:19 +01:00
Thomas Allmer
f44a0f4fd4 fix(cli): rewrite dynamic imports with "`" 2021-02-04 09:45:35 +01:00
Thomas Allmer
750418bb51 fix: use class node api of check-html-links in rocket 2021-02-03 23:17:29 +01:00
Guillaume Grossetie
bc2698c1ba resolves #54 introduce a ignoreLinkPatterns option 2021-02-03 20:43:02 +01:00
Thomas Allmer
74f7ddf478 fix: create social media images only during build 2021-02-03 20:39:16 +01:00
125 changed files with 2896 additions and 767 deletions

View File

@@ -43,7 +43,7 @@
## The Goal for Rocket ## The Goal for Rocket
> Our goal is to provide developers with a meta framework for static websites with a spricle of JavaScript. > Our goal is to provide developers with a meta framework for static websites with a sprinkle of JavaScript.
Get a site up and running in no time and focus on the content. Get a site up and running in no time and focus on the content.
You can still tweak every detail of every underlying tool that gets used. You can still tweak every detail of every underlying tool that gets used.
@@ -54,7 +54,7 @@ Rocket is part of the [Modern Web Family](https://twitter.com/modern_web_dev).
We are always looking for contributors of all skill levels! If you're looking to ease your way into the project, try out a [good first issue](https://github.com/modernweb-dev/rocket/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We are always looking for contributors of all skill levels! If you're looking to ease your way into the project, try out a [good first issue](https://github.com/modernweb-dev/rocket/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
If you are interested in helping contribute to Modern Web, please take a look at our [Contributing Guide](https://github.com/modernweb-dev/rocket/blob/main/CONTRIBUTING.md). Also, feel free to drop into [slack](https://rocket.modern-web.dev/discover/slack/) and say hi. 👋 If you are interested in helping contribute to Modern Web, please take a look at our [Contributing Guide](https://github.com/modernweb-dev/rocket/blob/main/CONTRIBUTING.md). Also, feel free to drop into [slack](https://rocket.modern-web.dev/about/slack/) and say hi. 👋
### Financial Contributors ### Financial Contributors

10
docs/_assets/body.css Normal file
View File

@@ -0,0 +1,10 @@
html {
--demo-background-color: #eee;
--demo-color: #222;
}
html[theme="dark"] body {
background: #333;
--demo-background-color: #888;
--demo-color: #eee;
}

View File

@@ -7,3 +7,5 @@
rel="stylesheet" rel="stylesheet"
/> />
<meta name="twitter:creator" content="@modern_web_dev" /> <meta name="twitter:creator" content="@modern_web_dev" />
<link rel="stylesheet" href="{{ '/_assets/body.css' | asset | url }}" mdjs-use>

View File

@@ -0,0 +1,69 @@
class DemoElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.platform = 'the web';
this.language = 'en-US';
this.theme = 'light';
this.observer = new MutationObserver(this.updateData);
}
updateData = () => {
this.platform = document.documentElement.getAttribute('platform') || 'the web';
this.language = document.documentElement.getAttribute('data-lang') || 'en-US';
this.theme = document.documentElement.getAttribute('theme') || 'light';
this.requestUpdate();
};
connectedCallback() {
this.updateData();
this.observer.observe(document.documentElement, { attributes: true });
}
requestUpdate() {
this.shadowRoot.innerHTML = this.render();
}
render() {
return `
<style>
:host {
display: block;
background: var(--demo-background-color);
color: var(--demo-color);
padding: 10px;
}
:host[platform~="web"] {
border-bottom: 2px solid #333;
}
@media screen and (min-width: 640px) {
.about {
display: flex;
}
.about ul {
width: 50%;
}
}
</style>
<p>Hello I am DemoElement 👋</p>
<div class="about">
<ul>
<li>My purpose is to demonstrate how an element can adopt to different environments</li>
<li>I like <strong>${this.platform}</strong></li>
</ul>
<ul>
<li>My mother languages is <strong>${this.language}</strong></li>
<li>I feel very comfortable in the <strong>${this.theme}</strong></li>
</ul>
</div>
`;
}
}
customElements.define('demo-element', DemoElement);

View File

@@ -56,8 +56,8 @@ mdjs comes with some additional helpers you can choose to import:
````md ````md
```js script ```js script
import '@mdjs/mdjs-story/mdjs-story.js'; import '@mdjs/mdjs-story/define';
import '@mdjs/mdjs-preview/mdjs-preview.js'; import '@mdjs/mdjs-preview/define';
``` ```
```` ````
@@ -65,8 +65,8 @@ Once loaded you can use them like so:
````md ````md
```js script ```js script
import '@mdjs/mdjs-story/mdjs-story.js'; import '@mdjs/mdjs-story/define';
import '@mdjs/mdjs-preview/mdjs-preview.js'; import '@mdjs/mdjs-preview/define';
``` ```
```` ````
@@ -118,8 +118,8 @@ export const JsPreviewStory = () => html` <demo-wc-card>JS Preview Story</demo-w
Here is a live example from [demo-wc-card](https://www.npmjs.com/package/demo-wc-card). Here is a live example from [demo-wc-card](https://www.npmjs.com/package/demo-wc-card).
```js script ```js script
import '@mdjs/mdjs-story/mdjs-story.js'; import '@mdjs/mdjs-story/define';
import '@mdjs/mdjs-preview/mdjs-preview.js'; import '@mdjs/mdjs-preview/define';
import { html } from 'lit-html'; import { html } from 'lit-html';
``` ```
@@ -132,31 +132,6 @@ export const header = () => {
## Supported Systems ## Supported Systems
### es-dev-server
Preview your mdjs readme with live demos and auto reload.
- Add to your `package.json`:
```json
"scripts": {
"start": "es-dev-server",
}
```
- Create a `es-dev-server.config.js` in the root of your repository.
```js
const { mdjsTransformer } = require('@mdjs/core');
module.exports = {
nodeResolve: true,
open: 'README.md',
watch: true,
responseTransformers: [mdjsTransformer],
};
```
### Storybook ### Storybook
Please check out [@open-wc/demoing-storybook](https://open-wc.org/demoing/) for a fully integrated setup. Please check out [@open-wc/demoing-storybook](https://open-wc.org/demoing/) for a fully integrated setup.

View File

@@ -2,18 +2,165 @@
You can showcase live running code by annotating a code block with `js preview-story`. You can showcase live running code by annotating a code block with `js preview-story`.
````md ## Features
```js preview-story
import { html } from 'lit-html';
export const foo = () => html` <p>my html</p> `; - Shows components inside the page as they are
- You can enable “Simulation Mode” to break them out
- Simulation mode renders in an iframe to supporting media queries and isolated Simulation settings
- Simulation Settings
- Style (windows, mac, android, iOS)
- Size (small, medium, large, Galaxy S5, iPhone X, iPad …)
- Automatic Height
- Theme (light, dark)
- Language (en, nl, …)
- Settings are ”global” for all Simulators (e.g. changing one will change all)
- Settings can be remembered for other pages / return visits
```js script
import { html } from 'lit-html';
import './assets/demo-element.js';
```
## JavaScript Story
````md
```js script
import { html } from 'lit-html';
import './assets/demo-element.js';
```
```js preview-story
export const foo = () => html`<demo-element></demo-element>`;
``` ```
```` ````
will result in will result in
```js preview-story ```js preview-story
import { html } from 'lit-html'; export const foo = () => html` <demo-element></demo-element> `;
```
export const foo = () => html` <p>my html</p> `;
## HTML Story
````md
```html preview-story
<demo-element></demo-element>
```
````
will result in
```html preview-story
<demo-element></demo-element>
```
## Setup Simulation Mode
For simulation mode we need a dedicated html file that will be used as an iframe target while loading stories.
The fastest way to create such a file is to use the `layout-simulator` layout.
Create a file `docs/simulator.md` with the following content.
```md
---
layout: layout-simulator
eleventyExcludeFromCollections: true
excludeFromSearch: true
---
```
Once you have that you need to configure it for the story renderer by setting it in your `rocket.config.js`.
```js
export default {
setupUnifiedPlugins: [
adjustPluginOptions('mdjsSetupCode', {
simulationSettings: { simulatorUrl: '/simulator/' },
}),
],
};
```
<inline-notification type="tip">
You can freely choose the path for the "simulator" by creating the md file in a different folder and adjusting the path in the config.
</inline-notification>
## Simulator states
To simulate these stats that usually come from the device itself we put those infos on the document tag.
We can simulate the following settings
1. `platform`
Adopting styles and behavior depending on which device platform you are.
```html
<html platform="web"></html>
<html platform="android"></html>
<html platform="ios"></html>
<!-- potentially later -->
<html platform="web-windows"></html>
<html platform="web-mac"></html>
```
2. `theme`
Adjust your styles based on a theme - light/dark are the default but you can add as many as you want.
```html
<html theme="light"></html>
<html theme="dark"></html>
```
3. `language`
Best to relay on `data-lang` and `lang` often gets changes by translations services which may interfere with you translation loading system.
```html
<html lang="en-US" data-lang="en-US"></html>
<html lang="de-DE" data-lang="de-DE"></html>
```
If you want to react to such document changes you can use an [MutationObserver](https://developer.mozilla.org/de/docs/Web/API/MutationObserver).
For a vanilla web component it could look something like this:
```js
class DemoElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.platform = 'the web';
this.language = 'en-US';
this.theme = 'light';
this.observer = new MutationObserver(this.updateData);
}
updateData = () => {
this.platform = document.documentElement.getAttribute('platform') || 'the web';
this.language = document.documentElement.getAttribute('data-lang') || 'en-US';
this.theme = document.documentElement.getAttribute('theme') || 'light';
this.requestUpdate();
};
connectedCallback() {
this.updateData();
this.observer.observe(document.documentElement, { attributes: true });
}
requestUpdate() {
this.shadowRoot.innerHTML = this.render();
}
render() {
return `
...
`;
}
}
customElements.define('demo-element', DemoElement);
```
```js script
import '@rocket/launch/inline-notification/inline-notification.js';
``` ```

View File

@@ -2,10 +2,16 @@
You can showcase live running code by annotating a code block with `js story`. You can showcase live running code by annotating a code block with `js story`.
````md ```js script
```js story
import { html } from 'lit-html'; import { html } from 'lit-html';
```
````md
```js script
import { html } from 'lit-html';
```
```js story
export const foo = () => html` <p>my html</p> `; export const foo = () => html` <p>my html</p> `;
``` ```
```` ````
@@ -13,7 +19,5 @@ export const foo = () => html` <p>my html</p> `;
will result in will result in
```js story ```js story
import { html } from 'lit-html';
export const foo = () => html` <p>my html</p> `; export const foo = () => html` <p>my html</p> `;
``` ```

View File

@@ -1,7 +1,17 @@
# Tools >> Check HTML Links ||30 # Tools >> Check HTML Links ||30
```js
import '@rocket/launch/inline-notification/inline-notification.js';
```
A fast checker for broken links/references in HTML. A fast checker for broken links/references in HTML.
<inline-notification type="tip">
Read the [Introducing Check HTMl Links - no more bad links](../../blog/introducing-check-html-links.md) Blog post to find out how it came to be and how it works.
</inline-notification>
## Features ## Features
- Checks all html files for broken local links/references (in href, src, srcset) - Checks all html files for broken local links/references (in href, src, srcset)
@@ -16,10 +26,25 @@ A fast checker for broken links/references in HTML.
npm i -D check-html-links npm i -D check-html-links
``` ```
## Usage ## CLI flags
``` | Name | Type | Description |
| ------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| root-dir | string | the root directory to serve files from. Defaults to the current working directory |
| ignore-link-pattern | string | do not check links matching the pattern |
| continue-on-error | boolean | if present it will not exit with an error code - useful while writing or for temporary passing a ci |
## Usage Examples
```bash
# check a folder _site
npx check-html-links _site npx check-html-links _site
# ignore all links like <a href="/users/123">
npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
# ignore all links like <a href="/users/123"> & <a href="/users/123/details">
npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
``` ```
## Example Output ## Example Output

View File

@@ -47,3 +47,9 @@ export const headlineConverter = () => html`
``` ```
How it then works is very similar to https://www.11ty.dev/docs/plugins/navigation/ How it then works is very similar to https://www.11ty.dev/docs/plugins/navigation/
## Sidebar redirects
By default, the sidebar nav redirects clicks on category headings to the first child page in that category.
To disable those redirects, override `_includes/_joiningBlocks/_layoutSidebar/sidebar/20-navigation.njk` and add the `no-redirects` attribute to the `<rocket-navigation>` element.

View File

@@ -129,3 +129,25 @@ const config = {
{% endraw %} {% endraw %}
``` ```
## Enabling / Disabling
Generating images from SVG is quite fast but it can still add that's why by default during `rocket start` there will be no social media images created.
If you with so create them also during start you can
```js
const config = {
start: {
createSocialMediaImages: true,
},
};
```
Similarly, if you never want to create social media images even during build then you can globally disable it via
```js
const config = {
createSocialMediaImages: true,
};
```

5
docs/simulator.md Normal file
View File

@@ -0,0 +1,5 @@
---
layout: layout-simulator
eleventyExcludeFromCollections: true
excludeFromSearch: true
---

View File

@@ -38,6 +38,6 @@
"testing" "testing"
], ],
"dependencies": { "dependencies": {
"plugins-manager": "^0.2.0" "plugins-manager": "^0.2.1"
} }
} }

View File

@@ -1,5 +1,17 @@
# @rocket/building-rollup # @rocket/building-rollup
## 0.2.0
### Minor Changes
- bad4686: Preserve attributes on script tags. Preserve export names.
## 0.1.3
### Patch Changes
- be0d0b3: fix: add missing main entry to the packages
## 0.1.2 ## 0.1.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rocket/building-rollup", "name": "@rocket/building-rollup",
"version": "0.1.2", "version": "0.2.0",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -13,6 +13,7 @@
}, },
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)", "author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
"homepage": "https://rocket.modern-web.dev/docs/tools/building-rollup/", "homepage": "https://rocket.modern-web.dev/docs/tools/building-rollup/",
"main": "./index.js",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./index.js" ".": "./index.js"
@@ -54,9 +55,9 @@
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-node-resolve": "^11.0.1", "@rollup/plugin-node-resolve": "^11.0.1",
"@web/rollup-plugin-html": "^1.4.0", "@web/rollup-plugin-html": "^1.6.0",
"@web/rollup-plugin-import-meta-assets": "^1.0.4", "@web/rollup-plugin-import-meta-assets": "^1.0.4",
"@web/rollup-plugin-polyfills-loader": "^1.0.3", "@web/rollup-plugin-polyfills-loader": "^1.1.0",
"browserslist": "^4.16.1", "browserslist": "^4.16.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-workbox": "^6.1.0" "rollup-plugin-workbox": "^6.1.0"

View File

@@ -22,7 +22,7 @@ export function createBasicMetaConfig(userConfig = { output: {} }) {
const assetName = `[${developmentMode ? 'name' : 'hash'}][extname]`; const assetName = `[${developmentMode ? 'name' : 'hash'}][extname]`;
const config = { const config = {
preserveEntrySignatures: false, preserveEntrySignatures: 'strict',
treeshake: !developmentMode, treeshake: !developmentMode,
setupPlugins: [], setupPlugins: [],
...userConfig, ...userConfig,

View File

@@ -53,7 +53,7 @@ export function createSpaMetaConfig(userConfig = { output: {} }) {
// directory to match patterns against to be precached // directory to match patterns against to be precached
globDirectory: path.join(config.output.dir), globDirectory: path.join(config.output.dir),
// cache any html js and css by default // cache any html js and css by default
globPatterns: ['**/*.{html,js,css,webmanifest}'], globPatterns: ['**/*.{html,js,css,webmanifest}', '**/*-search-index.json'],
skipWaiting: true, skipWaiting: true,
clientsClaim: true, clientsClaim: true,
runtimeCaching: [ runtimeCaching: [

View File

@@ -75,6 +75,14 @@ describe('createMapConfig', () => {
'<h1>Has js in sub-js/index.html</h1>\n\n\n<script type="module" src="../sub-js.js"></script>', '<h1>Has js in sub-js/index.html</h1>\n\n\n<script type="module" src="../sub-js.js"></script>',
); );
const subJsAbsoluteIndexHtml = await readOutput('sub-js-absolute/index.html', {
stripToBody: true,
stripServiceWorker: true,
});
expect(subJsAbsoluteIndexHtml).to.equal(
'<h1>Has js in sub-js-absolute/index.html</h1>\n\n\n<script type="module" src="../sub-js-absolute.js"></script>',
);
const serviceWorkerJs = await readOutput('service-worker.js'); const serviceWorkerJs = await readOutput('service-worker.js');
expect(serviceWorkerJs).to.include('Promise'); // not empty string might be enough... expect(serviceWorkerJs).to.include('Promise'); // not empty string might be enough...
}); });

View File

@@ -0,0 +1,2 @@
<h1>Has js in sub-js-absolute/index.html</h1>
<script type="module" src="/sub-js-absolute/sub-js-absolute.js"></script>

View File

@@ -1,5 +1,25 @@
# check-html-links # check-html-links
## 0.2.1
### Patch Changes
- be0d0b3: fix: add missing main entry to the packages
## 0.2.0
### Minor Changes
- 750418b: Uses a class for the CLI and adding the following options:
- `--root-dir` the root directory to serve files from. Defaults to the current working directory
- `--ignore-link-pattern` do not check links matching the pattern
- `--continue-on-error` if present it will not exit with an error code - useful while writing or for temporary passing a ci
BREAKING CHANGE:
- Exists with an error code if a broken link is found
## 0.1.2 ## 0.1.2
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "check-html-links", "name": "check-html-links",
"version": "0.1.2", "version": "0.2.1",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -13,6 +13,7 @@
}, },
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)", "author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
"homepage": "https://rocket.modern-web.dev/docs/tools/check-html-links/", "homepage": "https://rocket.modern-web.dev/docs/tools/check-html-links/",
"main": "./index.js",
"bin": { "bin": {
"check-html-links": "src/cli.js" "check-html-links": "src/cli.js"
}, },
@@ -33,7 +34,9 @@
], ],
"dependencies": { "dependencies": {
"chalk": "^4.0.0", "chalk": "^4.0.0",
"command-line-args": "^5.1.1",
"glob": "^7.0.0", "glob": "^7.0.0",
"minimatch": "^3.0.4",
"sax-wasm": "^2.0.0" "sax-wasm": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/** @typedef {import('../types/main').CheckHtmlLinksCliOptions} CheckHtmlLinksCliOptions */
import path from 'path';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import { validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
export class CheckHtmlLinksCli {
/** @type {CheckHtmlLinksCliOptions} */
options;
constructor({ argv } = { argv: undefined }) {
const mainDefinitions = [
{ name: 'ignore-link-pattern', type: String, multiple: true },
{ name: 'root-dir', type: String, defaultOption: true },
{ name: 'continue-on-error', type: Boolean, defaultOption: false },
];
const options = commandLineArgs(mainDefinitions, {
stopAtFirstUnknown: true,
argv,
});
this.options = {
printOnError: true,
continueOnError: options['continue-on-error'],
rootDir: options['root-dir'],
ignoreLinkPatterns: options['ignore-link-pattern'],
};
}
/**
* @param {Partial<CheckHtmlLinksCliOptions>} newOptions
*/
setOptions(newOptions) {
this.options = {
...this.options,
...newOptions,
};
}
async run() {
const { ignoreLinkPatterns, rootDir: userRootDir } = this.options;
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const files = await listFiles('**/*.html', rootDir);
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, { ignoreLinkPatterns });
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
const performance = process.hrtime(performanceStart);
/** @type {string[]} */
let output = [];
let message = '';
if (errors.length > 0) {
let referenceCount = 0;
for (const error of errors) {
referenceCount += error.usage.length;
}
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`,
];
message = output.join('\n');
if (this.options.printOnError === true) {
console.error(message);
}
if (this.options.continueOnError === false) {
process.exit(1);
}
} else {
console.log(
`✅ All internal links are valid. (executed in ${performance[0]}s ${
performance[1] / 1000000
}ms)`,
);
}
return { errors, message };
}
}

View File

@@ -1,55 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import path from 'path'; import { CheckHtmlLinksCli } from './CheckHtmlLinksCli.js';
import chalk from 'chalk';
import { validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
async function main() { const cli = new CheckHtmlLinksCli();
const userRootDir = process.argv[2]; cli.run();
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();

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import fs from 'fs'; import fs from 'fs';
import saxWasm from 'sax-wasm'; import saxWasm from 'sax-wasm';
import minimatch from 'minimatch';
import { createRequire } from 'module'; import { createRequire } from 'module';
import { listFiles } from './listFiles.js'; import { listFiles } from './listFiles.js';
@@ -10,6 +11,7 @@ import path from 'path';
/** @typedef {import('../types/main').LocalFile} LocalFile */ /** @typedef {import('../types/main').LocalFile} LocalFile */
/** @typedef {import('../types/main').Usage} Usage */ /** @typedef {import('../types/main').Usage} Usage */
/** @typedef {import('../types/main').Error} Error */ /** @typedef {import('../types/main').Error} Error */
/** @typedef {import('../types/main').Options} Options */
/** @typedef {import('sax-wasm').Attribute} Attribute */ /** @typedef {import('sax-wasm').Attribute} Attribute */
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
@@ -185,8 +187,9 @@ function getValueAndAnchor(inValue) {
* @param {object} options * @param {object} options
* @param {string} options.htmlFilePath * @param {string} options.htmlFilePath
* @param {string} options.rootDir * @param {string} options.rootDir
* @param {function(string): boolean} options.ignoreUsage
*/ */
async function resolveLinks(links, { htmlFilePath, rootDir }) { async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
for (const hrefObj of links) { for (const hrefObj of links) {
const { value, anchor } = getValueAndAnchor(hrefObj.value); const { value, anchor } = getValueAndAnchor(hrefObj.value);
@@ -201,7 +204,9 @@ async function resolveLinks(links, { htmlFilePath, rootDir }) {
let valueFile = value.endsWith('/') ? path.join(value, 'index.html') : value; let valueFile = value.endsWith('/') ? path.join(value, 'index.html') : value;
if (value.includes('mailto:')) { if (ignoreUsage(value)) {
// ignore
} else if (value.includes('mailto:')) {
// ignore for now - could add a check to validate if the email address is valid // ignore for now - could add a check to validate if the email address is valid
} else if (valueFile === '' && anchor !== '') { } else if (valueFile === '' && anchor !== '') {
addLocalFile(htmlFilePath, anchor, usageObj); addLocalFile(htmlFilePath, anchor, usageObj);
@@ -261,8 +266,9 @@ async function validateLocalFiles(checkLocalFiles) {
/** /**
* @param {string[]} files * @param {string[]} files
* @param {string} rootDir * @param {string} rootDir
* @param {Options} opts?
*/ */
export async function validateFiles(files, rootDir) { export async function validateFiles(files, rootDir, opts) {
await parserReferences.prepareWasm(saxWasmBuffer); await parserReferences.prepareWasm(saxWasmBuffer);
await parserIds.prepareWasm(saxWasmBuffer); await parserIds.prepareWasm(saxWasmBuffer);
@@ -270,10 +276,20 @@ export async function validateFiles(files, rootDir) {
checkLocalFiles = []; checkLocalFiles = [];
idCache = new Map(); idCache = new Map();
let numberLinks = 0; let numberLinks = 0;
const ignoreLinkPatternRegExps = opts
? opts.ignoreLinkPatterns?.map(pattern => minimatch.makeRe(pattern))
: null;
/** @type {function(string): boolean} */
const ignoreUsage = ignoreLinkPatternRegExps
? usage => !!ignoreLinkPatternRegExps.find(regExp => usage.match(regExp))
: () => false;
for (const htmlFilePath of files) { for (const htmlFilePath of files) {
const { links } = await extractReferences(htmlFilePath); const { links } = await extractReferences(htmlFilePath);
numberLinks += links.length; numberLinks += links.length;
await resolveLinks(links, { htmlFilePath, rootDir }); await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
} }
await validateLocalFiles(checkLocalFiles); await validateLocalFiles(checkLocalFiles);
@@ -282,10 +298,11 @@ export async function validateFiles(files, rootDir) {
/** /**
* @param {string} inRootDir * @param {string} inRootDir
* @param {Options} opts?
*/ */
export async function validateFolder(inRootDir) { export async function validateFolder(inRootDir, opts) {
const rootDir = path.resolve(inRootDir); const rootDir = path.resolve(inRootDir);
const files = await listFiles('**/*.html', rootDir); const files = await listFiles('**/*.html', rootDir);
const { errors } = await validateFiles(files, rootDir); const { errors } = await validateFiles(files, rootDir, opts);
return errors; return errors;
} }

View File

@@ -0,0 +1,5 @@
<!-- ignore known subsystems -->
<a href="/docs/"></a>
<a href="/developer/getting-started.html#js"></a>
<a href="/developer/language-guides/"></a>
<a href="/developer/javascript.html"></a>

View File

@@ -0,0 +1,8 @@
<a href="/absolute/index.html"></a>
<a href="./relative/index.html"></a>
<a href="./relative/subfolder/index.html"></a>
<!-- valid -->
<a href="./page.html"></a>
<a href=" ./page.html "></a>
<a href=" /page.html "></a>

View File

@@ -5,9 +5,9 @@ import { validateFolder } from 'check-html-links';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
export async function execute(inPath) { export async function execute(inPath, opts) {
const testDir = path.join(__dirname, inPath.split('/').join(path.sep)); const testDir = path.join(__dirname, inPath.split('/').join(path.sep));
const errors = await validateFolder(testDir); const errors = await validateFolder(testDir, opts);
return { return {
cleanup: items => { cleanup: items => {
const newItems = []; const newItems = [];

View File

@@ -183,6 +183,28 @@ describe('validateFolder', () => {
expect(cleanup(errors)).to.deep.equal([]); expect(cleanup(errors)).to.deep.equal([]);
}); });
it('ignoring a folder', async () => {
const { errors, cleanup } = await execute('fixtures/internal-link-ignore', {
ignoreLinkPatterns: ['./relative/*', './relative/**/*'],
});
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-link-ignore/absolute/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
character: 9,
file: 'fixtures/internal-link-ignore/index.html',
line: 0,
value: '/absolute/index.html',
},
],
},
]);
});
it('can handle img src', async () => { it('can handle img src', async () => {
const { errors, cleanup } = await execute('fixtures/internal-images'); const { errors, cleanup } = await execute('fixtures/internal-images');
expect(cleanup(errors)).to.deep.equal([ expect(cleanup(errors)).to.deep.equal([

View File

@@ -25,3 +25,14 @@ export interface Error {
onlyAnchorMissing: boolean; onlyAnchorMissing: boolean;
usage: Usage[]; usage: Usage[];
} }
interface Options {
ignoreLinkPatterns: string[] | null;
}
export interface CheckHtmlLinksCliOptions {
printOnError: boolean;
rootDir: string;
ignoreLinkPatterns: string[] | null;
continueOnError: boolean;
}

View File

@@ -1,5 +1,66 @@
# @rocket/cli # @rocket/cli
## 0.6.2
### Patch Changes
- ed86ff2: Do not set `data-localize-lang` in the simulator iframe html tag
- f4a0ab5: Pass document shadowRoot to iframe
- c675820: fix: windows path issue avoid filtering of index section of collections
## 0.6.1
### Patch Changes
- abc8a02: You can now use an absolute path for the rootDir
- Updated dependencies [bad4686]
- Updated dependencies [2267e72]
- @rocket/building-rollup@0.2.0
- @rocket/eleventy-plugin-mdjs-unified@0.4.0
## 0.6.0
### Minor Changes
- 5edc40f: Make sure each rocket instance has it's own eleventy config'
- ef8ebb0: To support dynamically created content to be part of the anchor navigation of the page we now analyze the final html output instead of `entry.templateContent`.
BREAKING CHANGE:
- only add anchors for the currently active pages (before it added anchor for every page)
### Patch Changes
- be0d0b3: fix: add missing main entry to the packages
- Updated dependencies [be0d0b3]
- Updated dependencies [ef8ebb0]
- @rocket/building-rollup@0.1.3
- check-html-links@0.2.1
- @rocket/core@0.1.2
- plugins-manager@0.2.1
- @rocket/eleventy-rocket-nav@0.3.0
## 0.5.2
### Patch Changes
- 8e095b7: Watching `_assets`, `_data`, `_includes` for changes to trigger updated automatically
## 0.5.1
### Patch Changes
- f44a0f4: Rewrite dynamic imports with "`"
- 74f7ddf: Adds performance improvements for social media images by:
- creating social media images only in `rocket build` phase
- adds a config `createSocialMediaImages` to enable (default) or disable it globally
- adds config `start.createSocialMediaImages` to enable or disable (default) it during `rocket start`
- 750418b: Use class-based node API of check-html-links
- Updated dependencies [f44a0f4]
- Updated dependencies [750418b]
- @rocket/eleventy-plugin-mdjs-unified@0.3.1
- check-html-links@0.2.0
## 0.5.0 ## 0.5.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rocket/cli", "name": "@rocket/cli",
"version": "0.5.0", "version": "0.6.2",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -13,6 +13,7 @@
}, },
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)", "author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
"homepage": "https://rocket.modern-web.dev/", "homepage": "https://rocket.modern-web.dev/",
"main": "./index.cjs",
"bin": { "bin": {
"rocket": "src/cli.js" "rocket": "src/cli.js"
}, },
@@ -56,21 +57,23 @@
"dependencies": { "dependencies": {
"@11ty/eleventy": "^0.11.1", "@11ty/eleventy": "^0.11.1",
"@11ty/eleventy-img": "^0.7.4", "@11ty/eleventy-img": "^0.7.4",
"@rocket/building-rollup": "^0.1.2", "@rocket/building-rollup": "^0.2.0",
"@rocket/core": "^0.1.1", "@rocket/core": "^0.1.2",
"@rocket/eleventy-plugin-mdjs-unified": "^0.3.0", "@rocket/eleventy-plugin-mdjs-unified": "^0.4.0",
"@rocket/eleventy-rocket-nav": "^0.2.1", "@rocket/eleventy-rocket-nav": "^0.3.0",
"@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-babel": "^5.2.2",
"@rollup/plugin-node-resolve": "^11.0.1", "@rollup/plugin-node-resolve": "^11.0.1",
"@web/config-loader": "^0.1.3", "@web/config-loader": "^0.1.3",
"@web/dev-server": "^0.1.4", "@web/dev-server": "^0.1.4",
"@web/dev-server-rollup": "^0.3.2", "@web/dev-server-rollup": "^0.3.2",
"@web/rollup-plugin-copy": "^0.2.0", "@web/rollup-plugin-copy": "^0.2.0",
"check-html-links": "^0.1.2", "check-html-links": "^0.2.1",
"command-line-args": "^5.1.1", "command-line-args": "^5.1.1",
"command-line-usage": "^6.1.1", "command-line-usage": "^6.1.1",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"plugins-manager": "^0.2.0", "micromatch": "^4.0.2",
"plugins-manager": "^0.2.1",
"slash": "^3.0.0",
"utf8": "^3.0.0" "utf8": "^3.0.0"
}, },
"types": "dist-types/index.d.ts" "types": "dist-types/index.d.ts"

View File

@@ -0,0 +1,69 @@
<html theme="light" platform="web" lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
margin: 0;
height: fit-content;
}
html[edge-distance] body {
padding: 8px;
}
</style>
<script type="module">
import { render } from 'lit-html';
async function onHashChange() {
const urlParts = new URLSearchParams(document.location.hash.substr(1));
if (urlParts.get('stylesheets')) {
for (const stylesheet of urlParts.getAll('stylesheets')) {
if (!document.querySelector(`link[rel="stylesheet"][href="${stylesheet}"]`)) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = stylesheet;
document.head.appendChild(link);
}
}
}
if (urlParts.get('theme')) {
document.documentElement.setAttribute('theme', urlParts.get('theme'));
}
if (urlParts.get('platform')) {
document.documentElement.setAttribute('platform', urlParts.get('platform'));
}
if (urlParts.get('language')) {
document.documentElement.setAttribute('lang', urlParts.get('language'));
document.documentElement.setAttribute('data-lang', urlParts.get('language'));
}
if (urlParts.get('story-key')) {
document.documentElement.setAttribute('story-key', urlParts.get('story-key'));
}
if (urlParts.get('edge-distance') === 'true') {
document.documentElement.setAttribute('edge-distance', '');
} else {
document.documentElement.removeAttribute('edge-distance');
}
const mod = await import(urlParts.get('story-file'));
render(mod[urlParts.get('story-key')]({ shadowRoot: document }), document.body);
}
window.addEventListener('hashchange', onHashChange, false);
onHashChange();
const observer = new ResizeObserver(() => {
const dimensions = document.body.getBoundingClientRect();
const data = {
action: 'mdjs-viewer-resize',
storyKey: document.documentElement.getAttribute('story-key'),
width: dimensions.width,
height: dimensions.height,
};
parent.postMessage(JSON.stringify(data), '*');
});
observer.observe(document.body);
</script>
</head>
<body></body>
</html>

View File

@@ -3,6 +3,7 @@
import { rollup } from 'rollup'; import { rollup } from 'rollup';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path';
import { copy } from '@web/rollup-plugin-copy'; import { copy } from '@web/rollup-plugin-copy';
import { createMpaConfig } from '@rocket/building-rollup'; import { createMpaConfig } from '@rocket/building-rollup';
@@ -45,7 +46,7 @@ async function productionBuild(config) {
dir: config.outputDir, dir: config.outputDir,
}, },
// custom // custom
rootDir: config.outputDevDir, rootDir: path.resolve(config.outputDevDir),
absoluteBaseUrl: config.absoluteBaseUrl, absoluteBaseUrl: config.absoluteBaseUrl,
setupPlugins: [ setupPlugins: [
...defaultSetupPlugins, ...defaultSetupPlugins,

View File

@@ -10,6 +10,7 @@ import computedConfigPkg from './public/computedConfig.cjs';
import path from 'path'; import path from 'path';
import Eleventy from '@11ty/eleventy'; import Eleventy from '@11ty/eleventy';
import TemplateConfig from '@11ty/eleventy/src/TemplateConfig.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import fs from 'fs-extra'; import fs from 'fs-extra';
@@ -33,6 +34,35 @@ export class RocketEleventy extends Eleventy {
await super.write(); await super.write();
await this.__rocketCli.update(); await this.__rocketCli.update();
} }
// forks it so we can watch for changes but don't include them while building
getChokidarConfig() {
let ignores = this.eleventyFiles.getGlobWatcherIgnores();
const keepWatching = [
path.join(this.__rocketCli.config._inputDirCwdRelative, '_assets', '**'),
path.join(this.__rocketCli.config._inputDirCwdRelative, '_data', '**'),
path.join(this.__rocketCli.config._inputDirCwdRelative, '_includes', '**'),
];
ignores = ignores.filter(ignore => !keepWatching.includes(ignore));
// debug("Ignoring watcher changes to: %o", ignores);
let configOptions = this.config.chokidarConfig;
// cant override these yet
// TODO maybe if array, merge the array?
delete configOptions.ignored;
return Object.assign(
{
ignored: ignores,
ignoreInitial: true,
// also interesting: awaitWriteFinish
},
configOptions,
);
}
} }
export class RocketCli { export class RocketCli {
@@ -70,11 +100,16 @@ export class RocketCli {
await this.mergePresets(); await this.mergePresets();
const elev = new RocketEleventy(_inputDirCwdRelative, outputDevDir, this); const elev = new RocketEleventy(_inputDirCwdRelative, outputDevDir, this);
elev.isVerbose = false;
// 11ty always wants a relative path to cwd - why? // 11ty always wants a relative path to cwd - why?
const rel = path.relative(process.cwd(), path.join(__dirname)); const rel = path.relative(process.cwd(), path.join(__dirname));
const relCwdPathToConfig = path.join(rel, 'shared', '.eleventy.cjs'); const relCwdPathToConfig = path.join(rel, 'shared', '.eleventy.cjs');
const config = new TemplateConfig(null, relCwdPathToConfig);
elev.config = config.getConfig();
elev.resetConfig();
elev.setConfigPathOverride(relCwdPathToConfig); elev.setConfigPathOverride(relCwdPathToConfig);
elev.isVerbose = false;
await elev.init(); await elev.init();
this.eleventy = elev; this.eleventy = elev;

View File

@@ -2,8 +2,7 @@
/** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */ /** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */
import chalk from 'chalk'; import { CheckHtmlLinksCli } from 'check-html-links';
import { validateFolder, formatErrors } from 'check-html-links';
export class RocketLint { export class RocketLint {
static pluginName = 'RocketLint'; static pluginName = 'RocketLint';
@@ -49,31 +48,20 @@ export class RocketLint {
return; return;
} }
const performanceStart = process.hrtime(); const checkLinks = new CheckHtmlLinksCli();
console.log('👀 Checking if all internal links work...'); checkLinks.setOptions({
const errors = await validateFolder(this.config.lintInputDir); rootDir: this.config.lintInputDir,
const performance = process.hrtime(performanceStart); printOnError: false,
continueOnError: true,
});
const { errors, message } = await checkLinks.run();
if (errors.length > 0) { if (errors.length > 0) {
let referenceCount = 0; if (this.config.command === 'start') {
for (const error of errors) { console.log(message);
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 { } else {
throw new Error(output.join('\n')); throw new Error(message);
} }
} else {
console.log('✅ All internal links are valid.');
} }
} }

View File

@@ -15,6 +15,7 @@ export class RocketStart {
*/ */
setupCommand(config) { setupCommand(config) {
delete config.pathPrefix; delete config.pathPrefix;
config.createSocialMediaImages = !!config?.start?.createSocialMediaImages;
return config; return config;
} }

View File

@@ -1,5 +1,6 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const slash = require('slash');
const { readdirSync } = require('fs'); const { readdirSync } = require('fs');
function getDirectories(source) { function getDirectories(source) {
@@ -23,7 +24,7 @@ const rocketCollections = {
let docs = [ let docs = [
...collection.getFilteredByGlob(`${_inputDirCwdRelative}/${section}/**/*.md`), ...collection.getFilteredByGlob(`${_inputDirCwdRelative}/${section}/**/*.md`),
]; ];
docs = docs.filter(page => page.inputPath !== `./${indexSection}`); docs = docs.filter(page => page.inputPath !== `./${slash(indexSection)}`);
return docs; return docs;
}); });

View File

@@ -36,6 +36,7 @@ export async function normalizeConfig(inConfig) {
eleventy: () => {}, eleventy: () => {},
command: 'help', command: 'help',
watch: true, watch: true,
createSocialMediaImages: true,
inputDir: 'docs', inputDir: 'docs',
outputDir: '_site', outputDir: '_site',
outputDevDir: '_site-dev', outputDevDir: '_site-dev',

View File

@@ -2,14 +2,14 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const Image = require('@11ty/eleventy-img'); const Image = require('@11ty/eleventy-img');
const { getComputedConfig } = require('./computedConfig.cjs'); const { getComputedConfig } = require('./computedConfig.cjs');
const { createSocialImageSvg: defaultcreateSocialImageSvg } = require('./createSocialImageSvg.cjs'); const { createSocialImageSvg: defaultCreateSocialImageSvg } = require('./createSocialImageSvg.cjs');
async function createSocialImage(args) { async function createSocialImage(args) {
const { const {
title = '', title = '',
subTitle = '', subTitle = '',
footer = '', footer = '',
createSocialImageSvg = defaultcreateSocialImageSvg, createSocialImageSvg = defaultCreateSocialImageSvg,
} = args; } = args;
const cleanedUpArgs = { ...args }; const cleanedUpArgs = { ...args };
delete cleanedUpArgs.createSocialImageSvg; delete cleanedUpArgs.createSocialImageSvg;

View File

@@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { processContentWithTitle } = require('@rocket/core/title'); const { processContentWithTitle } = require('@rocket/core/title');
const { createSocialImage: defaultcreateSocialImage } = require('./createSocialImage.cjs'); const { createSocialImage: defaultCreateSocialImage } = require('./createSocialImage.cjs');
const { getComputedConfig } = require('./computedConfig.cjs'); const { getComputedConfig } = require('./computedConfig.cjs');
const { executeSetupFunctions } = require('plugins-manager'); const { executeSetupFunctions } = require('plugins-manager');
@@ -71,7 +71,7 @@ function layoutPlugin({ defaultLayout = 'layout-default' } = {}) {
} }
function socialMediaImagePlugin(args = {}) { function socialMediaImagePlugin(args = {}) {
const { createSocialImage = defaultcreateSocialImage } = args; const { createSocialImage = defaultCreateSocialImage, rocketConfig = {} } = args;
const cleanedUpArgs = { ...args }; const cleanedUpArgs = { ...args };
delete cleanedUpArgs.createSocialImage; delete cleanedUpArgs.createSocialImage;
@@ -80,6 +80,11 @@ function socialMediaImagePlugin(args = {}) {
if (data.socialMediaImage) { if (data.socialMediaImage) {
return data.socialMediaImage; return data.socialMediaImage;
} }
if (rocketConfig.createSocialMediaImages === false) {
return;
}
if (!data.title) { if (!data.title) {
return; return;
} }
@@ -158,7 +163,7 @@ function generateEleventyComputed() {
{ name: 'title', plugin: titlePlugin }, { name: 'title', plugin: titlePlugin },
{ name: 'eleventyNavigation', plugin: eleventyNavigationPlugin }, { name: 'eleventyNavigation', plugin: eleventyNavigationPlugin },
{ name: 'section', plugin: sectionPlugin }, { name: 'section', plugin: sectionPlugin },
{ name: 'socialMediaImage', plugin: socialMediaImagePlugin }, { name: 'socialMediaImage', plugin: socialMediaImagePlugin, options: { rocketConfig } },
{ name: '_joiningBlocks', plugin: joiningBlocksPlugin, options: rocketConfig }, { name: '_joiningBlocks', plugin: joiningBlocksPlugin, options: rocketConfig },
{ name: 'layout', plugin: layoutPlugin }, { name: 'layout', plugin: layoutPlugin },
]; ];

View File

@@ -19,6 +19,7 @@ export function setFixtureDir(importMetaUrl) {
* @property {boolean} stripToBody * @property {boolean} stripToBody
* @property {boolean} stripStartEndWhitespace * @property {boolean} stripStartEndWhitespace
* @property {boolean} stripScripts * @property {boolean} stripScripts
* @property {boolean} formatHtml
* @property {start|build} type * @property {start|build} type
*/ */
@@ -54,6 +55,10 @@ export async function readOutput(
type = 'build', type = 'build',
} = {}, } = {},
) { ) {
if (!cli || !cli.config) {
throw new Error(`No valid cli provided to readOutput - you passed a ${typeof cli}: ${cli}`);
}
const outputDir = type === 'build' ? cli.config.outputDir : cli.config.outputDevDir; const outputDir = type === 'build' ? cli.config.outputDir : cli.config.outputDevDir;
let text = await fs.promises.readFile(path.join(outputDir, fileName)); let text = await fs.promises.readFile(path.join(outputDir, fileName));
text = text.toString(); text = text.toString();
@@ -81,6 +86,16 @@ export async function readOutput(
return text; return text;
} }
export function startOutputExist(cli, fileName) {
const outputDir = cli.config.outputDevDir;
return fs.existsSync(path.join(outputDir, fileName));
}
export function buildOutputExist(cli, fileName) {
const outputDir = cli.config.outputDir;
return fs.existsSync(path.join(outputDir, fileName));
}
/** /**
* @param {*} cli * @param {*} cli
* @param {string} fileName * @param {string} fileName
@@ -91,10 +106,21 @@ export async function readStartOutput(cli, fileName, options = {}) {
return readOutput(cli, fileName, options); return readOutput(cli, fileName, options);
} }
/**
* @param {*} cli
* @param {string} fileName
* @param {readOutputOptions} options
*/
export async function readBuildOutput(cli, fileName, options = {}) {
options.type = 'build';
return readOutput(cli, fileName, options);
}
export async function execute(cli, configFileDir) { export async function execute(cli, configFileDir) {
await cli.setup(); await cli.setup();
cli.config.outputDevDir = path.join(configFileDir, '__output-dev'); cli.config.outputDevDir = path.join(configFileDir, '__output-dev');
cli.config.devServer.open = false; cli.config.devServer.open = false;
cli.config.devServer.port = 8080;
cli.config.watch = false; cli.config.watch = false;
cli.config.outputDir = path.join(configFileDir, '__output'); cli.config.outputDir = path.join(configFileDir, '__output');
await cli.run(); await cli.run();
@@ -110,6 +136,15 @@ export async function executeStart(pathToConfig) {
return cli; return cli;
} }
export async function executeBuild(pathToConfig) {
const configFile = path.join(fixtureDir, pathToConfig.split('/').join(path.sep));
const cli = new RocketCli({
argv: ['build', '--config-file', configFile],
});
await execute(cli, path.dirname(configFile));
return cli;
}
export async function executeLint(pathToConfig) { export async function executeLint(pathToConfig) {
const configFile = path.join(fixtureDir, pathToConfig.split('/').join(path.sep)); const configFile = path.join(fixtureDir, pathToConfig.split('/').join(path.sep));
const cli = new RocketCli({ const cli = new RocketCli({

View File

@@ -1,6 +1,12 @@
import chai from 'chai'; import chai from 'chai';
import chalk from 'chalk'; import chalk from 'chalk';
import { executeStart, readOutput, readStartOutput, setFixtureDir } from '@rocket/cli/test-helpers'; import {
executeBuild,
executeStart,
readBuildOutput,
readStartOutput,
setFixtureDir,
} from '@rocket/cli/test-helpers';
const { expect } = chai; const { expect } = chai;
@@ -22,42 +28,49 @@ describe('RocketCli computedConfig', () => {
it('will extract a title from markdown and set first folder as section', async () => { it('will extract a title from markdown and set first folder as section', async () => {
cli = await executeStart('computed-config-fixtures/headlines/rocket.config.js'); cli = await executeStart('computed-config-fixtures/headlines/rocket.config.js');
const indexHtml = await readOutput(cli, 'index.html', { const indexHtml = await readStartOutput(cli, 'index.html');
type: 'start',
});
const [indexTitle, indexSection] = indexHtml.split('\n'); const [indexTitle, indexSection] = indexHtml.split('\n');
expect(indexTitle).to.equal('Root'); expect(indexTitle).to.equal('Root');
expect(indexSection).to.be.undefined; expect(indexSection).to.be.undefined;
const subHtml = await readOutput(cli, 'sub/index.html', { const subHtml = await readStartOutput(cli, 'sub/index.html');
type: 'start',
});
const [subTitle, subSection] = subHtml.split('\n'); const [subTitle, subSection] = subHtml.split('\n');
expect(subTitle).to.equal('Root: Sub'); expect(subTitle).to.equal('Root: Sub');
expect(subSection).to.equal('sub'); expect(subSection).to.equal('sub');
const subSubHtml = await readOutput(cli, 'sub/subsub/index.html', { const subSubHtml = await readStartOutput(cli, 'sub/subsub/index.html');
type: 'start',
});
const [subSubTitle, subSubSection] = subSubHtml.split('\n'); const [subSubTitle, subSubSection] = subSubHtml.split('\n');
expect(subSubTitle).to.equal('Sub: SubSub'); expect(subSubTitle).to.equal('Sub: SubSub');
expect(subSubSection).to.equal('sub'); expect(subSubSection).to.equal('sub');
const sub2Html = await readOutput(cli, 'sub2/index.html', { const sub2Html = await readStartOutput(cli, 'sub2/index.html');
type: 'start',
});
const [sub2Title, sub2Section] = sub2Html.split('\n'); const [sub2Title, sub2Section] = sub2Html.split('\n');
expect(sub2Title).to.equal('Root: Sub2'); expect(sub2Title).to.equal('Root: Sub2');
expect(sub2Section).to.equal('sub2'); expect(sub2Section).to.equal('sub2');
const withDataHtml = await readOutput(cli, 'with-data/index.html', { const withDataHtml = await readStartOutput(cli, 'with-data/index.html');
type: 'start',
});
const [withDataTitle, withDataSection] = withDataHtml.split('\n'); const [withDataTitle, withDataSection] = withDataHtml.split('\n');
expect(withDataTitle).to.equal('Set via data'); expect(withDataTitle).to.equal('Set via data');
expect(withDataSection).be.undefined; expect(withDataSection).be.undefined;
}); });
it('will note create a social media image in "start"', async () => {
cli = await executeStart('computed-config-fixtures/social-images-only-build/rocket.config.js');
const indexHtml = await readStartOutput(cli, 'index.html');
expect(indexHtml).to.equal('');
});
it('will create a social media image in "build"', async () => {
cli = await executeBuild('computed-config-fixtures/social-images-only-build/rocket.config.js');
const indexHtml = await readBuildOutput(cli, 'index.html', {
stripToBody: true,
stripServiceWorker: true,
});
expect(indexHtml).to.equal('/_merged_assets/11ty-img/5893749-1200.png');
});
it('will create a social media image for every page', async () => { it('will create a social media image for every page', async () => {
cli = await executeStart('computed-config-fixtures/social-images/rocket.config.js'); cli = await executeStart('computed-config-fixtures/social-images/rocket.config.js');
@@ -175,9 +188,7 @@ describe('RocketCli computedConfig', () => {
it('can be configured via setupEleventyComputedConfig', async () => { it('can be configured via setupEleventyComputedConfig', async () => {
cli = await executeStart('computed-config-fixtures/setup/addPlugin.rocket.config.js'); cli = await executeStart('computed-config-fixtures/setup/addPlugin.rocket.config.js');
const indexHtml = await readOutput(cli, 'index.html', { const indexHtml = await readStartOutput(cli, 'index.html');
type: 'start',
});
expect(indexHtml).to.equal('test-value'); expect(indexHtml).to.equal('test-value');
}); });

View File

@@ -1,86 +1,25 @@
import chai from 'chai'; import chai from 'chai';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { RocketCli } from '../src/RocketCli.js';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
import chalk from 'chalk'; import chalk from 'chalk';
import {
const __dirname = path.dirname(fileURLToPath(import.meta.url)); executeBuild,
executeLint,
executeStart,
expectThrowsAsync,
readBuildOutput,
readStartOutput,
setFixtureDir,
} from '@rocket/cli/test-helpers';
const { expect } = chai; const { expect } = chai;
/**
* @param {function} method
* @param {string} errorMessage
*/
async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) {
let error = null;
try {
await method();
} catch (err) {
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.equal(errorMessage);
}
}
describe('RocketCli e2e', () => { describe('RocketCli e2e', () => {
let cli; let cli;
async function readOutput(
fileName,
{
stripServiceWorker = false,
stripToBody = false,
stripStartEndWhitespace = true,
type = 'build',
} = {},
) {
const outputDir = type === 'build' ? cli.config.outputDir : cli.config.outputDevDir;
let text = await fs.promises.readFile(path.join(outputDir, fileName));
text = text.toString();
if (stripToBody) {
const bodyOpenTagEnd = text.indexOf('>', text.indexOf('<body') + 1) + 1;
const bodyCloseTagStart = text.indexOf('</body>');
text = text.substring(bodyOpenTagEnd, bodyCloseTagStart);
}
if (stripServiceWorker) {
const scriptOpenTagEnd = text.indexOf('<script inject-service-worker');
const scriptCloseTagStart = text.indexOf('</script>', scriptOpenTagEnd) + 9;
text = text.substring(0, scriptOpenTagEnd) + text.substring(scriptCloseTagStart);
}
if (stripStartEndWhitespace) {
text = text.trim();
}
return text;
}
async function execute() {
await cli.setup();
cli.config.outputDevDir = path.join(__dirname, 'e2e-fixtures', '__output-dev');
cli.config.devServer.open = false;
cli.config.devServer.port = 8080;
cli.config.watch = false;
cli.config.outputDir = path.join(__dirname, 'e2e-fixtures', '__output');
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(() => { before(() => {
// ignore colors in tests as most CIs won't support it // ignore colors in tests as most CIs won't support it
chalk.level = 0; chalk.level = 0;
setFixtureDir(import.meta.url);
}); });
afterEach(async () => { afterEach(async () => {
@@ -90,79 +29,39 @@ describe('RocketCli e2e', () => {
}); });
it('can add a unified plugin via the config', async () => { it('can add a unified plugin via the config', async () => {
cli = new RocketCli({ cli = await executeStart('e2e-fixtures/unified-plugin/rocket.config.js');
argv: [ const indexHtml = await readStartOutput(cli, 'index.html');
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'unified-plugin', 'rocket.config.js'),
],
});
await execute();
const indexHtml = await readOutput('index.html', {
stripServiceWorker: true,
stripToBody: true,
});
expect(indexHtml).to.equal('<p>See a 🐶</p>'); expect(indexHtml).to.equal('<p>See a 🐶</p>');
}); });
describe('eleventy in config', () => { describe('eleventy in config', () => {
// TODO: find out while this has a side effect and breaks other tests it('can modify eleventy via an elventy function in the config', async () => {
it.skip('can modify eleventy via an elventy function in the config', async () => { cli = await executeStart('e2e-fixtures/content/eleventy.rocket.config.js');
cli = new RocketCli({ const indexHtml = await readStartOutput(cli, 'index.html');
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'eleventy.rocket.config.js'),
],
});
await execute();
const indexHtml = await readOutput('index.html', {
type: 'start',
});
expect(indexHtml).to.equal( expect(indexHtml).to.equal(
['# BEFORE #', '<p>Content inside <code>docs/index.md</code></p>'].join('\n'), ['# BEFORE #', '<p>Content inside <code>docs/index.md</code></p>'].join('\n'),
); );
}); });
it('will throw if you try to set options by returning an object', async () => { it('will throw if you try to set options by returning an object', async () => {
cli = new RocketCli({ await expectThrowsAsync(
argv: [ () => executeStart('e2e-fixtures/content/eleventy-return.rocket.config.js'),
'start', {
'--config-file', errorMatch: /Error in your Eleventy config file.*/,
path.join(__dirname, 'e2e-fixtures', 'content', 'eleventy-return.rocket.config.js'), },
], );
});
await expectThrowsAsync(() => execute(), {
errorMatch: /Error in your Eleventy config file.*/,
});
}); });
}); });
describe('setupDevAndBuildPlugins in config', () => { describe('setupDevAndBuildPlugins in config', () => {
it('can add a rollup plugin via setupDevAndBuildPlugins for build command', async () => { it('can add a rollup plugin via setupDevAndBuildPlugins for build command', async () => {
cli = new RocketCli({ cli = await executeBuild('e2e-fixtures/rollup-plugin/devbuild.rocket.config.js');
argv: [ const inlineModule = await readBuildOutput(cli, 'e97af63d.js');
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild.rocket.config.js'),
],
});
await execute();
const inlineModule = await readOutput('e97af63d.js');
expect(inlineModule).to.equal('var a={test:"data"};console.log(a);'); expect(inlineModule).to.equal('var a={test:"data"};console.log(a);');
}); });
it('can add a rollup plugin via setupDevAndBuildPlugins for start command', async () => { it('can add a rollup plugin via setupDevAndBuildPlugins for start command', async () => {
cli = new RocketCli({ cli = await executeStart('e2e-fixtures/rollup-plugin/devbuild.rocket.config.js');
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild.rocket.config.js'),
],
});
await execute();
const response = await fetch('http://localhost:8080/test-data.json'); const response = await fetch('http://localhost:8080/test-data.json');
expect(response.ok).to.be.true; // no server error expect(response.ok).to.be.true; // no server error
@@ -173,88 +72,45 @@ describe('RocketCli e2e', () => {
}); });
it('can add a rollup plugin for dev & build and modify a build only plugin via the config', async () => { it('can add a rollup plugin for dev & build and modify a build only plugin via the config', async () => {
cli = new RocketCli({ cli = await executeBuild('e2e-fixtures/rollup-plugin/devbuild-build.rocket.config.js');
argv: [ const inlineModule = await readBuildOutput(cli, 'e97af63d.js');
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild-build.rocket.config.js'),
],
});
await execute();
const inlineModule = await readOutput('e97af63d.js');
expect(inlineModule).to.equal('var a={test:"data"};console.log(a);'); expect(inlineModule).to.equal('var a={test:"data"};console.log(a);');
const swCode = await readOutput('my-service-worker.js'); const swCode = await readBuildOutput(cli, 'my-service-worker.js');
expect(swCode).to.not.be.undefined; expect(swCode).to.not.be.undefined;
}); });
it('can adjust the inputDir', async () => { it('can adjust the inputDir', async () => {
cli = new RocketCli({ cli = await executeStart('e2e-fixtures/change-input-dir/rocket.config.js');
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'change-input-dir', 'rocket.config.js'),
],
});
await execute();
const indexHtml = await readOutput('index.html', { const indexHtml = await readStartOutput(cli, 'index.html');
type: 'start',
});
expect(indexHtml).to.equal('<p>Markdown in <code>docs/page/index.md</code></p>'); expect(indexHtml).to.equal('<p>Markdown in <code>docs/page/index.md</code></p>');
}); });
it('can access main rocket config values via {{rocketConfig.value}}', async () => { it('can access main rocket config values via {{rocketConfig.value}}', async () => {
cli = new RocketCli({ cli = await executeStart('e2e-fixtures/rocket-config-in-template/rocket.config.js');
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rocket-config-in-template', 'rocket.config.js'),
],
});
await execute();
const indexHtml = await readOutput('index.html', { const indexHtml = await readStartOutput(cli, 'index.html');
type: 'start',
});
expect(indexHtml).to.equal( expect(indexHtml).to.equal(
'<p>You can show Rocket config data like rocketConfig.absoluteBaseUrl = <a href="http://test-domain.com/">http://test-domain.com/</a></p>', '<p>You can show Rocket config data like rocketConfig.absoluteBaseUrl = <a href="http://test-domain.com/">http://test-domain.com/</a></p>',
); );
}); });
it('can add a pathPrefix that will not influence the start command', async () => { it('can add a pathPrefix that will not influence the start command', async () => {
cli = new RocketCli({ cli = await executeStart('e2e-fixtures/content/pathPrefix.rocket.config.js');
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
],
});
await execute();
const linkHtml = await readOutput('link/index.html', { const linkHtml = await readStartOutput(cli, 'link/index.html');
type: 'start',
});
expect(linkHtml).to.equal( expect(linkHtml).to.equal(
['<p><a href="../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'), ['<p><a href="../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'),
); );
const assetHtml = await readOutput('use-assets/index.html', { const assetHtml = await readStartOutput(cli, 'use-assets/index.html');
type: 'start',
});
expect(assetHtml).to.equal('<link rel="stylesheet" href="/_merged_assets/some.css">'); expect(assetHtml).to.equal('<link rel="stylesheet" href="/_merged_assets/some.css">');
}); });
it('can add a pathPrefix that will be used in the build command', async () => { it('can add a pathPrefix that will be used in the build command', async () => {
cli = new RocketCli({ cli = await executeBuild('e2e-fixtures/content/pathPrefix.rocket.config.js');
argv: [
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
],
});
await execute();
const linkHtml = await readOutput('link/index.html', { const linkHtml = await readBuildOutput(cli, 'link/index.html', {
stripServiceWorker: true, stripServiceWorker: true,
stripToBody: true, stripToBody: true,
}); });
@@ -263,7 +119,7 @@ describe('RocketCli e2e', () => {
'\n', '\n',
), ),
); );
const assetHtml = await readOutput('use-assets/index.html', { const assetHtml = await readBuildOutput(cli, 'use-assets/index.html', {
stripServiceWorker: true, stripServiceWorker: true,
}); });
expect(assetHtml).to.equal( expect(assetHtml).to.equal(

View File

@@ -0,0 +1,67 @@
import chai from 'chai';
import chalk from 'chalk';
import {
executeStart,
readStartOutput,
setFixtureDir,
startOutputExist,
} from '@rocket/cli/test-helpers';
const { expect } = chai;
describe('RocketCli use cases', () => {
let cli;
before(() => {
// ignore colors in tests as most CIs won't support it
chalk.level = 0;
setFixtureDir(import.meta.url);
});
afterEach(async () => {
if (cli?.cleanup) {
await cli.cleanup();
}
});
it('supports dynamic imports', async () => {
cli = await executeStart('use-cases/dynamic-imports/rocket.config.js');
expect(startOutputExist(cli, 'sub/assets/myData.js'), 'static files did not get copied').to.be
.true;
const aboutHtml = await readStartOutput(cli, 'about/index.html', { formatHtml: true });
expect(aboutHtml).to.equal(
[
'<p><code>about.md</code></p>',
'<script type="module" src="/about/__mdjs-stories.js" mdjs-setup></script>',
].join('\n'),
);
const subHtml = await readStartOutput(cli, 'sub/index.html', { formatHtml: true });
expect(subHtml).to.equal(
[
'<p><code>sub/index.md</code></p>',
'<script type="module" src="/sub/__mdjs-stories.js" mdjs-setup></script>',
].join('\n'),
);
const subDetailsHtml = await readStartOutput(cli, 'sub/details/index.html', {
formatHtml: true,
});
expect(subDetailsHtml).to.equal(
[
'<p><code>sub/details.md</code></p>',
'<script type="module" src="/sub/details/__mdjs-stories.js" mdjs-setup></script>',
].join('\n'),
);
const indexHtml = await readStartOutput(cli, 'index.html', { formatHtml: true });
expect(indexHtml).to.equal(
[
'<p><code>index.md</code></p>',
'<script type="module" src="/__mdjs-stories.js" mdjs-setup></script>',
].join('\n'),
);
});
});

View File

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

View File

@@ -2,6 +2,9 @@ import { adjustPluginOptions } from 'plugins-manager';
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */ /** @type {Partial<import("../../../types/main").RocketCliOptions>} */
const config = { const config = {
start: {
createSocialMediaImages: true,
},
setupEleventyComputedConfig: [ setupEleventyComputedConfig: [
adjustPluginOptions('socialMediaImage', { adjustPluginOptions('socialMediaImage', {
createSocialImageSvg: async () => { createSocialImageSvg: async () => {

View File

@@ -1 +0,0 @@
module.exports = 'layout.njk';

View File

@@ -0,0 +1 @@
{{ socialMediaImage }}

View File

@@ -1,4 +1,8 @@
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */ /** @type {Partial<import("../../../types/main").RocketCliOptions>} */
const config = {}; const config = {
start: {
createSocialMediaImages: true,
},
};
export default config; export default config;

View File

@@ -6,7 +6,7 @@ import json from '@rollup/plugin-json';
import { addPlugin, adjustPluginOptions } from 'plugins-manager'; import { addPlugin, adjustPluginOptions } from 'plugins-manager';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outputDir = path.join(__dirname, '..', '__output'); const outputDir = path.join(__dirname, '__output');
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */ /** @type {Partial<import("../../../types/main").RocketCliOptions>} */
const config = { const config = {

View File

@@ -29,6 +29,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({ expect(cleanup(config)).to.deep.equal({
command: 'help', command: 'help',
createSocialMediaImages: true,
devServer: {}, devServer: {},
build: {}, build: {},
watch: true, watch: true,
@@ -61,6 +62,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({ expect(cleanup(config)).to.deep.equal({
command: 'help', command: 'help',
createSocialMediaImages: true,
devServer: { devServer: {
more: 'settings', more: 'settings',
}, },
@@ -92,6 +94,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({ expect(cleanup(config)).to.deep.equal({
command: 'help', command: 'help',
createSocialMediaImages: true,
devServer: { devServer: {
more: 'from-file', more: 'from-file',
}, },
@@ -128,6 +131,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({ expect(cleanup(config)).to.deep.equal({
command: 'help', command: 'help',
createSocialMediaImages: true,
devServer: {}, devServer: {},
build: {}, build: {},
watch: true, watch: true,

View File

@@ -0,0 +1 @@
**/*.njk

View File

@@ -0,0 +1 @@
{{ content | safe }}

View File

@@ -0,0 +1,8 @@
`about.md`
```js script
import { myData } from './sub/assets/myData.js';
import('./sub/assets/myData.js');
const name = 'myData';
import(`./sub/assets/${name}.js`);
```

View File

@@ -0,0 +1,8 @@
`index.md`
```js script
import { myData } from './sub/assets/myData.js';
import('./sub/assets/myData.js');
const name = 'myData';
import(`./sub/assets/${name}.js`);
```

View File

@@ -0,0 +1 @@
export const myData = 'The answer to everything is 42';

View File

@@ -0,0 +1,8 @@
`sub/details.md`
```js script
import { myData } from './assets/myData.js';
import('./assets/myData.js');
const name = 'myData';
import(`./assets/${name}.js`);
```

View File

@@ -0,0 +1,8 @@
`sub/index.md`
```js script
import { myData } from './assets/myData.js';
import('./assets/myData.js');
const name = 'myData';
import(`./assets/${name}.js`);
```

View File

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

View File

@@ -13,14 +13,21 @@ export interface RocketPreset {
setupEleventyComputedConfig: function[]; setupEleventyComputedConfig: function[];
} }
interface RocketStartConfig {
createSocialMediaImages?: boolean;
}
export interface RocketCliOptions { export interface RocketCliOptions {
presets: Array<RocketPreset>; presets: Array<RocketPreset>;
pathPrefix?: string; pathPrefix?: string;
inputDir: string; inputDir: string;
outputDir: string; outputDir: string;
emptyOutputDir?: boolen; emptyOutputDir?: boolean;
absoluteBaseUrl?: string; absoluteBaseUrl?: string;
watch: boolean; watch: boolean;
createSocialMediaImages?: boolean;
start?: RocketStartConfig;
// TODO: improve all setup functions // TODO: improve all setup functions
setupUnifiedPlugins?: function[]; setupUnifiedPlugins?: function[];

View File

@@ -1,5 +1,11 @@
# @rocket/core # @rocket/core
## 0.1.2
### Patch Changes
- be0d0b3: fix: add missing main entry to the packages
## 0.1.1 ## 0.1.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rocket/core", "name": "@rocket/core",
"version": "0.1.1", "version": "0.1.2",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -13,6 +13,7 @@
}, },
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)", "author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
"homepage": "https://rocket.modern-web.dev/", "homepage": "https://rocket.modern-web.dev/",
"main": "./dist/title.cjs",
"type": "module", "type": "module",
"exports": { "exports": {
"./title": { "./title": {

View File

@@ -1,5 +1,11 @@
# @rocket/drawer # @rocket/drawer
## 0.1.3
### Patch Changes
- 0b64116: Update @lion dependencies
## 0.1.2 ## 0.1.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rocket/drawer", "name": "@rocket/drawer",
"version": "0.1.2", "version": "0.1.3",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -33,7 +33,7 @@
"testing" "testing"
], ],
"dependencies": { "dependencies": {
"@lion/overlays": "^0.23.2", "@lion/overlays": "^0.26.1",
"lit-element": "^2.4.0" "lit-element": "^2.4.0"
}, },
"types": "dist-types/index.d.ts" "types": "dist-types/index.d.ts"

View File

@@ -18,6 +18,7 @@ export class RocketDrawer extends OverlayMixin(LitElement) {
return { return {
useOverlay: { type: Boolean, reflect: true }, useOverlay: { type: Boolean, reflect: true },
useOverlayMediaQuery: { type: String }, useOverlayMediaQuery: { type: String },
mediaMatcher: { type: Object },
}; };
} }
@@ -89,6 +90,20 @@ export class RocketDrawer extends OverlayMixin(LitElement) {
} }
} }
} }
if (changedProperties.has('useOverlayMediaQuery')) {
this.mediaMatcher.removeEventListener('change', this.onMatchMedia);
this.mediaMatcher = window.matchMedia(this.useOverlayMediaQuery);
this.mediaMatcher.addEventListener('change', this.onMatchMedia);
this.useOverlay = !!this.mediaMatcher.matches;
}
}
/**
* @param { MediaQueryListEvent } query
*/
onMatchMedia(query) {
this.useOverlay = !!query.matches;
} }
_setupOpenCloseListeners() { _setupOpenCloseListeners() {
@@ -118,11 +133,15 @@ export class RocketDrawer extends OverlayMixin(LitElement) {
this.__toggle = this.__toggle.bind(this); this.__toggle = this.__toggle.bind(this);
this.onMatchMedia = this.onMatchMedia.bind(this);
this.onGestureStart = this.onGestureStart.bind(this); this.onGestureStart = this.onGestureStart.bind(this);
this.onGestureMove = this.onGestureMove.bind(this); this.onGestureMove = this.onGestureMove.bind(this);
this.onGestureEnd = this.onGestureEnd.bind(this); this.onGestureEnd = this.onGestureEnd.bind(this);
this.updateFromTouch = this.updateFromTouch.bind(this); this.updateFromTouch = this.updateFromTouch.bind(this);
this.mediaMatcher = window.matchMedia(this.useOverlayMediaQuery);
this.mediaMatcher.addEventListener('change', this.onMatchMedia);
this._startX = 0; this._startX = 0;
this._currentX = 0; this._currentX = 0;
this._velocity = 0; this._velocity = 0;
@@ -133,10 +152,7 @@ export class RocketDrawer extends OverlayMixin(LitElement) {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.useOverlay = !!window.matchMedia(this.useOverlayMediaQuery).matches; this.useOverlay = !!this.mediaMatcher.matches;
window.matchMedia(this.useOverlayMediaQuery).addListener(query => {
this.useOverlay = !!query.matches;
});
} }
render() { render() {

View File

@@ -1,5 +1,24 @@
# @rocket/eleventy-plugin-mdjs-unified # @rocket/eleventy-plugin-mdjs-unified
## 0.4.0
### Minor Changes
- 2267e72: Write the mdjs JavaScript code to a file and load it from there instead of an inline script
### Patch Changes
- Updated dependencies [a8e66d8]
- Updated dependencies [fe6a929]
- Updated dependencies [a8e66d8]
- @mdjs/core@0.7.0
## 0.3.1
### Patch Changes
- f44a0f4: Rewrite dynamic imports with "`"
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes

View File

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

View File

@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
const path = require('path');
const fs = require('fs');
const { mdjsProcess } = require('@mdjs/core'); const { mdjsProcess } = require('@mdjs/core');
const visit = require('unist-util-visit'); const visit = require('unist-util-visit');
const { init, parse } = require('es-module-lexer'); const { init, parse } = require('es-module-lexer');
@@ -7,7 +9,7 @@ const { init, parse } = require('es-module-lexer');
const { parseTitle } = require('@rocket/core/title'); const { parseTitle } = require('@rocket/core/title');
/** @typedef {import('@mdjs/core').MdjsProcessPlugin} MdjsProcessPlugin */ /** @typedef {import('@mdjs/core').MdjsProcessPlugin} MdjsProcessPlugin */
/** @typedef {import('../types/code').EleventPluginMdjsUnified} EleventPluginMdjsUnified */ /** @typedef {import('../types/code').EleventyPluginMdjsUnified} EleventyPluginMdjsUnified */
/** @typedef {import('../types/code').NodeChildren} NodeChildren */ /** @typedef {import('../types/code').NodeChildren} NodeChildren */
/** @typedef {import('../types/code').NodeElement} NodeElement */ /** @typedef {import('../types/code').NodeElement} NodeElement */
/** @typedef {import('unist').Node} Node */ /** @typedef {import('unist').Node} Node */
@@ -72,10 +74,14 @@ async function processImports(source, inputPath) {
newSource += '.' + importSrc; newSource += '.' + importSrc;
} else if (importSrc.startsWith("'./")) { } else if (importSrc.startsWith("'./")) {
newSource += "'." + importSrc.substring(1); newSource += "'." + importSrc.substring(1);
} else if (importSrc.startsWith('`./')) {
newSource += '`.' + importSrc.substring(1);
} else if (importSrc.startsWith('../')) { } else if (importSrc.startsWith('../')) {
newSource += '../' + importSrc; newSource += '../' + importSrc;
} else if (importSrc.startsWith("'../")) { } else if (importSrc.startsWith("'../")) {
newSource += "'../" + importSrc.substring(1); newSource += "'../" + importSrc.substring(1);
} else if (importSrc.startsWith('`../')) {
newSource += '`../' + importSrc.substring(1);
} else { } else {
newSource += importSrc; newSource += importSrc;
} }
@@ -89,7 +95,7 @@ async function processImports(source, inputPath) {
} }
/** /**
* @param {EleventPluginMdjsUnified} pluginOptions * @param {EleventyPluginMdjsUnified} pluginOptions
*/ */
function eleventyUnified(pluginOptions) { function eleventyUnified(pluginOptions) {
/** /**
@@ -135,10 +141,12 @@ function eleventyUnified(pluginOptions) {
let code = result.html; let code = result.html;
if (result.jsCode) { if (result.jsCode) {
const newFolder = path.dirname(eleventySettings.page.outputPath);
const newName = path.join(newFolder, '__mdjs-stories.js');
await fs.promises.mkdir(newFolder, { recursive: true });
await fs.promises.writeFile(newName, result.jsCode, 'utf8');
code += ` code += `
<script type="module"> <script type="module" src="${eleventySettings.page.url}__mdjs-stories.js" mdjs-setup></script>
${result.jsCode}
</script>
`; `;
} }
return code; return code;
@@ -153,15 +161,15 @@ function eleventyUnified(pluginOptions) {
/** /**
* @param {*} eleventyConfig * @param {*} eleventyConfig
* @param {EleventPluginMdjsUnified} [pluginOptions] * @param {EleventyPluginMdjsUnified} [pluginOptions]
*/ */
function configFunction(eleventyConfig, pluginOptions = {}) { function configFunction(eleventyConfig, pluginOptions = {}) {
eleventyConfig.setLibrary('md', eleventyUnified(pluginOptions)); eleventyConfig.setLibrary('md', eleventyUnified(pluginOptions));
} }
const eleventPluginMdjsUnified = { const EleventyPluginMdjsUnified = {
initArguments: {}, initArguments: {},
configFunction, configFunction,
}; };
module.exports = eleventPluginMdjsUnified; module.exports = EleventyPluginMdjsUnified;

View File

@@ -53,13 +53,12 @@ describe('eleventy-plugin-mdjs-unified', () => {
it('renders markdown with javascript', async () => { it('renders markdown with javascript', async () => {
const files = await renderEleventy('./test-node/fixtures/mdjs'); const files = await renderEleventy('./test-node/fixtures/mdjs');
expect(files).to.deep.equal([
{ expect(files.length).to.equal(1);
html: expect(files[0].name).to.equal('first/index.html');
'<h1 id="first"><a aria-hidden="true" tabindex="-1" href="#first"><span class="icon icon-link"></span></a>First</h1>\n<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token string">\'bar\'</span><span class="token punctuation">;</span>\n<span class="token keyword module">import</span> <span class="token punctuation">{</span> html <span class="token punctuation">}</span> <span class="token keyword module">from</span> <span class="token string">\'lit-html\'</span><span class="token punctuation">;</span>\n</code></pre>\n<mdjs-story mdjs-story-name="inline"></mdjs-story>\n<mdjs-preview mdjs-story-name="withBorder"></mdjs-preview>\n <script type="module">\n \nexport const inline = () => html` <p>main</p> `;\nexport const withBorder = () => html` <p>main</p> `;\nconst rootNode = document;\nconst stories = [{ key: \'inline\', story: inline, code: `<pre class="language-js"><code class="language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">inline</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">\\`</span><span class="token html language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>p</span><span class="token punctuation">></span></span>main<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>p</span><span class="token punctuation">></span></span> </span><span class="token template-punctuation string">\\`</span></span><span class="token punctuation">;</span>\n</code></pre>` }, { key: \'withBorder\', story: withBorder, code: `<pre class="language-js"><code class="language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">withBorder</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">\\`</span><span class="token html language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>p</span><span class="token punctuation">></span></span>main<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>p</span><span class="token punctuation">></span></span> </span><span class="token template-punctuation string">\\`</span></span><span class="token punctuation">;</span>\n</code></pre>` }];\nfor (const story of stories) {\n const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);\n storyEl.codeHasHtml = true;\n storyEl.story = story.story;\n storyEl.code = story.code;\n};\nif (!customElements.get(\'mdjs-preview\')) { import(\'@mdjs/mdjs-preview/mdjs-preview.js\'); }\nif (!customElements.get(\'mdjs-story\')) { import(\'@mdjs/mdjs-story/mdjs-story.js\'); }\n </script>\n ',
name: 'first/index.html', expect(files[0].html).to.include('<script type="module"');
}, expect(files[0].html).to.include('mdjs-setup>');
]);
}); });
it('rewrites relative import pathes', async () => { it('rewrites relative import pathes', async () => {
@@ -67,7 +66,7 @@ describe('eleventy-plugin-mdjs-unified', () => {
expect(files).to.deep.equal([ expect(files).to.deep.equal([
{ {
html: html:
"<p>first</p>\n <script type=\"module\">\n import '../import-me.js';\nimport('../import-me-too.js');\n </script>\n ", '<p>first</p>\n <script type="module" src="/first/__mdjs-stories.js" mdjs-setup></script>\n ',
name: 'first/index.html', name: 'first/index.html',
}, },
]); ]);
@@ -78,7 +77,7 @@ describe('eleventy-plugin-mdjs-unified', () => {
expect(files).to.deep.equal([ expect(files).to.deep.equal([
{ {
html: html:
"<p>first</p>\n <script type=\"module\">\n import '../../import-me.js';\nimport('../../import-me-too.js');\n </script>\n ", '<p>first</p>\n <script type="module" src="/subpage/first/__mdjs-stories.js" mdjs-setup></script>\n ',
name: 'subpage/first/index.html', name: 'subpage/first/index.html',
}, },
]); ]);
@@ -89,7 +88,7 @@ describe('eleventy-plugin-mdjs-unified', () => {
expect(files).to.deep.equal([ expect(files).to.deep.equal([
{ {
html: html:
"<p>index</p>\n <script type=\"module\">\n import './import-me.js';\nimport('./import-me-too.js');\n </script>\n ", '<p>index</p>\n <script type="module" src="/__mdjs-stories.js" mdjs-setup></script>\n ',
name: 'index.html', name: 'index.html',
}, },
]); ]);

View File

@@ -3,7 +3,7 @@ import { Node } from 'unist';
export const setupUnifiedPluginsFn: (plugins: MdjsProcessPlugin[]) => MdjsProcessPlugin[]; export const setupUnifiedPluginsFn: (plugins: MdjsProcessPlugin[]) => MdjsProcessPlugin[];
export interface EleventPluginMdjsUnified { export interface EleventyPluginMdjsUnified {
setupUnifiedPlugins?: setupUnifiedPluginsFn[]; setupUnifiedPlugins?: setupUnifiedPluginsFn[];
} }

View File

@@ -1,4 +1,5 @@
const RocketNav = require('./eleventy-rocket-nav'); const RocketNav = require('./eleventy-rocket-nav');
const { addPageAnchors } = require('./src/addPageAnchors.js');
// export the configuration function for plugin // export the configuration function for plugin
module.exports = function (eleventyConfig) { module.exports = function (eleventyConfig) {
@@ -8,6 +9,10 @@ module.exports = function (eleventyConfig) {
eleventyConfig.addNunjucksFilter('rocketNavToHtml', function (pages, options) { eleventyConfig.addNunjucksFilter('rocketNavToHtml', function (pages, options) {
return RocketNav.toHtml.call(eleventyConfig, pages, options); return RocketNav.toHtml.call(eleventyConfig, pages, options);
}); });
eleventyConfig.addTransform('rocket-nav-add-page-anchors', async function (content) {
const newContent = await addPageAnchors(content);
return newContent;
});
}; };
module.exports.navigation = { module.exports.navigation = {

View File

@@ -1,5 +1,15 @@
# @rocket/eleventy-rocket-nav # @rocket/eleventy-rocket-nav
## 0.3.0
### Minor Changes
- ef8ebb0: To support dynamically created content to be part of the anchor navigation of the page we now analyze the final html output instead of `entry.templateContent`.
BREAKING CHANGE:
- only add anchors for the currently active pages (before it added anchor for every page)
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -94,21 +94,8 @@ function findNavigationEntries(nodes = [], key = '') {
entry.title = entry.key; entry.title = entry.key;
} }
if (entry.key) { if (entry.key) {
if (!headingsCache.has(entry.templateContent)) {
headingsCache.set(entry.templateContent, getHeadingsOfHtml(entry.templateContent));
}
const headings = /** @type {Heading[]} */ (headingsCache.get(entry.templateContent));
const anchors = headings.map(heading => ({
key: heading.text + Math.random(),
parent: entry.key,
url: `${entry.url}#${heading.id}`,
pluginType: 'eleventy-navigation',
parentKey: entry.key,
title: heading.text,
anchor: true,
}));
// @ts-ignore // @ts-ignore
entry.children = [...anchors, ...findNavigationEntries(nodes, entry.key)]; entry.children = findNavigationEntries(nodes, entry.key);
} }
return entry; return entry;
}); });
@@ -227,43 +214,36 @@ function navigationToHtml(pages, _options = {}) {
}>${pages }>${pages
.map(entry => { .map(entry => {
const liClass = []; const liClass = [];
const aClass = [];
if (options.listItemClass) { if (options.listItemClass) {
liClass.push(options.listItemClass); liClass.push(options.listItemClass);
} }
if (options.anchorClass) { if (options.activeKey === entry.key && options.activeListItemClass) {
aClass.push(options.anchorClass); liClass.push(options.activeListItemClass);
}
if (options.activeKey === entry.key) {
if (options.activeListItemClass) {
liClass.push(options.activeListItemClass);
}
if (options.activeAnchorClass) {
aClass.push(options.activeAnchorClass);
}
} }
if (options.activeTreeListClass && activePages && activePages.includes(entry.key)) { if (options.activeTreeListClass && activePages && activePages.includes(entry.key)) {
liClass.push(options.activeTreeListClass); liClass.push(options.activeTreeListClass);
} }
if (options.activeAnchorListClass && activePages && activePages.includes(entry.key)) {
aClass.push(options.activeAnchorListClass);
}
if (options.listItemHasChildrenClass && entry.children && entry.children.length) { if (options.listItemHasChildrenClass && entry.children && entry.children.length) {
liClass.push(options.listItemHasChildrenClass); liClass.push(options.listItemHasChildrenClass);
} }
if (entry.anchor) { const output = [];
liClass.push('anchor'); output.push(
aClass.push('anchor'); `<${options.listItemElement}${liClass.length ? ` class="${liClass.join(' ')}"` : ''}>`,
);
output.push(`<a href="${urlFilter(entry.url)}">${entry.title}</a>`);
if (options.showExcerpt && entry.excerpt) {
output.push(`: ${entry.excerpt}`);
} }
if (options.activeKey === entry.key && options.activeListItemClass) {
output.push('<!-- ADD PAGE ANCHORS -->');
}
if (entry.children) {
output.push(navigationToHtml(entry.children, options));
}
output.push(`</${options.listItemElement}>`);
return `<${options.listItemElement}${ return output.join('\n');
liClass.length ? ` class="${liClass.join(' ')}"` : ''
}><a href="${urlFilter(entry.url)}"${
aClass.length ? ` class="${aClass.join(' ')}"` : ''
}>${entry.title}</a>${options.showExcerpt && entry.excerpt ? `: ${entry.excerpt}` : ''}${
entry.children ? navigationToHtml(entry.children, options) : ''
}</${options.listItemElement}>`;
}) })
.join('\n')}</${options.listElement}>` .join('\n')}</${options.listElement}>`
: ''; : '';

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rocket/eleventy-rocket-nav", "name": "@rocket/eleventy-rocket-nav",
"version": "0.2.1", "version": "0.3.0",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -13,7 +13,7 @@
}, },
"main": ".eleventy.js", "main": ".eleventy.js",
"scripts": { "scripts": {
"test": "mocha test-node/**/*.test.js test-node/*.test.js", "test": "mocha test-node/**/*.test.js test-node/*.test.js --timeout 5000",
"test:watch": "mocha test-node/**/*.test.js test-node/*.test.js --watch" "test:watch": "mocha test-node/**/*.test.js test-node/*.test.js --watch"
}, },
"files": [ "files": [

View File

@@ -0,0 +1,136 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
const fs = require('fs');
const { SaxEventType, SAXParser } = require('sax-wasm');
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
const saxWasmBuffer = fs.readFileSync(saxPath);
/** @typedef {import('../types').Heading} Heading */
/** @typedef {import('sax-wasm').Text} Text */
/** @typedef {import('sax-wasm').Tag} Tag */
/** @typedef {import('sax-wasm').Position} Position */
// Instantiate
const parser = new SAXParser(
SaxEventType.CloseTag | SaxEventType.Comment,
{ highWaterMark: 256 * 1024 }, // 256k chunks
);
/**
* @param {object} options
* @param {string} options.content
* @param {Position} options.start
* @param {Position} options.end
* @param {string} options.insert
*/
function removeBetween({ content, start, end, insert = '' }) {
const lines = content.split('\n');
const i = start.line;
const line = lines[i];
const upToChange = line.slice(0, start.character - 1);
const afterChange = line.slice(end.character + 2);
lines[i] = `${upToChange}${insert}${afterChange}`;
return lines.join('\n');
}
/**
* @param {Tag} data
* @param {string} name
*/
function getAttribute(data, name) {
if (data.attributes) {
const { attributes } = data;
const foundIndex = attributes.findIndex(entry => entry.name.value === name);
if (foundIndex !== -1) {
return attributes[foundIndex].value.value;
}
}
return null;
}
/**
* @param {Tag} data
*/
function getText(data) {
if (data.textNodes) {
return data.textNodes.map(textNode => textNode.value).join('');
}
return null;
}
/**
* @param {string} html
*/
function getHeadingsOfHtml(html) {
/** @type {Heading[]} */
const headings = [];
/** @type {Text} */
let insertPoint;
parser.eventHandler = (ev, _data) => {
if (ev === SaxEventType.Comment) {
const data = /** @type {Text} */ (/** @type {any} */ (_data));
// NOTE: we NEED to access data internal value so sax-wasm does not reuse it's value
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const tmp = data.start.line + data.end.line;
if (data.value.trim() === 'ADD PAGE ANCHORS' || data.value.trim() === '-->ADD PAGE ANCHORS') {
insertPoint = data;
}
}
if (ev === SaxEventType.CloseTag) {
const data = /** @type {Tag} */ (/** @type {any} */ (_data));
if (data.name === 'h2') {
const id = getAttribute(data, 'id');
const text = getText(data);
if (id && text) {
headings.push({ text, id });
}
}
}
};
parser.write(Buffer.from(html, 'utf8'));
parser.end();
// @ts-ignore
return { headings, insertPoint };
}
let isSetup = false;
/**
* @param {string} content
*/
async function addPageAnchors(content) {
if (!isSetup) {
await parser.prepareWasm(saxWasmBuffer);
isSetup = true;
}
const { headings, insertPoint } = getHeadingsOfHtml(content);
const pageAnchorsHtml = [];
if (headings.length > 0) {
pageAnchorsHtml.push('<ul>');
for (const heading of headings) {
pageAnchorsHtml.push(' <li class="menu-item anchor">');
pageAnchorsHtml.push(` <a href="#${heading.id}" class="anchor">${heading.text}</a>`);
pageAnchorsHtml.push(' </li>');
}
pageAnchorsHtml.push('</ul>');
}
if (insertPoint) {
return removeBetween({
content,
start: insertPoint.start,
end: insertPoint.end,
insert: pageAnchorsHtml.join('\n'),
});
}
return content;
}
module.exports = {
addPageAnchors,
};

View File

@@ -0,0 +1,32 @@
const { expect } = require('chai');
const prettier = require('prettier');
const { addPageAnchors } = require('../src/addPageAnchors.js');
const format = code => prettier.format(code, { parser: 'html' }).trim();
describe('addPageAnchors', () => {
it('finds and adds anchors for each h2 as an unordered list', async () => {
const input = [
'<body>',
' <!-- ADD PAGE ANCHORS -->',
' <div id="content">',
' <h2 id="first">👉 First Headline</h2>',
' </div>',
'</body>',
].join('\n');
const expected = [
'<body>',
' <ul>',
' <li class="menu-item anchor">',
' <a href="#first" class="anchor">👉 First Headline</a>',
' </li>',
' </ul>',
' <div id="content">',
' <h2 id="first">👉 First Headline</h2>',
' </div>',
'</body>',
].join('\n');
const result = await addPageAnchors(input);
expect(format(result)).to.deep.equal(expected);
});
});

View File

@@ -0,0 +1,5 @@
const eleventyNavigationPlugin = require('@rocket/eleventy-rocket-nav');
module.exports = function (eleventyConfig) {
eleventyConfig.addPlugin(eleventyNavigationPlugin);
};

View File

@@ -0,0 +1,9 @@
<body>
{{ collections.all | rocketNav | rocketNavToHtml({
listItemClass: "menu-item",
activeListItemClass: "current",
activeKey: eleventyNavigation.key
}) | safe }}
{{ content | safe }}
</body>

View File

@@ -0,0 +1,9 @@
---
title: Bats
eleventyNavigation:
key: Bats
parent: Mammals
order: 2
---
🦇 can fly.

View File

@@ -0,0 +1,12 @@
---
title: Humans
eleventyNavigation:
key: Humans
parent: Mammals
order: 1
---
<h2 id="anatomy">Anatomy</h2>
<p>Has arms.</p>
<h2 id="age">📖 Age</h2>
<p>Up to 130 years.</p>

View File

@@ -0,0 +1,7 @@
---
title: Mammals
eleventyNavigation:
key: Mammals
---
Mammals need air.

View File

@@ -0,0 +1,96 @@
const path = require('path');
const fs = require('fs-extra');
const { expect } = require('chai');
const Eleventy = require('@11ty/eleventy');
const TemplateConfig = require('@11ty/eleventy/src/TemplateConfig');
const prettier = require('prettier');
async function execute(fixtureDir) {
const relPath = path.relative(process.cwd(), __dirname);
const relativeInputPath = path.join(relPath, fixtureDir.split('/').join(path.sep));
const relativeOutputPath = path.join(relPath, 'fixtures', '__output');
const relativeConfigPath = path.join(relativeInputPath, '.eleventy.js');
await fs.emptyDir(relativeOutputPath);
const elev = new Eleventy(relativeInputPath, relativeOutputPath);
const config = new TemplateConfig(null, relativeConfigPath);
elev.config = config.getConfig();
elev.setConfigPathOverride(relativeConfigPath);
elev.resetConfig();
await elev.init();
await elev.write();
return {
readOutput: async readPath => {
const relativeReadPath = path.join(relativeOutputPath, readPath);
let text = await fs.promises.readFile(relativeReadPath);
text = text.toString();
text = prettier.format(text, { parser: 'html', printWidth: 100 });
return text.trim();
},
};
}
describe('eleventy-rocket-nav', () => {
it('renders a menu with anchors for h2 content', async () => {
const { readOutput } = await execute('fixtures/three-pages');
const bats = await readOutput('bats/index.html');
expect(bats).to.deep.equal(
[
'<body>',
' <ul>',
' <li class="menu-item active">',
' <a href="/mammals/">Mammals</a>',
' <ul>',
' <li class="menu-item">',
' <a href="/humans/">Humans</a>',
' </li>',
' <li class="menu-item current">',
' <a href="/bats/">Bats</a>',
' </li>',
' </ul>',
' </li>',
' </ul>',
'',
' <p>🦇 can fly.</p>',
'</body>',
].join('\n'),
);
const humans = await readOutput('humans/index.html');
expect(humans).to.deep.equal(
[
'<body>',
' <ul>',
' <li class="menu-item active">',
' <a href="/mammals/">Mammals</a>',
' <ul>',
' <li class="menu-item current">',
' <a href="/humans/">Humans</a>',
' <ul>',
' <li class="menu-item anchor">',
' <a href="#anatomy" class="anchor">Anatomy</a>',
' </li>',
' <li class="menu-item anchor">',
' <a href="#age" class="anchor">📖 Age</a>',
' </li>',
' </ul>',
' </li>',
' <li class="menu-item">',
' <a href="/bats/">Bats</a>',
' </li>',
' </ul>',
' </li>',
' </ul>',
'',
' <h2 id="anatomy">Anatomy</h2>',
' <p>Has arms.</p>',
' <h2 id="age">📖 Age</h2>',
' <p>Up to 130 years.</p>',
'</body>',
].join('\n'),
);
});
});

View File

@@ -35,7 +35,7 @@
"preset" "preset"
], ],
"dependencies": { "dependencies": {
"@rocket/drawer": "^0.1.2", "@rocket/drawer": "^0.1.3",
"@rocket/navigation": "^0.2.0" "@rocket/navigation": "^0.2.1"
} }
} }

View File

@@ -1,5 +1,20 @@
# Change Log # Change Log
## 0.7.0
### Minor Changes
- a8e66d8: Extract building of the JavaScript setup code into a unified plugin called mdjsSetupCode
- fe6a929: For the story preview keep the original code block around to get code highlighting from the main document. This enables styling and reduces the code complexity.
### Patch Changes
- a8e66d8: You can provide a highlightCode function to the mdjsSetupCode unified plugin
- Updated dependencies [edb1abf]
- Updated dependencies [604a80e]
- @mdjs/mdjs-preview@0.4.0
- @mdjs/mdjs-story@0.2.0
## 0.6.2 ## 0.6.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mdjs/core", "name": "@mdjs/core",
"version": "0.6.2", "version": "0.7.0",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@@ -43,12 +43,12 @@
"remark" "remark"
], ],
"dependencies": { "dependencies": {
"@mdjs/mdjs-preview": "^0.3.0", "@mdjs/mdjs-preview": "^0.4.0",
"@mdjs/mdjs-story": "^0.1.0", "@mdjs/mdjs-story": "^0.2.0",
"@types/unist": "^2.0.3", "@types/unist": "^2.0.3",
"es-module-lexer": "^0.3.26", "es-module-lexer": "^0.3.26",
"github-markdown-css": "^4.0.0", "github-markdown-css": "^4.0.0",
"plugins-manager": "^0.2.0", "plugins-manager": "^0.2.1",
"rehype-autolink-headings": "^5.0.1", "rehype-autolink-headings": "^5.0.1",
"rehype-prism-template": "^0.4.1", "rehype-prism-template": "^0.4.1",
"rehype-raw": "^5.0.0", "rehype-raw": "^5.0.0",

View File

@@ -18,6 +18,7 @@ const { executeSetupFunctions } = require('plugins-manager');
const { mdjsParse } = require('./mdjsParse.js'); const { mdjsParse } = require('./mdjsParse.js');
const { mdjsStoryParse } = require('./mdjsStoryParse.js'); const { mdjsStoryParse } = require('./mdjsStoryParse.js');
const { mdjsSetupCode } = require('./mdjsSetupCode.js');
/** @type {MdjsProcessPlugin[]} */ /** @type {MdjsProcessPlugin[]} */
const defaultMetaPlugins = [ const defaultMetaPlugins = [
@@ -25,6 +26,7 @@ const defaultMetaPlugins = [
{ name: 'gfm', plugin: gfm }, { name: 'gfm', plugin: gfm },
{ name: 'mdjsParse', plugin: mdjsParse }, { name: 'mdjsParse', plugin: mdjsParse },
{ name: 'mdjsStoryParse', plugin: mdjsStoryParse }, { name: 'mdjsStoryParse', plugin: mdjsStoryParse },
{ name: 'mdjsSetupCode', plugin: mdjsSetupCode },
// @ts-ignore // @ts-ignore
{ name: 'remark2rehype', plugin: remark2rehype, options: { allowDangerousHtml: true } }, { name: 'remark2rehype', plugin: remark2rehype, options: { allowDangerousHtml: true } },
// @ts-ignore // @ts-ignore
@@ -50,29 +52,15 @@ const defaultMetaPlugins = [
* @param {function[]} [options.setupUnifiedPlugins] * @param {function[]} [options.setupUnifiedPlugins]
* @param {MdjsProcessPlugin[]} [options.plugins] deprecated option use setupUnifiedPlugins instead * @param {MdjsProcessPlugin[]} [options.plugins] deprecated option use setupUnifiedPlugins instead
*/ */
async function mdjsProcess( async function mdjsProcess(mdjs, { setupUnifiedPlugins = [] } = {}) {
mdjs,
{ rootNodeQueryCode = 'document', setupUnifiedPlugins = [] } = {},
) {
const parser = unified(); const parser = unified();
const metaPlugins = executeSetupFunctions(setupUnifiedPlugins, defaultMetaPlugins); const metaPlugins = executeSetupFunctions(setupUnifiedPlugins, defaultMetaPlugins);
// @ts-ignore /**
for (const pluginObj of metaPlugins) { * @param {string} code
parser.use(pluginObj.plugin, pluginObj.options); */
} async function highlightCode(code) {
/** @type {unknown} */
const parseResult = await parser.process(mdjs);
const result = /** @type {ParseResult} */ (parseResult);
const { stories, jsCode } = result.data;
let fullJsCode = jsCode;
if (stories && stories.length > 0) {
const storiesCode = stories.map(story => story.code).join('\n');
// @ts-ignore // @ts-ignore
const codePlugins = metaPlugins.filter(pluginObj => const codePlugins = metaPlugins.filter(pluginObj =>
['markdown', 'remark2rehype', 'rehypePrism', 'htmlStringify'].includes(pluginObj.name), ['markdown', 'remark2rehype', 'rehypePrism', 'htmlStringify'].includes(pluginObj.name),
@@ -82,46 +70,30 @@ async function mdjsProcess(
for (const pluginObj of codePlugins) { for (const pluginObj of codePlugins) {
codeParser.use(pluginObj.plugin, pluginObj.options); codeParser.use(pluginObj.plugin, pluginObj.options);
} }
const codeResult = await codeParser.process(code);
const invokeStoriesCode = []; return codeResult.contents;
for (const story of stories) {
let code = '';
switch (story.type) {
case 'html':
code = `\`\`\`html\n${story.code.split('`')[1]}\n\`\`\``;
break;
case 'js':
code = `\`\`\`js\n${story.code}\n\`\`\``;
break;
default:
break;
}
const codeResult = await codeParser.process(code);
const highlightedCode = /** @type {string} */ (codeResult.contents)
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
invokeStoriesCode.push(
`{ key: '${story.key}', story: ${story.key}, code: \`${highlightedCode}\` }`,
);
}
fullJsCode = [
jsCode,
storiesCode,
`const rootNode = ${rootNodeQueryCode};`,
`const stories = [${invokeStoriesCode.join(', ')}];`,
`for (const story of stories) {`,
// eslint-disable-next-line no-template-curly-in-string
' const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);',
` storyEl.codeHasHtml = true;`,
` storyEl.story = story.story;`,
` storyEl.code = story.code;`,
`};`,
`if (!customElements.get('mdjs-preview')) { import('@mdjs/mdjs-preview/mdjs-preview.js'); }`,
`if (!customElements.get('mdjs-story')) { import('@mdjs/mdjs-story/mdjs-story.js'); }`,
].join('\n');
} }
return { stories, jsCode: fullJsCode, html: result.contents };
// @ts-ignore
for (const pluginObj of metaPlugins) {
if (pluginObj.name === 'mdjsSetupCode') {
if (pluginObj.options && !pluginObj.options.highlightCode) {
pluginObj.options.highlightCode = highlightCode;
}
if (!pluginObj.options) {
pluginObj.options = { highlightCode };
}
}
parser.use(pluginObj.plugin, pluginObj.options);
}
/** @type {unknown} */
const parseResult = await parser.process(mdjs);
const result = /** @type {ParseResult} */ (parseResult);
const { stories, setupJsCode } = result.data;
return { stories, jsCode: setupJsCode, html: result.contents };
} }
module.exports = { module.exports = {

View File

@@ -0,0 +1,57 @@
/** @typedef {import('vfile').VFileOptions} VFileOptions */
/** @typedef {import('unist').Node} Node */
/** @typedef {import('@mdjs/core/types/code').Story} Story */
function mdjsSetupCode({ rootNodeQueryCode = 'document', simulationSettings = {} } = {}) {
/**
* @param {Node} tree
* @param {VFileOptions} file
*/
async function transformer(tree, file) {
const { stories, jsCode } = file.data;
file.data.setupJsCode = jsCode;
if (stories && stories.length > 0) {
const storiesCode = stories.map(/** @param {Story} story */ story => story.code).join('\n');
const invokeStoriesCode = [];
for (const story of stories) {
invokeStoriesCode.push(`{ key: '${story.key}', story: ${story.key} }`);
}
file.data.setupJsCode = [
'/** script code **/',
jsCode,
'/** stories code **/',
storiesCode,
'/** stories setup code **/',
`const rootNode = ${rootNodeQueryCode};`,
`const stories = [${invokeStoriesCode.join(', ')}];`,
'let needsMdjsElements = false;',
`for (const story of stories) {`,
// eslint-disable-next-line no-template-curly-in-string
' const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);',
' if (storyEl) {',
` storyEl.story = story.story;`,
` storyEl.key = story.key;`,
` needsMdjsElements = true;`,
` Object.assign(storyEl, ${JSON.stringify(simulationSettings)});`,
' }',
`};`,
'if (needsMdjsElements) {',
` if (!customElements.get('mdjs-preview')) { import('@mdjs/mdjs-preview/define'); }`,
` if (!customElements.get('mdjs-story')) { import('@mdjs/mdjs-story/define'); }`,
'}',
].join('\n');
}
return tree;
}
return transformer;
}
module.exports = {
mdjsSetupCode,
};

View File

@@ -3,6 +3,7 @@
/** @typedef {import('@mdjs/core/types/code').StoryTypes} StoryTypes */ /** @typedef {import('@mdjs/core/types/code').StoryTypes} StoryTypes */
/** @typedef {(name: string) => string} TagFunction */ /** @typedef {(name: string) => string} TagFunction */
/** @typedef {import('unist').Node} UnistNode */ /** @typedef {import('unist').Node} UnistNode */
/** @typedef {import('unist').Parent} UnistParent */
/** @typedef {import('vfile').VFileOptions} VFileOptions */ /** @typedef {import('vfile').VFileOptions} VFileOptions */
const visit = require('unist-util-visit'); const visit = require('unist-util-visit');
@@ -40,7 +41,7 @@ function defaultStoryTag(name) {
* @param {string} name * @param {string} name
*/ */
function defaultPreviewStoryTag(name) { function defaultPreviewStoryTag(name) {
return `<mdjs-preview mdjs-story-name="${name}"></mdjs-preview>`; return `<mdjs-preview mdjs-story-name="${name}">[[CODE SLOT]]</mdjs-preview>`;
} }
/** /**
@@ -55,14 +56,16 @@ function mdjsStoryParse({
} = {}) { } = {}) {
/** @type {Story[]} */ /** @type {Story[]} */
const stories = []; const stories = [];
let index = 0; let htmlIndex = 0;
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
/** /**
* @param {UnistNode} node * @param {UnistNode} node
* @param {number} index
* @param {UnistParent} parent
*/ */
const nodeCodeVisitor = node => { const nodeCodeVisitor = (node, index, parent) => {
if (node.lang === 'js' && node.meta === 'story' && typeof node.value === 'string') { if (node.lang === 'js' && node.meta === 'story' && typeof node.value === 'string') {
const storyData = extractStoryData(node.value); const storyData = extractStoryData(node.value);
node.type = 'html'; node.type = 'html';
@@ -71,30 +74,65 @@ function mdjsStoryParse({
} }
if (node.lang === 'js' && node.meta === 'preview-story' && typeof node.value === 'string') { if (node.lang === 'js' && node.meta === 'preview-story' && typeof node.value === 'string') {
const storyData = extractStoryData(node.value); const storyData = extractStoryData(node.value);
node.type = 'html'; const newValue = previewStoryTag(storyData.name);
node.value = previewStoryTag(storyData.name); if (newValue.includes('[[CODE SLOT]]')) {
const tagParts = newValue.split('[[CODE SLOT]]');
node = {
type: 'root',
children: [
{ type: 'html', value: tagParts[0] },
{ type: 'text', value: '\n\n' },
node,
{ type: 'text', value: '\n\n' },
{ type: 'html', value: tagParts[1] },
],
};
parent.children.splice(index, 1, node);
} else {
node.type = 'html';
node.value = previewStoryTag(storyData.name);
}
stories.push(storyData); stories.push(storyData);
} }
if (node.lang === 'html' && node.meta === 'story') { if (node.lang === 'html' && node.meta === 'story') {
const storyData = extractStoryData( const storyData = extractStoryData(
`export const HtmlStory${index} = () => html\`${node.value}\`;`, `export const HtmlStory${htmlIndex} = () => html\`${node.value}\`;`,
{ type: 'html' }, { type: 'html' },
); );
node.type = 'html'; node.type = 'html';
node.value = storyTag(storyData.name); node.value = storyTag(storyData.name);
stories.push(storyData); stories.push(storyData);
index += 1; htmlIndex += 1;
} }
if (node.lang === 'html' && node.meta === 'preview-story') { if (node.lang === 'html' && node.meta === 'preview-story') {
const storyData = extractStoryData( const storyData = extractStoryData(
`export const HtmlStory${index} = () => html\`${node.value}\`;`, `export const HtmlStory${htmlIndex} = () => html\`${node.value}\`;`,
{ type: 'html' }, { type: 'html' },
); );
node.type = 'html';
node.value = previewStoryTag(storyData.name); const newValue = previewStoryTag(storyData.name);
if (newValue.includes('[[CODE SLOT]]')) {
const tagParts = newValue.split('[[CODE SLOT]]');
node = {
type: 'root',
children: [
{ type: 'html', value: tagParts[0] },
{ type: 'text', value: '\n\n' },
node,
{ type: 'text', value: '\n\n' },
{ type: 'html', value: tagParts[1] },
],
};
parent.children.splice(index, 1, node);
} else {
node.type = 'html';
node.value = previewStoryTag(storyData.name);
}
stories.push(storyData); stories.push(storyData);
index += 1; htmlIndex += 1;
} }
}; };

View File

@@ -45,7 +45,16 @@ describe('Integration', () => {
'<pre><code class="language-js">const foo = 1;', '<pre><code class="language-js">const foo = 1;',
'</code></pre>', '</code></pre>',
'<mdjs-story mdjs-story-name="fooStory"></mdjs-story>', '<mdjs-story mdjs-story-name="fooStory"></mdjs-story>',
'<mdjs-preview mdjs-story-name="fooPreviewStory"></mdjs-preview>', '<mdjs-preview mdjs-story-name="fooPreviewStory">',
'',
'',
'',
'<pre><code class="language-js">export const fooPreviewStory = () => {}',
'</code></pre>',
'',
'',
'',
'</mdjs-preview>',
]; ];
const parser = unified() const parser = unified()

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-template-curly-in-string */ /* eslint-disable no-template-curly-in-string */
const chai = require('chai'); const chai = require('chai');
const { adjustPluginOptions } = require('plugins-manager');
const { mdjsProcess } = require('../src/mdjsProcess.js'); const { mdjsProcess } = require('../src/mdjsProcess.js');
const { expect } = chai; const { expect } = chai;
@@ -28,27 +29,44 @@ describe('mdjsProcess', () => {
'<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>', '<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>',
'</code></pre>', '</code></pre>',
'<mdjs-story mdjs-story-name="fooStory"></mdjs-story>', '<mdjs-story mdjs-story-name="fooStory"></mdjs-story>',
'<mdjs-preview mdjs-story-name="fooPreviewStory"></mdjs-preview>', '<mdjs-preview mdjs-story-name="fooPreviewStory">',
'',
'',
'',
'<pre class="language-js"><code class="language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">fooPreviewStory</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span>',
'</code></pre>',
'',
'',
'',
'</mdjs-preview>',
].join('\n'); ].join('\n');
const expectedJsCode = [ const expectedJsCode = [
'/** script code **/',
'const bar = 2;', 'const bar = 2;',
'/** stories code **/',
'export const fooStory = () => {}', 'export const fooStory = () => {}',
'export const fooPreviewStory = () => {}', 'export const fooPreviewStory = () => {}',
'/** stories setup code **/',
'const rootNode = document;', 'const rootNode = document;',
`const stories = [{ key: 'fooStory', story: fooStory, code: \`<pre class="language-js"><code class="language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">fooStory</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span>`, `const stories = [{ key: 'fooStory', story: fooStory }, { key: 'fooPreviewStory', story: fooPreviewStory }];`,
`</code></pre>\` }, { key: 'fooPreviewStory', story: fooPreviewStory, code: \`<pre class="language-js"><code class="language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">fooPreviewStory</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span>`, 'let needsMdjsElements = false;',
`</code></pre>\` }];`,
'for (const story of stories) {', 'for (const story of stories) {',
' const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);', ' const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);',
' storyEl.codeHasHtml = true;', ' if (storyEl) {',
' storyEl.story = story.story;', ' storyEl.story = story.story;',
' storyEl.code = story.code;', ' storyEl.key = story.key;',
' needsMdjsElements = true;',
' Object.assign(storyEl, {});',
' }',
'};', '};',
`if (!customElements.get('mdjs-preview')) { import('@mdjs/mdjs-preview/mdjs-preview.js'); }`, 'if (needsMdjsElements) {',
`if (!customElements.get('mdjs-story')) { import('@mdjs/mdjs-story/mdjs-story.js'); }`, ` if (!customElements.get('mdjs-preview')) { import('@mdjs/mdjs-preview/define'); }`,
` if (!customElements.get('mdjs-story')) { import('@mdjs/mdjs-story/define'); }`,
'}',
].join('\n'); ].join('\n');
const result = await mdjsProcess(input); const result = await mdjsProcess(input);
expect(result.html).to.equal(expected); expect(result.html).to.equal(expected);
expect(result.jsCode).to.equal(expectedJsCode); expect(result.jsCode).to.equal(expectedJsCode);
}); });
@@ -150,4 +168,45 @@ describe('mdjsProcess', () => {
const result = await mdjsProcess(input); const result = await mdjsProcess(input);
expect(result.html.trim()).to.equal(expected); expect(result.html.trim()).to.equal(expected);
}); });
it('can adjust languages for story preview', async () => {
const input = [
'Intro',
'```js preview-story',
'export const fooPreviewStory = () => {}',
'```',
].join('\n');
const expected = [
`/** script code **/`,
``,
`/** stories code **/`,
`export const fooPreviewStory = () => {}`,
`/** stories setup code **/`,
`const rootNode = document;`,
`const stories = [{ key: 'fooPreviewStory', story: fooPreviewStory }];`,
`let needsMdjsElements = false;`,
`for (const story of stories) {`,
' const storyEl = rootNode.querySelector(`[mdjs-story-name="${story.key}"]`);',
` if (storyEl) {`,
` storyEl.story = story.story;`,
` storyEl.key = story.key;`,
` needsMdjsElements = true;`,
' Object.assign(storyEl, {"languages":[{"key":"en","name":"English"}]});',
` }`,
`};`,
`if (needsMdjsElements) {`,
` if (!customElements.get('mdjs-preview')) { import('@mdjs/mdjs-preview/define'); }`,
` if (!customElements.get('mdjs-story')) { import('@mdjs/mdjs-story/define'); }`,
`}`,
].join('\n');
const result = await mdjsProcess(input, {
setupUnifiedPlugins: [
adjustPluginOptions('mdjsSetupCode', {
simulationSettings: { languages: [{ key: 'en', name: 'English' }] },
}),
],
});
expect(result.jsCode.trim()).to.equal(expected);
});
}); });

View File

@@ -28,7 +28,7 @@ describe('mdjsParse', () => {
expect(/** @type {MDJSVFileData} */ (result.data).jsCode).to.equal('const bar = 22;'); expect(/** @type {MDJSVFileData} */ (result.data).jsCode).to.equal('const bar = 22;');
}); });
// TODO: fix this bug // TODO: fix this bug - maybe something in unified itself 🤔
it.skip('handling only "js script" code blocks', async () => { it.skip('handling only "js script" code blocks', async () => {
const input = [ const input = [
// //

View File

@@ -37,9 +37,27 @@ describe('mdjsStoryParse', () => {
'<pre><code class="language-js">const foo = 1;', '<pre><code class="language-js">const foo = 1;',
'</code></pre>', '</code></pre>',
'<mdjs-story mdjs-story-name="fooStory"></mdjs-story>', '<mdjs-story mdjs-story-name="fooStory"></mdjs-story>',
'<mdjs-preview mdjs-story-name="fooPreviewStory"></mdjs-preview>', '<mdjs-preview mdjs-story-name="fooPreviewStory">',
'',
'',
'',
'<pre><code class="language-js">export const fooPreviewStory = () => {}',
'</code></pre>',
'',
'',
'',
'</mdjs-preview>',
'<mdjs-story mdjs-story-name="HtmlStory0"></mdjs-story>', '<mdjs-story mdjs-story-name="HtmlStory0"></mdjs-story>',
'<mdjs-preview mdjs-story-name="HtmlStory1"></mdjs-preview>', '<mdjs-preview mdjs-story-name="HtmlStory1">',
'',
'',
'',
'<pre><code class="language-html">&#x3C;demo-el>&#x3C;/demo-el>',
'</code></pre>',
'',
'',
'',
'</mdjs-preview>',
'', '',
].join('\n'); ].join('\n');

View File

@@ -25,6 +25,7 @@ export interface ParseResult {
data: { data: {
stories: Story[]; stories: Story[];
jsCode: string; jsCode: string;
setupJsCode: string;
}; };
} }

Some files were not shown because too many files have changed in this diff Show More