Compare commits

..

10 Commits

Author SHA1 Message Date
Jorge del Casar
0b16c5b6f3 feat: add generate import map 2021-02-07 09:15:25 +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
78 changed files with 1750 additions and 304 deletions

View File

@@ -1,7 +1,17 @@
# Tools >> Check HTML Links ||30
```js
import '@rocket/launch/inline-notification/inline-notification.js';
```
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
- 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
```
## 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
# 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

View File

@@ -129,3 +129,25 @@ const config = {
{% 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,
};
```

View File

@@ -1,5 +1,19 @@
# check-html-links
## 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
### Patch Changes

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "check-html-links",
"version": "0.1.2",
"version": "0.2.0",
"publishConfig": {
"access": "public"
},
@@ -33,7 +33,9 @@
],
"dependencies": {
"chalk": "^4.0.0",
"command-line-args": "^5.1.1",
"glob": "^7.0.0",
"minimatch": "^3.0.4",
"sax-wasm": "^2.0.0"
},
"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
import path from 'path';
import chalk from 'chalk';
import { validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
import { CheckHtmlLinksCli } from './CheckHtmlLinksCli.js';
async function main() {
const userRootDir = process.argv[2];
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const files = await listFiles('**/*.html', rootDir);
const filesOutput =
files.length == 0
? '🧐 No files to check. Did you select the correct folder?'
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`;
console.log(filesOutput);
const { errors, numberLinks } = await validateFiles(files, rootDir);
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
const performance = process.hrtime(performanceStart);
if (errors.length > 0) {
let referenceCount = 0;
for (const error of errors) {
referenceCount += error.usage.length;
}
const output = [
`❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links) while checking ${
files.length
} files:`,
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
console.error(output.join('\n'));
process.exit(1);
} else {
console.log(
`✅ All internal links are valid. (executed in %ds %dms)`,
performance[0],
performance[1] / 1000000,
);
}
}
main();
const cli = new CheckHtmlLinksCli();
cli.run();

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import fs from 'fs';
import saxWasm from 'sax-wasm';
import minimatch from 'minimatch';
import { createRequire } from 'module';
import { listFiles } from './listFiles.js';
@@ -10,6 +11,7 @@ import path from 'path';
/** @typedef {import('../types/main').LocalFile} LocalFile */
/** @typedef {import('../types/main').Usage} Usage */
/** @typedef {import('../types/main').Error} Error */
/** @typedef {import('../types/main').Options} Options */
/** @typedef {import('sax-wasm').Attribute} Attribute */
const require = createRequire(import.meta.url);
@@ -185,8 +187,9 @@ function getValueAndAnchor(inValue) {
* @param {object} options
* @param {string} options.htmlFilePath
* @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) {
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;
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
} else if (valueFile === '' && anchor !== '') {
addLocalFile(htmlFilePath, anchor, usageObj);
@@ -261,8 +266,9 @@ async function validateLocalFiles(checkLocalFiles) {
/**
* @param {string[]} files
* @param {string} rootDir
* @param {Options} opts?
*/
export async function validateFiles(files, rootDir) {
export async function validateFiles(files, rootDir, opts) {
await parserReferences.prepareWasm(saxWasmBuffer);
await parserIds.prepareWasm(saxWasmBuffer);
@@ -270,10 +276,20 @@ export async function validateFiles(files, rootDir) {
checkLocalFiles = [];
idCache = new Map();
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) {
const { links } = await extractReferences(htmlFilePath);
numberLinks += links.length;
await resolveLinks(links, { htmlFilePath, rootDir });
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
}
await validateLocalFiles(checkLocalFiles);
@@ -282,10 +298,11 @@ export async function validateFiles(files, rootDir) {
/**
* @param {string} inRootDir
* @param {Options} opts?
*/
export async function validateFolder(inRootDir) {
export async function validateFolder(inRootDir, opts) {
const rootDir = path.resolve(inRootDir);
const files = await listFiles('**/*.html', rootDir);
const { errors } = await validateFiles(files, rootDir);
const { errors } = await validateFiles(files, rootDir, opts);
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));
export async function execute(inPath) {
export async function execute(inPath, opts) {
const testDir = path.join(__dirname, inPath.split('/').join(path.sep));
const errors = await validateFolder(testDir);
const errors = await validateFolder(testDir, opts);
return {
cleanup: items => {
const newItems = [];

View File

@@ -183,6 +183,28 @@ describe('validateFolder', () => {
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 () => {
const { errors, cleanup } = await execute('fixtures/internal-images');
expect(cleanup(errors)).to.deep.equal([

View File

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

View File

@@ -1,5 +1,26 @@
# @rocket/cli
## 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
### Minor Changes

View File

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

View File

@@ -33,6 +33,35 @@ export class RocketEleventy extends Eleventy {
await super.write();
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 {

View File

@@ -2,8 +2,7 @@
/** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */
import chalk from 'chalk';
import { validateFolder, formatErrors } from 'check-html-links';
import { CheckHtmlLinksCli } from 'check-html-links';
export class RocketLint {
static pluginName = 'RocketLint';
@@ -49,31 +48,20 @@ export class RocketLint {
return;
}
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const errors = await validateFolder(this.config.lintInputDir);
const performance = process.hrtime(performanceStart);
const checkLinks = new CheckHtmlLinksCli();
checkLinks.setOptions({
rootDir: this.config.lintInputDir,
printOnError: false,
continueOnError: true,
});
const { errors, message } = await checkLinks.run();
if (errors.length > 0) {
let referenceCount = 0;
for (const error of errors) {
referenceCount += error.usage.length;
}
const output = [
`❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links):`,
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
if (this.config.watch) {
console.log(output.join('\n'));
if (this.config.command === 'start') {
console.log(message);
} 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) {
delete config.pathPrefix;
config.createSocialMediaImages = !!config?.start?.createSocialMediaImages;
return config;
}

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ export function setFixtureDir(importMetaUrl) {
* @property {boolean} stripToBody
* @property {boolean} stripStartEndWhitespace
* @property {boolean} stripScripts
* @property {boolean} formatHtml
* @property {start|build} type
*/
@@ -54,6 +55,10 @@ export async function readOutput(
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;
let text = await fs.promises.readFile(path.join(outputDir, fileName));
text = text.toString();
@@ -81,6 +86,16 @@ export async function readOutput(
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 {string} fileName
@@ -91,10 +106,21 @@ export async function readStartOutput(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) {
await cli.setup();
cli.config.outputDevDir = path.join(configFileDir, '__output-dev');
cli.config.devServer.open = false;
cli.config.devServer.port = 8080;
cli.config.watch = false;
cli.config.outputDir = path.join(configFileDir, '__output');
await cli.run();
@@ -110,6 +136,15 @@ export async function executeStart(pathToConfig) {
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) {
const configFile = path.join(fixtureDir, pathToConfig.split('/').join(path.sep));
const cli = new RocketCli({

View File

@@ -1,6 +1,12 @@
import chai from 'chai';
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;
@@ -22,42 +28,49 @@ describe('RocketCli computedConfig', () => {
it('will extract a title from markdown and set first folder as section', async () => {
cli = await executeStart('computed-config-fixtures/headlines/rocket.config.js');
const indexHtml = await readOutput(cli, 'index.html', {
type: 'start',
});
const indexHtml = await readStartOutput(cli, 'index.html');
const [indexTitle, indexSection] = indexHtml.split('\n');
expect(indexTitle).to.equal('Root');
expect(indexSection).to.be.undefined;
const subHtml = await readOutput(cli, 'sub/index.html', {
type: 'start',
});
const subHtml = await readStartOutput(cli, 'sub/index.html');
const [subTitle, subSection] = subHtml.split('\n');
expect(subTitle).to.equal('Root: Sub');
expect(subSection).to.equal('sub');
const subSubHtml = await readOutput(cli, 'sub/subsub/index.html', {
type: 'start',
});
const subSubHtml = await readStartOutput(cli, 'sub/subsub/index.html');
const [subSubTitle, subSubSection] = subSubHtml.split('\n');
expect(subSubTitle).to.equal('Sub: SubSub');
expect(subSubSection).to.equal('sub');
const sub2Html = await readOutput(cli, 'sub2/index.html', {
type: 'start',
});
const sub2Html = await readStartOutput(cli, 'sub2/index.html');
const [sub2Title, sub2Section] = sub2Html.split('\n');
expect(sub2Title).to.equal('Root: Sub2');
expect(sub2Section).to.equal('sub2');
const withDataHtml = await readOutput(cli, 'with-data/index.html', {
type: 'start',
});
const withDataHtml = await readStartOutput(cli, 'with-data/index.html');
const [withDataTitle, withDataSection] = withDataHtml.split('\n');
expect(withDataTitle).to.equal('Set via data');
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 () => {
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 () => {
cli = await executeStart('computed-config-fixtures/setup/addPlugin.rocket.config.js');
const indexHtml = await readOutput(cli, 'index.html', {
type: 'start',
});
const indexHtml = await readStartOutput(cli, 'index.html');
expect(indexHtml).to.equal('test-value');
});

View File

@@ -1,86 +1,25 @@
import chai from 'chai';
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';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import {
executeBuild,
executeLint,
executeStart,
expectThrowsAsync,
readBuildOutput,
readStartOutput,
setFixtureDir,
} from '@rocket/cli/test-helpers';
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', () => {
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(() => {
// ignore colors in tests as most CIs won't support it
chalk.level = 0;
setFixtureDir(import.meta.url);
});
afterEach(async () => {
@@ -90,79 +29,40 @@ describe('RocketCli e2e', () => {
});
it('can add a unified plugin via the config', async () => {
cli = new RocketCli({
argv: [
'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,
});
cli = await executeStart('e2e-fixtures/unified-plugin/rocket.config.js');
const indexHtml = await readStartOutput(cli, 'index.html');
expect(indexHtml).to.equal('<p>See a 🐶</p>');
});
describe('eleventy in config', () => {
// TODO: find out while this has a side effect and breaks other tests
it.skip('can modify eleventy via an elventy function in the config', async () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'eleventy.rocket.config.js'),
],
});
await execute();
const indexHtml = await readOutput('index.html', {
type: 'start',
});
cli = await executeStart('e2e-fixtures/content/eleventy.rocket.config.js');
const indexHtml = await readStartOutput(cli, 'index.html');
expect(indexHtml).to.equal(
['# 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 () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'eleventy-return.rocket.config.js'),
],
});
await expectThrowsAsync(() => execute(), {
errorMatch: /Error in your Eleventy config file.*/,
});
await expectThrowsAsync(
() => executeStart('e2e-fixtures/content/eleventy-return.rocket.config.js'),
{
errorMatch: /Error in your Eleventy config file.*/,
},
);
});
});
describe('setupDevAndBuildPlugins in config', () => {
it('can add a rollup plugin via setupDevAndBuildPlugins for build command', async () => {
cli = new RocketCli({
argv: [
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild.rocket.config.js'),
],
});
await execute();
const inlineModule = await readOutput('e97af63d.js');
cli = await executeBuild('e2e-fixtures/rollup-plugin/devbuild.rocket.config.js');
const inlineModule = await readBuildOutput(cli, 'e97af63d.js');
expect(inlineModule).to.equal('var a={test:"data"};console.log(a);');
});
it('can add a rollup plugin via setupDevAndBuildPlugins for start command', async () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild.rocket.config.js'),
],
});
await execute();
cli = await executeStart('e2e-fixtures/rollup-plugin/devbuild.rocket.config.js');
const response = await fetch('http://localhost:8080/test-data.json');
expect(response.ok).to.be.true; // no server error
@@ -173,88 +73,45 @@ describe('RocketCli e2e', () => {
});
it('can add a rollup plugin for dev & build and modify a build only plugin via the config', async () => {
cli = new RocketCli({
argv: [
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rollup-plugin', 'devbuild-build.rocket.config.js'),
],
});
await execute();
const inlineModule = await readOutput('e97af63d.js');
cli = await executeBuild('e2e-fixtures/rollup-plugin/devbuild-build.rocket.config.js');
const inlineModule = await readBuildOutput(cli, 'e97af63d.js');
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;
});
it('can adjust the inputDir', async () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'change-input-dir', 'rocket.config.js'),
],
});
await execute();
cli = await executeStart('e2e-fixtures/change-input-dir/rocket.config.js');
const indexHtml = await readOutput('index.html', {
type: 'start',
});
const indexHtml = await readStartOutput(cli, 'index.html');
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 () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'rocket-config-in-template', 'rocket.config.js'),
],
});
await execute();
cli = await executeStart('e2e-fixtures/rocket-config-in-template/rocket.config.js');
const indexHtml = await readOutput('index.html', {
type: 'start',
});
const indexHtml = await readStartOutput(cli, 'index.html');
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>',
);
});
it('can add a pathPrefix that will not influence the start command', async () => {
cli = new RocketCli({
argv: [
'start',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
],
});
await execute();
cli = await executeStart('e2e-fixtures/content/pathPrefix.rocket.config.js');
const linkHtml = await readOutput('link/index.html', {
type: 'start',
});
const linkHtml = await readStartOutput(cli, 'link/index.html');
expect(linkHtml).to.equal(
['<p><a href="../">home</a></p>', '<p><a href="/">absolute home</a></p>'].join('\n'),
);
const assetHtml = await readOutput('use-assets/index.html', {
type: 'start',
});
const assetHtml = await readStartOutput(cli, 'use-assets/index.html');
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 () => {
cli = new RocketCli({
argv: [
'build',
'--config-file',
path.join(__dirname, 'e2e-fixtures', 'content', 'pathPrefix.rocket.config.js'),
],
});
await execute();
cli = await executeBuild('e2e-fixtures/content/pathPrefix.rocket.config.js');
const linkHtml = await readOutput('link/index.html', {
const linkHtml = await readBuildOutput(cli, 'link/index.html', {
stripServiceWorker: true,
stripToBody: true,
});
@@ -263,7 +120,7 @@ describe('RocketCli e2e', () => {
'\n',
),
);
const assetHtml = await readOutput('use-assets/index.html', {
const assetHtml = await readBuildOutput(cli, 'use-assets/index.html', {
stripServiceWorker: true,
});
expect(assetHtml).to.equal(

View File

@@ -0,0 +1,87 @@
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">',
' import { myData } from "../sub/assets/myData.js";',
' import("../sub/assets/myData.js");',
' const name = "myData";',
' import(`../sub/assets/${name}.js`);',
'</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">',
' import { myData } from "./assets/myData.js";',
' import("./assets/myData.js");',
' const name = "myData";',
' import(`./assets/${name}.js`);',
'</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">',
' import { myData } from "../assets/myData.js";',
' import("../assets/myData.js");',
' const name = "myData";',
' import(`../assets/${name}.js`);',
'</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">',
' import { myData } from "./sub/assets/myData.js";',
' import("./sub/assets/myData.js");',
' const name = "myData";',
' import(`./sub/assets/${name}.js`);',
'</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>} */
const config = {
start: {
createSocialMediaImages: true,
},
setupEleventyComputedConfig: [
adjustPluginOptions('socialMediaImage', {
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>} */
const config = {};
const config = {
start: {
createSocialMediaImages: true,
},
};
export default config;

View File

@@ -6,7 +6,7 @@ import json from '@rollup/plugin-json';
import { addPlugin, adjustPluginOptions } from 'plugins-manager';
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>} */
const config = {

View File

@@ -29,6 +29,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({
command: 'help',
createSocialMediaImages: true,
devServer: {},
build: {},
watch: true,
@@ -61,6 +62,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({
command: 'help',
createSocialMediaImages: true,
devServer: {
more: 'settings',
},
@@ -92,6 +94,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({
command: 'help',
createSocialMediaImages: true,
devServer: {
more: 'from-file',
},
@@ -128,6 +131,7 @@ describe('normalizeConfig', () => {
expect(cleanup(config)).to.deep.equal({
command: 'help',
createSocialMediaImages: true,
devServer: {},
build: {},
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[];
}
interface RocketStartConfig {
createSocialMediaImages?: boolean;
}
export interface RocketCliOptions {
presets: Array<RocketPreset>;
pathPrefix?: string;
inputDir: string;
outputDir: string;
emptyOutputDir?: boolen;
emptyOutputDir?: boolean;
absoluteBaseUrl?: string;
watch: boolean;
createSocialMediaImages?: boolean;
start?: RocketStartConfig;
// TODO: improve all setup functions
setupUnifiedPlugins?: function[];

View File

@@ -1,5 +1,11 @@
# @rocket/eleventy-plugin-mdjs-unified
## 0.3.1
### Patch Changes
- f44a0f4: Rewrite dynamic imports with "`"
## 0.3.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/eleventy-plugin-mdjs-unified",
"version": "0.3.0",
"version": "0.3.1",
"publishConfig": {
"access": "public"
},

View File

@@ -72,10 +72,14 @@ async function processImports(source, inputPath) {
newSource += '.' + importSrc;
} else if (importSrc.startsWith("'./")) {
newSource += "'." + importSrc.substring(1);
} else if (importSrc.startsWith('`./')) {
newSource += '`.' + importSrc.substring(1);
} else if (importSrc.startsWith('../')) {
newSource += '../' + importSrc;
} else if (importSrc.startsWith("'../")) {
newSource += "'../" + importSrc.substring(1);
} else if (importSrc.startsWith('`../')) {
newSource += '`../' + importSrc.substring(1);
} else {
newSource += importSrc;
}

View File

@@ -0,0 +1,28 @@
# Check HTML Links
A fast checker for broken links/references in HTML.
## Installation
```
npm i -D check-html-links
```
## Usage
```
npx check-html-links _site
```
For docs please see our homepage [https://rocket.modern-web.dev/docs/tools/check-html-links/](https://rocket.modern-web.dev/docs/tools/check-html-links/).
## Comparison
Checking the output of the [11ty-website](https://github.com/11ty/11ty-website) with 13 missing reference targets (used by 516 links) while checking 501 files. (on January 17, 2021)
| Tool | Lines printed | Times | Lang | Dependency Tree |
| ---------------- | ------------- | ------ | ---- | --------------- |
| check-html-links | 38 | ~2.5s | node | 19 |
| link-checker | 3000+ | ~11s | node | 106 |
| hyperlink | 68 | 4m 20s | node | 481 |
| htmltest | 1000+ | ~0.7s | GO | - |

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
import path from 'path';
import chalk from 'chalk';
import { extractBareImports } from './inspectFolder.js';
import { checkRemoteSpecifiers, createImportMap } from './createImportMap.js';
import { listFiles } from './listFiles.js';
async function main() {
const userRootDir = process.argv[2];
const userImportMapDist = process.argv[3];
const remoteUrl = process.argv[4];
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
const importMapDist = userImportMapDist
? path.resolve(userImportMapDist)
: `${process.cwd()}/import-map.json`;
const performanceInspectStart = process.hrtime();
console.log('👀 Looking for all imports...');
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 { localSpecifiers, bareSpecifiers } = await extractBareImports(files, rootDir);
const performanceInspectEnd = process.hrtime(performanceInspectStart);
console.log(
`🔗 Found a total of ${chalk.green.bold(
localSpecifiers.length,
)} local imports and ${chalk.green.bold(bareSpecifiers.length)} bare imports!`,
);
console.log(
`✅ Files inspected. (executed in %ds %dms)`,
performanceInspectEnd[0],
performanceInspectEnd[1] / 1000000,
);
const performanceRemoteStart = process.hrtime();
const foundBareSpecifiers = await checkRemoteSpecifiers(bareSpecifiers, remoteUrl);
createImportMap(foundBareSpecifiers, remoteUrl, importMapDist);
const performanceRemoteEnd = process.hrtime(performanceRemoteStart);
console.log(
`✅ Remote CDN check and import map created. (executed in %ds %dms)`,
performanceRemoteEnd[0],
performanceRemoteEnd[1] / 1000000,
);
}
main();

View File

@@ -0,0 +1,48 @@
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
export const defaultRemoteUrl = 'https://cdn.skypack.dev/';
export const defaultImportMapDist = path.resolve('./import-map.json');
/**
*
* @param {string[]} bareSpecifiers
* @param {string} remoteUrl
*/
export async function checkRemoteSpecifiers(bareSpecifiers, remoteUrl = defaultRemoteUrl) {
/** @type {Promise<void>[]} */
const promises = [];
/** @type {string[]} */
const result = [];
bareSpecifiers.forEach(specifier => {
const remoteSpecifier = `${remoteUrl}${specifier}`;
const promise = fetch(remoteSpecifier).then(response => {
if (response.ok) {
result.push(specifier);
}
});
promises.push(promise);
});
await Promise.all(promises);
return result;
}
/**
*
* @param {string[]} bareSpecifiers
* @param {string} remoteUrl
* @param {string} importMapDist
*/
export function createImportMap(
bareSpecifiers,
remoteUrl = defaultRemoteUrl,
importMapDist = defaultImportMapDist,
) {
const imports = {};
bareSpecifiers.forEach(specifier => {
const remoteSpecifier = `${remoteUrl}${specifier}`;
imports[specifier] = remoteSpecifier;
});
fs.writeFileSync(importMapDist, JSON.stringify({ imports }, null, 2));
}

View File

@@ -0,0 +1,197 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import fs from 'fs';
import saxWasm from 'sax-wasm';
import { createRequire } from 'module';
import { init, parse } from 'es-module-lexer';
import { listFiles } from './listFiles.js';
import path from 'path';
/** @typedef {import('../types/main').Script} Script */
/** @typedef {import('../types/main').Import} Import */
/** @typedef {import('sax-wasm').Attribute} Attribute */
/** @typedef {import('sax-wasm').Tag} Tag */
/** @typedef {import('sax-wasm').Text} Text */
/** @typedef {import('es-module-lexer').ImportSpecifier} ImportSpecifier */
const require = createRequire(import.meta.url);
const { SaxEventType, SAXParser } = saxWasm;
const streamOptions = { highWaterMark: 256 * 1024 };
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
const saxWasmBuffer = fs.readFileSync(saxPath);
const parserSpecifiers = new SAXParser(
SaxEventType.OpenTag | SaxEventType.CloseTag | SaxEventType.Attribute | SaxEventType.Text,
streamOptions,
);
let localSpecifiers = new Set();
let bareSpecifiers = new Set();
/**
* @param {string} filePath
* @param {object} option
* @param {string} option.rootDir
*/
function analyzeFile(filePath, { rootDir }) {
if (fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory()) {
const source = fs.readFileSync(filePath, { encoding: 'utf-8' });
const importDir = path.dirname(filePath);
console.log('Resolving specifier %s', filePath);
getSpecifiers(source).forEach(specifier => {
addSpecifier(specifier, { importDir, rootDir });
});
}
}
/**
* @param {string} source
*/
function getSpecifiers(source) {
/** @type {[ImportSpecifier[], string[]]} */
const [imports] = parse(source, 'optional-sourcename');
return imports.map(specifier => source.substring(specifier.s, specifier.e).replace(/'/g, ''));
}
/**
*
* @param {string} specifier
* @param {object} options
* @param {string} options.importDir
* @param {string} options.rootDir
*/
function addSpecifier(specifier, { importDir, rootDir }) {
let newSpecifier = specifier.trim();
if (newSpecifier.endsWith('/')) {
newSpecifier += 'index.js';
}
if (newSpecifier.startsWith('/')) {
// Absolute path specifier
newSpecifier = path.join(rootDir, newSpecifier);
} else if (newSpecifier.startsWith('../') || newSpecifier.startsWith('./')) {
// Relative path specifier
newSpecifier = path.join(importDir, newSpecifier);
}
if (newSpecifier.startsWith('/')) {
if (!localSpecifiers.has(newSpecifier)) {
localSpecifiers.add(newSpecifier);
analyzeFile(newSpecifier, { rootDir });
}
} else {
const [namespace, name] = newSpecifier.split('/');
if (namespace.startsWith('@')) {
newSpecifier = `${namespace}/${name}`;
} else {
newSpecifier = namespace;
}
bareSpecifiers.add(newSpecifier);
}
}
/**
*
* @param {Script[]} scripts
* @param {object} options
* @param {string} options.importDir
* @param {string} options.rootDir
*/
async function resolveSpecifiers(scripts, { importDir, rootDir }) {
for (const scriptObj of scripts) {
if (scriptObj.specifier) {
addSpecifier(scriptObj.specifier, { importDir, rootDir });
} else {
getSpecifiers(scriptObj.content).forEach(specifier => {
addSpecifier(specifier, { importDir, rootDir });
});
}
}
}
/**
* @param {string} filePath
*/
function extractScripts(filePath) {
/** @type {Script[]} */
const scripts = [];
let captureContent = false;
parserSpecifiers.eventHandler = (ev, /** @type {any} */ _data) => {
const data = /** @type {Tag | Text} */ _data;
if (captureContent && ev === SaxEventType.Text) {
const scriptObj = scripts[scripts.length - 1];
scriptObj.content += data.value;
} else if (ev === SaxEventType.CloseTag && data.name.toString() === 'script') {
captureContent = false;
} else if (ev === SaxEventType.OpenTag) {
const tagName = data.name.toString();
const type = data.attributes.find(
(/** @type Attribute */ attr) => attr.name.toString() === 'type',
);
if (tagName === 'script' && type?.value.toString() === 'module') {
const scriptObj = /** @type {Script} */ {
filePath,
content: '',
specifier: '',
};
const src = data.attributes.find(
(/** @type Attribute */ attr) => attr.name.toString() === 'src',
);
if (src) {
let srcPath = src.value.toString().trim();
if (!srcPath.startsWith('.') || !srcPath.startsWith('/')) {
srcPath = `./${srcPath}`;
}
scriptObj.specifier = srcPath;
} else {
captureContent = true;
}
scripts.push(scriptObj);
}
}
};
return new Promise(resolve => {
const readable = fs.createReadStream(filePath, streamOptions);
readable.on('data', chunk => {
// @ts-expect-error
parserSpecifiers.write(chunk);
});
readable.on('end', () => {
parserSpecifiers.end();
resolve(scripts);
});
});
}
/**
* @param {string[]} files
* @param {string} rootDir
*/
export async function extractBareImports(files, rootDir) {
await parserSpecifiers.prepareWasm(saxWasmBuffer);
await init;
localSpecifiers = new Set();
bareSpecifiers = new Set();
for (const filePath of files) {
const importDir = path.dirname(filePath);
const scripts = await extractScripts(filePath);
await resolveSpecifiers(scripts, { importDir, rootDir });
}
return {
localSpecifiers: Array.from(localSpecifiers),
bareSpecifiers: Array.from(bareSpecifiers),
};
}
/**
* @param {string} inRootDir
*/
export async function inspectFolder(inRootDir, extension = 'html') {
const rootDir = path.resolve(inRootDir);
const files = await listFiles(`**/*.${extension}`, rootDir);
return await extractBareImports(files, rootDir);
}

View File

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

View File

@@ -0,0 +1,12 @@
<script>
import "/empty.js";
</script>
<script>
import "./empty.js";
</script>
<script>
import "./empty.js ";
</script>
<script>
import " ./empty.js ";
</script>

View File

@@ -0,0 +1,4 @@
<script src="/empty.js"></script>
<script src="./empty.js"></script>
<script src="./empty.js "></script>
<script src=" ./empty.js "></script>

View File

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

View File

@@ -0,0 +1,3 @@
import { LitElement } from 'lit-element';
export class TestElement extends LitElement {}

View File

@@ -0,0 +1,3 @@
import { TestComponent } from './src/TestComponent.js';
window.customElements.define('test-element', TestComponent);

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="scripts/main.js" type="module"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,545 @@
<!doctype html>
<html lang="en" dir="ltr">
<head>
<link rel="stylesheet" href="/_merged_assets/rocket-blog.css">
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap"
rel="stylesheet"
/>
<meta name="twitter:creator" content="@modern_web_dev" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown JavaScript: Overview: Rocket</title>
<meta property="og:title" content="Markdown JavaScript: Overview: Rocket"/>
<meta name="generator" content="rocket 0.1">
<link rel="canonical" href="/docs/markdown-javascript/overview/"/>
<meta name="description" content="Rocket is the way to build fast static websites with a sprinkle of JavaScript"/>
<meta property="og:description" content="Rocket is the way to build fast static websites with a sprinkle of JavaScript"/>
<link rel="apple-touch-icon" sizes="180x180" href="/_merged_assets/_static/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/_merged_assets/_static/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/_merged_assets/_static/icons/favicon-16x16.png">
<link rel="manifest" href="/_merged_assets/webmanifest.json">
<link rel="mask-icon" href="/_merged_assets/_static/icons/safari-pinned-tab.svg" color="#3f93ce">
<meta name="msapplication-TileColor" content="#1d3557">
<meta name="theme-color" content="#1d3557">
<meta property="og:site_name" content="Rocket"/>
<meta property="og:type" content="website"/>
<meta property="og:image" content="/_merged_assets/11ty-img/f8d62426-1200.png"/>
<meta property="og:url" content="/docs/markdown-javascript/overview/"/>
<meta name="twitter:card" content="summary_large_image"/>
<link rel="stylesheet" href="/_merged_assets/variables.css">
<link rel="stylesheet" href="/_merged_assets/layout.css">
<link rel="stylesheet" href="/_merged_assets/markdown.css">
<link rel="stylesheet" href="/_merged_assets/style.css">
<noscript><link rel="stylesheet" href="/_merged_assets/_static/noscript.css"/></noscript>
</head>
<body layout="layout-sidebar">
<header id="main-header">
<div class="content-area">
<button id="mobile-menu-trigger" data-action="trigger-mobile-menu">
<span class="sr-only">Show Menu</span>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewbox="0 0 448 512" class="icon">
<path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path>
</svg>
</button>
<a class="logo-link" href="/">
<img src="/_merged_assets/logo.svg" alt="Rocket Logo" />
<span>Rocket</span>
</a>
<rocket-search class="search" json-url="/_merged_assets/_static/rocket-search-index.json">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" style="width: 25px;">
<path d="M51.6 96.7c11 0 21-3.9 28.8-10.5l35 35c.8.8 1.8 1.2 2.9 1.2s2.1-.4 2.9-1.2c1.6-1.6 1.6-4.2 0-5.8l-35-35c6.5-7.8 10.5-17.9 10.5-28.8 0-24.9-20.2-45.1-45.1-45.1-24.8 0-45.1 20.3-45.1 45.1 0 24.9 20.3 45.1 45.1 45.1zm0-82c20.4 0 36.9 16.6 36.9 36.9C88.5 72 72 88.5 51.6 88.5S14.7 71.9 14.7 51.6c0-20.3 16.6-36.9 36.9-36.9z"/>
</svg>
</rocket-search>
<script type="module">
import '@rocket/search/rocket-search.js';
</script>
<a href="/guides/" class="
">Guides</a>
<a href="/docs/" class="
active
">Docs</a>
<a href="/blog/" class="
">Blog</a>
<launch-dark-switch class="light-dark-switch" label="Toggle darkmode">Toggle darkmode</launch-dark-switch>
<a class="social-link" href="https://github.com/modernweb-dev/rocket" aria-label="Rocket on GitHub" rel="noopener noreferrer" target="_blank">
<span class="sr-only">GitHub</span>
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 17 16" fill="none">
<title>GitHub</title>
<path fill="currentColor" fill-rule="evenodd" d="M8.18391.249268C3.82241.249268.253906 3.81777.253906 8.17927c0 3.46933 2.279874 6.44313 5.451874 7.53353.3965.0991.49563-.1983.49563-.3965v-1.3878c-2.18075.4956-2.67638-.9912-2.67638-.9912-.3965-.8922-.89212-1.1895-.89212-1.1895-.69388-.4957.09912-.4957.09912-.4957.793.0992 1.1895.793 1.1895.793.69388 1.2887 1.88338.8922 2.27988.6939.09912-.4956.29737-.8921.49562-1.0904-1.78425-.1982-3.5685-.8921-3.5685-3.96496 0-.89212.29738-1.586.793-2.08162-.09912-.19825-.3965-.99125.09913-2.08163 0 0 .69387-.19825 2.18075.793.59475-.19825 1.28862-.29737 1.9825-.29737.69387 0 1.38775.09912 1.98249.29737 1.4869-.99125 2.1808-.793 2.1808-.793.3965 1.09038.1982 1.88338.0991 2.08163.4956.59475.793 1.28862.793 2.08162 0 3.07286-1.8834 3.66766-3.66764 3.86586.29737.3965.59474.8921.59474 1.586v2.1808c0 .1982.0991.4956.5948.3965 3.172-1.0904 5.4518-4.0642 5.4518-7.53353-.0991-4.3615-3.6676-7.930002-8.02909-7.930002z" clip-rule="evenodd"></path>
</svg>
</a>
</div>
</header>
<div id="content-wrapper">
<div class="content-area">
<rocket-drawer id="sidebar">
<nav slot="content" id="sidebar-nav">
<a class="logo-link" href="/">
<img src="/_merged_assets/logo.svg" alt="Rocket Logo" />
<span>Rocket</span>
</a>
<rocket-navigation>
<ul><li class="menu-item"><a href="/docs/configuration/">Configuration</a><ul><li class="menu-item"><a href="/docs/configuration/overview/">Overview</a><ul><li class="menu-item anchor"><a href="/docs/configuration/overview/#adding-rollup-plugins" class="anchor">Adding Rollup Plugins</a></li>
<li class="menu-item anchor"><a href="/docs/configuration/overview/#modifying-options-of-plugins" class="anchor">Modifying Options of Plugins</a></li></ul></li>
<li class="menu-item"><a href="/docs/configuration/computed-config/">Computed Config</a><ul><li class="menu-item anchor"><a href="/docs/configuration/computed-config/#set-your-own-data" class="anchor">Set Your Own Data</a></li>
<li class="menu-item anchor"><a href="/docs/configuration/computed-config/#default-available-configs" class="anchor">Default Available Configs</a></li></ul></li></ul></li>
<li class="menu-item"><a href="/docs/presets/">Presets</a><ul><li class="menu-item"><a href="/docs/presets/joining-blocks/">Joining Blocks</a><ul><li class="menu-item anchor"><a href="/docs/presets/joining-blocks/#adding-content-without-overriding" class="anchor">Adding content without overriding</a></li>
<li class="menu-item anchor"><a href="/docs/presets/joining-blocks/#overriding-content" class="anchor">Overriding Content</a></li>
<li class="menu-item anchor"><a href="/docs/presets/joining-blocks/#reordering-and-overriding" class="anchor">Reordering and Overriding</a></li></ul></li>
<li class="menu-item"><a href="/docs/presets/launch/">Launch</a><ul><li class="menu-item anchor"><a href="/docs/presets/launch/#installation" class="anchor">Installation</a></li>
<li class="menu-item anchor"><a href="/docs/presets/launch/#data" class="anchor">Data</a></li>
<li class="menu-item anchor"><a href="/docs/presets/launch/#inline-notification" class="anchor">Inline Notification</a></li></ul></li>
<li class="menu-item"><a href="/docs/presets/search/">Search</a><ul><li class="menu-item anchor"><a href="/docs/presets/search/#installation" class="anchor">Installation</a></li></ul></li>
<li class="menu-item"><a href="/docs/presets/blog/">Blog</a><ul><li class="menu-item anchor"><a href="/docs/presets/blog/#installation" class="anchor">Installation</a></li>
<li class="menu-item anchor"><a href="/docs/presets/blog/#usage" class="anchor">Usage</a></li></ul></li></ul></li>
<li class="menu-item active"><a href="/docs/markdown-javascript/" class="active">Markdown JavaScript</a><ul><li class="menu-item current"><a href="/docs/markdown-javascript/overview/">Overview</a><ul><li class="menu-item anchor"><a href="/docs/markdown-javascript/overview/#web-components" class="anchor">Web Components</a></li>
<li class="menu-item anchor"><a href="/docs/markdown-javascript/overview/#demo-support-story" class="anchor">Demo Support (Story)</a></li>
<li class="menu-item anchor"><a href="/docs/markdown-javascript/overview/#supported-systems" class="anchor">Supported Systems</a></li>
<li class="menu-item anchor"><a href="/docs/markdown-javascript/overview/#build-mdjs" class="anchor">Build mdjs</a></li></ul></li>
<li class="menu-item"><a href="/docs/markdown-javascript/preview/">Preview</a></li>
<li class="menu-item"><a href="/docs/markdown-javascript/story/">Story</a></li></ul></li>
<li class="menu-item"><a href="/docs/eleventy-plugins/">Eleventy Plugins</a><ul><li class="menu-item"><a href="/docs/eleventy-plugins/mdjs-unified/">Markdown JavaScript (mdjs)</a><ul><li class="menu-item anchor"><a href="/docs/eleventy-plugins/mdjs-unified/#setup" class="anchor">Setup</a></li>
<li class="menu-item anchor"><a href="/docs/eleventy-plugins/mdjs-unified/#configure-a-unified-or-remark-plugin-with-mdjs" class="anchor">Configure a unified or remark Plugin with mdjs</a></li>
<li class="menu-item anchor"><a href="/docs/eleventy-plugins/mdjs-unified/#add-a-unified-or-remark-plugin" class="anchor">Add a unified or remark Plugin</a></li></ul></li></ul></li>
<li class="menu-item"><a href="/docs/tools/">Tools</a><ul><li class="menu-item"><a href="/docs/tools/plugins-manager/">Plugins Manager</a><ul><li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#problem" class="anchor">Problem</a></li>
<li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#problem-statement" class="anchor">Problem Statement</a></li>
<li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#solution" class="anchor">Solution</a></li>
<li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#adding-a-plugin" class="anchor">Adding a Plugin</a></li>
<li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#adjusting-plugin-options" class="anchor">Adjusting Plugin Options</a></li>
<li class="menu-item anchor"><a href="/docs/tools/plugins-manager/#converting-metaplugins-to-an-actual-plugin" class="anchor">Converting metaPlugins to an Actual Plugin</a></li></ul></li>
<li class="menu-item"><a href="/docs/tools/rollup-config/">Rollup Config</a><ul><li class="menu-item anchor"><a href="/docs/tools/rollup-config/#features" class="anchor">Features</a></li>
<li class="menu-item anchor"><a href="/docs/tools/rollup-config/#setup" class="anchor">Setup</a></li>
<li class="menu-item anchor"><a href="/docs/tools/rollup-config/#customizations" class="anchor">Customizations</a></li></ul></li>
<li class="menu-item"><a href="/docs/tools/check-html-links/">Check HTML Links</a><ul><li class="menu-item anchor"><a href="/docs/tools/check-html-links/#features" class="anchor">Features</a></li>
<li class="menu-item anchor"><a href="/docs/tools/check-html-links/#installation" class="anchor">Installation</a></li>
<li class="menu-item anchor"><a href="/docs/tools/check-html-links/#usage" class="anchor">Usage</a></li>
<li class="menu-item anchor"><a href="/docs/tools/check-html-links/#example-output" class="anchor">Example Output</a></li>
<li class="menu-item anchor"><a href="/docs/tools/check-html-links/#comparison" class="anchor">Comparison</a></li></ul></li></ul></li></ul>
<div class="sidebar-bottom">
<hr>
<launch-dark-switch class="light-dark-switch" label="Toggle darkmode">Toggle darkmode</launch-dark-switch>
<a href="https://github.com/modernweb-dev/rocket/issues">Help and Feedback</a>
</div>
</rocket-navigation>
</nav>
</rocket-drawer>
<main class="markdown-body">
<h1 id="markdown-javascript-overview"><a class="anchor" href="#markdown-javascript-overview"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Markdown JavaScript: Overview</h1>
<p>Markdown JavaScript (mdjs) is a format that allows you to use JavaScript with Markdown, to create interactive demos. It does so by "annotating" JavaScript that should be executed in Markdown.</p>
<p>To annotate we use a code block with <code>js script</code>.</p>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js script</span>
<span class="token code-block language-js"><span class="token comment">// execute me</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<h2 id="web-components"><a class="anchor" href="#web-components"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Web Components</h2>
<p>One very good use case for that can be web components.
HTML already works in Markdown so all you need is to load a web components definition file.</p>
<p>You could even do so within the same Markdown file.</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">##</span> This is my-card</span>
Here's an example of the component:
<span class="token code"><span class="token punctuation">```</span><span class="token code-language">html preview-story</span>
<span class="token code-block language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>my-card</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>h2</span><span class="token punctuation">></span></span>Hello world!<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>h2</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>button</span><span class="token punctuation">></span></span>Click me!<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>my-card</span><span class="token punctuation">></span></span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<p>You can even execute some JavaScript:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">##</span> This is my-el</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>my-el</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>my-el</span><span class="token punctuation">></span></span>
<span class="token code"><span class="token punctuation">```</span><span class="token code-language">js script</span>
<span class="token code-block language-js"><span class="token keyword module">import</span> <span class="token punctuation">{</span> <span class="token maybe-class-name">LitElement</span><span class="token punctuation">,</span> html <span class="token punctuation">}</span> <span class="token keyword module">from</span> <span class="token string">'https://unpkg.com/lit-element?module'</span><span class="token punctuation">;</span>
<span class="token keyword">class</span> <span class="token class-name">MyEl</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span>
<span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token property-access">innerHTML</span> <span class="token operator">=</span> <span class="token string">'&#x3C;p style="color: red">I am alive&#x3C;/p>'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
customElements<span class="token punctuation">.</span><span class="token method function property-access">define</span><span class="token punctuation">(</span><span class="token string">'my-el'</span><span class="token punctuation">,</span> <span class="token maybe-class-name">MyEl</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<h2 id="demo-support-story"><a class="anchor" href="#demo-support-story"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Demo Support (Story)</h2>
<p>mdjs comes with some additional helpers you can choose to import:</p>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js script</span>
<span class="token code-block language-js"><span class="token keyword module">import</span> <span class="token string">'@mdjs/mdjs-story/mdjs-story.js'</span><span class="token punctuation">;</span>
<span class="token keyword module">import</span> <span class="token string">'@mdjs/mdjs-preview/mdjs-preview.js'</span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<p>Once loaded you can use them like so:</p>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js script</span>
<span class="token code-block language-js"><span class="token keyword module">import</span> <span class="token string">'@mdjs/mdjs-story/mdjs-story.js'</span><span class="token punctuation">;</span>
<span class="token keyword module">import</span> <span class="token string">'@mdjs/mdjs-preview/mdjs-preview.js'</span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<h3 id="story"><a class="anchor" href="#story"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Story</h3>
<p>The code snippet will actually get executed at that place and you will have a live demo</p>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js story</span>
<span class="token code-block language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function"><span class="token maybe-class-name">JsStory</span></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 embedded-code html"> &#x3C;demo-wc-card>JS Story&#x3C;/demo-wc-card> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">html story</span>
<span class="token code-block language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>demo-wc-card</span><span class="token punctuation">></span></span>HTML Story<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>demo-wc-card</span><span class="token punctuation">></span></span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<h4 id="full-code-support"><a class="anchor" href="#full-code-support"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Full Code Support</h4>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js story</span>
<span class="token code-block language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function"><span class="token maybe-class-name">JsStory</span></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 keyword">const</span> calculateSomething <span class="token operator">=</span> <span class="token number">12</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token embedded-code html">
&#x3C;demo-wc-card .header=${`Something: ${calculateSomething}`}>JS Story&#x3C;/demo-wc-card>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<h3 id="preview-story"><a class="anchor" href="#preview-story"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Preview Story</h3>
<p>Will become a live demo wrapped in a container with a show code button.</p>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">js preview-story</span>
<span class="token code-block language-js"><span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function"><span class="token maybe-class-name">JsPreviewStory</span></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 embedded-code html"> &#x3C;demo-wc-card>JS Preview Story&#x3C;/demo-wc-card> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<pre class="language-md"><code class="language-md"><span class="token code"><span class="token punctuation">```</span><span class="token code-language">html preview-story</span>
<span class="token code-block language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>demo-wc-card</span><span class="token punctuation">></span></span>HTML Preview Story<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>demo-wc-card</span><span class="token punctuation">></span></span></span>
<span class="token punctuation">```</span></span>
</code></pre>
<p>Here is a live example from <a href="https://www.npmjs.com/package/demo-wc-card">demo-wc-card</a>.</p>
<mdjs-preview mdjs-story-name="header"></mdjs-preview>
<h2 id="supported-systems"><a class="anchor" href="#supported-systems"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Supported Systems</h2>
<h3 id="es-dev-server"><a class="anchor" href="#es-dev-server"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>es-dev-server</h3>
<p>Preview your mdjs readme with live demos and auto reload.</p>
<ul>
<li>
<p>Add to your <code>package.json</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"es-dev-server"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre>
</li>
<li>
<p>Create a <code>es-dev-server.config.js</code> in the root of your repository.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> mdjsTransformer <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@mdjs/core'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
module<span class="token punctuation">.</span><span class="token property-access">exports</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
nodeResolve<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
open<span class="token punctuation">:</span> <span class="token string">'README.md'</span><span class="token punctuation">,</span>
watch<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
responseTransformers<span class="token punctuation">:</span> <span class="token punctuation">[</span>mdjsTransformer<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre>
</li>
</ul>
<h3 id="storybook"><a class="anchor" href="#storybook"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Storybook</h3>
<p>Please check out <a href="https://open-wc.org/demoing/">@open-wc/demoing-storybook</a> for a fully integrated setup.</p>
<p>It includes <a href="https://open-wc.org/demoing/storybook-addon-markdown-docs.html">storybook-addon-markdown-docs</a> which uses mdjs under the hood.</p>
<h3 id="chrome-extension-currently-only-for-github"><a class="anchor" href="#chrome-extension-currently-only-for-github"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Chrome Extension (currently only for GitHub)</h3>
<p>See live demos directly inside GitHub pages, Markdown files, issues, pull requests...</p>
<p>Please check out <a href="https://github.com/open-wc/mdjs-viewer">mdjs-viewer</a>.</p>
<h2 id="build-mdjs"><a class="anchor" href="#build-mdjs"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Build mdjs</h2>
<h3 id="basic"><a class="anchor" href="#basic"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Basic</h3>
<p>mdjs offers two more "basic" integrations</p>
<h4 id="mdjsdocpage"><a class="anchor" href="#mdjsdocpage"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><code>mdjsDocPage</code></h4>
<p>Creates a full blown HTML page by passing in the Markdown.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> mdjsDocPage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@mdjs/core'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> page <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">mdjsDocPage</span><span class="token punctuation">(</span>markdownString<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/*
&#x3C;html>
... // load styles/components for mdjs, start stories
&#x3C;body>
&#x3C;h1>Some Markdown&#x3C;/h1>
&#x3C;/body>
&#x3C;/html>
*/</span>
</code></pre>
<h4 id="mdjsprocess"><a class="anchor" href="#mdjsprocess"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><code>mdjsProcess</code></h4>
<p>Pass in multiple Markdown documents and you get back all the JavaScript code and rendered HTML.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> mdjsProcess <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@mdjs/core'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">mdjsProcess</span><span class="token punctuation">(</span>markdownString<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">/*
{
jsCode: "
import '@mdjs/mdjs-story/mdjs-story.js';
...
",
html: '&#x3C;h1>Markdown One&#x3C;/h1>',
}
*/</span>
</code></pre>
<h3 id="advanced"><a class="anchor" href="#advanced"><svg class="octicon octicon-link" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Advanced</h3>
<p>mdjs is build to be integrated within the <a href="https://unifiedjs.com/">unifiedjs</a> system.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> unified <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'unified'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> markdown <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'remark-parse'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> htmlStringify <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'remark-html'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> mdjsParse <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@mdjs/core'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> parser <span class="token operator">=</span> <span class="token function">unified</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span>markdown<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span>mdjsParse<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span>htmlStringify<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> parser<span class="token punctuation">.</span><span class="token method function property-access">process</span><span class="token punctuation">(</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> jsCode <span class="token punctuation">}</span> <span class="token operator">=</span> result<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">;</span>
<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span><span class="token property-access">contents</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// &#x3C;h1>This is my-el>&#x3C;/h1></span>
<span class="token comment">// &#x3C;my-el>&#x3C;/my-el></span>
<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>jsCode<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// customElements.define('my-el', class extends HTMLElement {</span>
<span class="token comment">// ...</span>
</code></pre>
<script type="module">
import '@mdjs/mdjs-story/mdjs-story.js';
import '@mdjs/mdjs-preview/mdjs-preview.js';
import { html } from 'lit-html';
import 'demo-wc-card/demo-wc-card.js';
export const header = () => {
return html`<demo-wc-card .header=${'my new header'}></demo-wc-card>`;
};
const rootNode = document;
const stories = [{ key: 'header', story: header, code: `<pre class="language-js"><code class="language-js"><span class="token keyword module">import</span> <span class="token string">'demo-wc-card/demo-wc-card.js'</span><span class="token punctuation">;</span>
<span class="token keyword module">export</span> <span class="token keyword">const</span> <span class="token function-variable function">header</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 keyword">return</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>demo-wc-card</span> <span class="token attr-name">.header</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">\${</span><span class="token string">'my new header'</span><span class="token interpolation-punctuation punctuation">}</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>demo-wc-card</span><span class="token punctuation">></span></span> </span><span class="token template-punctuation string">\`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre>` }];
for (const story of stories) {
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'); }
</script>
<div class="content-footer">
<p>
Caught a mistake or want to contribute to the documentation?
<a href="https://github.com/modernweb-dev/rocket/edit/main/./docs/docs/markdown-javascript/overview.md">Edit this page on GitHub!</a>
</p>
</div>
</main>
</div>
</div>
<footer id="main-footer">
<div id="footer-menu">
<div class="content-area">
<nav>
<h3>Discover</h3>
<ul>
<li>
<a href="/blog/">Blog</a>
</li>
<li>
<a href="https://github.com/modernweb-dev/rocket/issues">Help and Feedback</a>
</li>
</ul>
</nav>
<nav>
<h3>Follow</h3>
<ul>
<li>
<a href="https://github.com/modernweb-dev/rocket">GitHub</a>
</li>
<li>
<a href="https://twitter.com/modern_web_dev">Twitter</a>
</li>
<li>
<a href="/about/slack/">Slack</a>
</li>
</ul>
</nav>
<nav>
<h3>Support</h3>
<ul>
<li>
<a href="/about/sponsor/">Sponsor</a>
</li>
<li>
<a href="https://github.com/modernweb-dev/rocket/blob/main/CONTRIBUTING.md">Contribute</a>
</li>
</ul>
</nav>
</div>
</div>
</footer>
<script type="module">
import '@rocket/navigation/rocket-navigation.js';
import '@rocket/drawer/rocket-drawer.js';
const drawer = document.querySelector('#sidebar');
// Toggle button
const triggers = document.querySelectorAll('[data-action="trigger-mobile-menu"]');
for (const trigger of [...triggers]) {
trigger.addEventListener('click', function () {
drawer.opened = true;
});
}
</script>
<script>
async function serviceWorkerUpdate() {
if ('serviceWorker' in navigator) {
const oldReg = await navigator.serviceWorker.getRegistration();
let oldSwState;
if (oldReg && oldReg.active) {
oldSwState = oldReg.active.state;
}
let refreshing;
navigator.serviceWorker.addEventListener('controllerchange', async () => {
if (refreshing) {
return;
}
const newReg = await navigator.serviceWorker.getRegistration();
let newSwState;
if (newReg && newReg.active) {
newSwState = newReg.active.state;
}
if (oldSwState === 'activated' && newSwState === 'activating') {
refreshing = true;
window.location.reload();
}
});
}
}
serviceWorkerUpdate();
</script>
</body>
</html>

View File

@@ -0,0 +1,4 @@
import { html, render } from 'lit-html';
import '../components/test-component.js';
render(html`<test-element></test-element>`, document.body);

View File

@@ -0,0 +1,24 @@
import chai from 'chai';
import { execute } from './test-helpers.js';
const { expect } = chai;
describe('inspectFolder', () => {
it.skip('can handle script src', async () => {
const { localSpecifiers, bareSpecifiers } = await execute('fixtures/script-src');
expect(localSpecifiers).lengthOf(1);
expect(bareSpecifiers).lengthOf(0);
});
it.skip('can handle script content', async () => {
const { localSpecifiers, bareSpecifiers } = await execute('fixtures/script-content');
expect(localSpecifiers).lengthOf(1);
expect(bareSpecifiers).lengthOf(0);
});
it('can handle multiple levels', async () => {
const { localSpecifiers, bareSpecifiers } = await execute('fixtures/test-case');
expect(localSpecifiers).lengthOf(3);
expect(bareSpecifiers).lengthOf(2);
});
});

View File

@@ -0,0 +1,11 @@
import path from 'path';
import { fileURLToPath } from 'url';
import { inspectFolder } from 'generate-import-map';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export async function execute(inPath) {
const testDir = path.join(__dirname, inPath.split('/').join(path.sep));
return await inspectFolder(testDir);
}

View File

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

View File

@@ -0,0 +1,10 @@
export interface Script {
specifier: string;
content: string;
filePath: string;
}
export interface Import {
s: number;
e: number;
}

View File

@@ -1,6 +1,13 @@
# @mdjs/mdjs-preview
## 0.3.1
### Patch Changes
- ee6b404: Pass on the shadowRoot to the story function
## 0.3.0
### Minor Changes
- 15e0abe: Clean up dependencies - add Types

View File

@@ -1,6 +1,6 @@
{
"name": "@mdjs/mdjs-preview",
"version": "0.3.0",
"version": "0.3.1",
"publishConfig": {
"access": "public"
},

View File

@@ -1,6 +1,19 @@
import { LitElement, html, css } from 'lit-element';
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
/**
* @typedef {object} StoryOptions
* @property {ShadowRoot | null} StoryOptions.shadowRoot
*/
/** @typedef {(options?: StoryOptions) => ReturnType<LitElement['render']>} LitHtmlStoryFn */
/**
* Renders a story within a preview frame
*
* @element mdjs-preview
* @prop {StoryFn} [story=(() => TemplateResult)] Function that returns the story
*/
export class MdJsPreview extends LitElement {
static get properties() {
return {
@@ -28,6 +41,7 @@ export class MdJsPreview extends LitElement {
constructor() {
super();
this.code = '';
/** @type {LitHtmlStoryFn} */
this.story = () => html` <p>Loading...</p> `;
this.codeHasHtml = false;
}
@@ -35,7 +49,7 @@ export class MdJsPreview extends LitElement {
render() {
return html`
<div id="wrapper">
<div>${this.story()}</div>
<div>${this.story({ shadowRoot: this.shadowRoot })}</div>
<button id="showCodeButton" @click=${this.toggleShowCode}>show code</button>
</div>
${this.codeHasHtml ? unsafeHTML(this.code) : html`<pre><code>${this.code}</code></pre>`}

View File

@@ -1,6 +1,13 @@
# @mdjs/mdjs-story
## 0.1.1
### Patch Changes
- ee6b404: Pass on the shadowRoot to the story function
## 0.1.0
### Minor Changes
- 15e0abe: Clean up dependencies - add Types

View File

@@ -1,6 +1,6 @@
{
"name": "@mdjs/mdjs-story",
"version": "0.1.0",
"version": "0.1.1",
"publishConfig": {
"access": "public"
},

View File

@@ -1,5 +1,18 @@
import { LitElement, html } from 'lit-element';
/**
* @typedef {object} StoryOptions
* @property {ShadowRoot | null} StoryOptions.shadowRoot
*/
/** @typedef {(options?: StoryOptions) => ReturnType<LitElement['render']>} LitHtmlStoryFn */
/**
* Renders a story
*
* @element mdjs-story
* @prop {StoryFn} [story=(() => TemplateResult)] Function that returns the story
*/
export class MdJsStory extends LitElement {
static get properties() {
return {
@@ -11,10 +24,11 @@ export class MdJsStory extends LitElement {
constructor() {
super();
this.story = () => html` <p>Loading...</p> `;
/** @type {LitHtmlStoryFn} */
this.story = () => html`<p>Loading...</p>`;
}
render() {
return this.story();
return this.story({ shadowRoot: this.shadowRoot });
}
}

View File

@@ -1474,6 +1474,11 @@
"@types/keygrip" "*"
"@types/node" "*"
"@types/es-module-lexer@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/es-module-lexer/-/es-module-lexer-0.3.0.tgz#9fee3f19f64e6b3f999eeb3a70bd177a4d57a6cb"
integrity sha512-XI3MGSejUQIJ3wzY0i5IHy5J3eb36M/ytgG8jIOssP08ovtRPcjpjXQqrx51AHBNBOisTS/NQNWJitI17+EwzQ==
"@types/estree@*":
version "0.0.45"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
@@ -1603,6 +1608,14 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44"
integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ==
"@types/node-fetch@^2.5.8":
version "2.5.8"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb"
integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "14.14.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b"
@@ -2294,6 +2307,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@@ -2946,6 +2964,13 @@ colorette@^1.2.1:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^1.0.0:
version "1.0.8"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
@@ -3386,6 +3411,11 @@ del@^2.2.0:
pinkie-promise "^2.0.0"
rimraf "^2.2.8"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
@@ -4092,6 +4122,15 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
form-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fresh@0.5.2, fresh@^0.5.2, fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -5808,6 +5847,18 @@ mime-db@1.44.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-db@1.45.0:
version "1.45.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
mime-types@^2.1.12:
version "2.1.28"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
dependencies:
mime-db "1.45.0"
mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
@@ -7401,6 +7452,11 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup-plugin-import-map@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-import-map/-/rollup-plugin-import-map-2.2.2.tgz#51100713ba379bf6bd077d090af9708f0201e9e7"
integrity sha512-y97Myu5kvuIoLrfSxRd90ytjv4wbOFCoWdi1pKVRfE8eOAcJqUOyPCLM4y3gw2u276dB/pLSFi48cl29SatXPw==
rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"