mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
feat(mdx-loader): Remark plugin to report unused MDX / Markdown directives (#9394)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
56cc8e8ffa
commit
c6762a2542
19 changed files with 506 additions and 26 deletions
|
@ -14,6 +14,8 @@ import type {PlaywrightTestConfig} from '@playwright/test';
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
|
|
||||||
|
timeout: 60000,
|
||||||
|
|
||||||
reporter: [['list'], ['@argos-ci/playwright/reporter']],
|
reporter: [['list'], ['@argos-ci/playwright/reporter']],
|
||||||
|
|
||||||
// Run website production built
|
// Run website production built
|
||||||
|
|
|
@ -36,8 +36,12 @@ function isBlacklisted(pathname: string) {
|
||||||
}
|
}
|
||||||
// Some paths explicitly blacklisted
|
// Some paths explicitly blacklisted
|
||||||
const BlacklistedPathnames: string[] = [
|
const BlacklistedPathnames: string[] = [
|
||||||
'/feature-requests', // Flaky because of Canny widget
|
// Flaky because of Canny widget
|
||||||
'/community/canary', // Flaky because of dynamic canary version fetched from npm
|
'/feature-requests',
|
||||||
|
// Flaky because of dynamic canary version fetched from npm
|
||||||
|
'/community/canary',
|
||||||
|
// Long blog post with many image carousels, often timeouts
|
||||||
|
'/blog/2022/08/01/announcing-docusaurus-2.0',
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
parseFrontMatter,
|
parseFrontMatter,
|
||||||
escapePath,
|
escapePath,
|
||||||
getFileLoaderUtils,
|
getFileLoaderUtils,
|
||||||
|
getWebpackLoaderCompilerName,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import stringifyObject from 'stringify-object';
|
import stringifyObject from 'stringify-object';
|
||||||
import preprocessor from './preprocessor';
|
import preprocessor from './preprocessor';
|
||||||
|
@ -134,10 +135,12 @@ export async function mdxLoader(
|
||||||
this: LoaderContext<Options>,
|
this: LoaderContext<Options>,
|
||||||
fileString: string,
|
fileString: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const compilerName = getWebpackLoaderCompilerName(this);
|
||||||
const callback = this.async();
|
const callback = this.async();
|
||||||
const filePath = this.resourcePath;
|
const filePath = this.resourcePath;
|
||||||
const reqOptions: Options = this.getOptions();
|
const reqOptions: Options = this.getOptions();
|
||||||
const {query} = this;
|
const {query} = this;
|
||||||
|
|
||||||
ensureMarkdownConfig(reqOptions);
|
ensureMarkdownConfig(reqOptions);
|
||||||
|
|
||||||
const {frontMatter} = parseFrontMatter(fileString);
|
const {frontMatter} = parseFrontMatter(fileString);
|
||||||
|
@ -165,6 +168,7 @@ export async function mdxLoader(
|
||||||
content: preprocessedContent,
|
content: preprocessedContent,
|
||||||
filePath,
|
filePath,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
|
compilerName,
|
||||||
});
|
});
|
||||||
} catch (errorUnknown) {
|
} catch (errorUnknown) {
|
||||||
const error = errorUnknown as Error;
|
const error = errorUnknown as Error;
|
||||||
|
|
|
@ -15,8 +15,10 @@ import details from './remark/details';
|
||||||
import head from './remark/head';
|
import head from './remark/head';
|
||||||
import mermaid from './remark/mermaid';
|
import mermaid from './remark/mermaid';
|
||||||
import transformAdmonitions from './remark/admonitions';
|
import transformAdmonitions from './remark/admonitions';
|
||||||
|
import unusedDirectivesWarning from './remark/unusedDirectives';
|
||||||
import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin';
|
import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin';
|
||||||
import {getFormat} from './format';
|
import {getFormat} from './format';
|
||||||
|
import type {WebpackCompilerName} from '@docusaurus/utils';
|
||||||
import type {MDXFrontMatter} from './frontMatter';
|
import type {MDXFrontMatter} from './frontMatter';
|
||||||
import type {Options} from './loader';
|
import type {Options} from './loader';
|
||||||
import type {AdmonitionOptions} from './remark/admonitions';
|
import type {AdmonitionOptions} from './remark/admonitions';
|
||||||
|
@ -37,10 +39,12 @@ type SimpleProcessor = {
|
||||||
content,
|
content,
|
||||||
filePath,
|
filePath,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
|
compilerName,
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
frontMatter: {[key: string]: unknown};
|
frontMatter: {[key: string]: unknown};
|
||||||
|
compilerName: WebpackCompilerName;
|
||||||
}) => Promise<SimpleProcessorResult>;
|
}) => Promise<SimpleProcessorResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,6 +127,7 @@ async function createProcessorFactory() {
|
||||||
gfm,
|
gfm,
|
||||||
options.markdownConfig.mdx1Compat.comments ? comment : null,
|
options.markdownConfig.mdx1Compat.comments ? comment : null,
|
||||||
...(options.remarkPlugins ?? []),
|
...(options.remarkPlugins ?? []),
|
||||||
|
unusedDirectivesWarning,
|
||||||
].filter((plugin): plugin is MDXPlugin => Boolean(plugin));
|
].filter((plugin): plugin is MDXPlugin => Boolean(plugin));
|
||||||
|
|
||||||
// codeCompatPlugin needs to be applied last after user-provided plugins
|
// codeCompatPlugin needs to be applied last after user-provided plugins
|
||||||
|
@ -167,12 +172,13 @@ async function createProcessorFactory() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
process: async ({content, filePath, frontMatter}) => {
|
process: async ({content, filePath, frontMatter, compilerName}) => {
|
||||||
const vfile = new VFile({
|
const vfile = new VFile({
|
||||||
value: content,
|
value: content,
|
||||||
path: filePath,
|
path: filePath,
|
||||||
data: {
|
data: {
|
||||||
frontMatter,
|
frontMatter,
|
||||||
|
compilerName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return mdxProcessor.process(vfile).then((result) => ({
|
return mdxProcessor.process(vfile).then((result) => ({
|
||||||
|
|
|
@ -35,6 +35,7 @@ const plugin: Plugin = function plugin(
|
||||||
const {toString} = await import('mdast-util-to-string');
|
const {toString} = await import('mdast-util-to-string');
|
||||||
visit(root, 'heading', (headingNode: Heading, index, parent) => {
|
visit(root, 'heading', (headingNode: Heading, index, parent) => {
|
||||||
if (headingNode.depth === 1) {
|
if (headingNode.depth === 1) {
|
||||||
|
vfile.data.compilerName;
|
||||||
vfile.data.contentTitle = toString(headingNode);
|
vfile.data.contentTitle = toString(headingNode);
|
||||||
if (removeContentTitle) {
|
if (removeContentTitle) {
|
||||||
parent!.children.splice(index, 1);
|
parent!.children.splice(index, 1);
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import visit from 'unist-util-visit';
|
import visit from 'unist-util-visit';
|
||||||
|
import {transformNode} from '../utils';
|
||||||
|
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
import type {Transformer} from 'unified';
|
import type {Transformer} from 'unified';
|
||||||
import type {Code} from 'mdast';
|
import type {Code} from 'mdast';
|
||||||
|
@ -16,10 +18,10 @@ import type {Code} from 'mdast';
|
||||||
// by theme-mermaid itself
|
// by theme-mermaid itself
|
||||||
export default function plugin(): Transformer {
|
export default function plugin(): Transformer {
|
||||||
return (root) => {
|
return (root) => {
|
||||||
visit(root, 'code', (node: Code, index, parent) => {
|
visit(root, 'code', (node: Code) => {
|
||||||
if (node.lang === 'mermaid') {
|
if (node.lang === 'mermaid') {
|
||||||
// TODO migrate to mdxJsxFlowElement? cf admonitions
|
// TODO migrate to mdxJsxFlowElement? cf admonitions
|
||||||
parent!.children.splice(index, 1, {
|
transformNode(node, {
|
||||||
type: 'mermaidCodeBlock',
|
type: 'mermaidCodeBlock',
|
||||||
data: {
|
data: {
|
||||||
hName: 'mermaid',
|
hName: 'mermaid',
|
||||||
|
|
|
@ -20,7 +20,7 @@ import visit from 'unist-util-visit';
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import sizeOf from 'image-size';
|
import sizeOf from 'image-size';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {assetRequireAttributeValue} from '../utils';
|
import {assetRequireAttributeValue, transformNode} from '../utils';
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
import type {Transformer} from 'unified';
|
import type {Transformer} from 'unified';
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
|
@ -110,14 +110,12 @@ ${(err as Error).message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(jsxNode).forEach(
|
transformNode(jsxNode, {
|
||||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
type: 'mdxJsxTextElement',
|
||||||
);
|
name: 'img',
|
||||||
|
attributes,
|
||||||
jsxNode.type = 'mdxJsxTextElement';
|
children: [],
|
||||||
jsxNode.name = 'img';
|
});
|
||||||
jsxNode.attributes = attributes;
|
|
||||||
jsxNode.children = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import visit from 'unist-util-visit';
|
import visit from 'unist-util-visit';
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import {assetRequireAttributeValue} from '../utils';
|
import {assetRequireAttributeValue, transformNode} from '../utils';
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
import type {Transformer} from 'unified';
|
import type {Transformer} from 'unified';
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
|
@ -90,14 +90,12 @@ async function toAssetRequireNode(
|
||||||
|
|
||||||
const {children} = node;
|
const {children} = node;
|
||||||
|
|
||||||
Object.keys(jsxNode).forEach(
|
transformNode(jsxNode, {
|
||||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
type: 'mdxJsxTextElement',
|
||||||
);
|
name: 'a',
|
||||||
|
attributes,
|
||||||
jsxNode.type = 'mdxJsxTextElement';
|
children,
|
||||||
jsxNode.name = 'a';
|
});
|
||||||
jsxNode.attributes = attributes;
|
|
||||||
jsxNode.children = children;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
|
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
:::danger
|
||||||
|
|
||||||
|
Take care of snowstorms...
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::unusedDirective
|
||||||
|
|
||||||
|
unused directive content
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::NotAContainerDirective with a phrase after
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Phrase before :::NotAContainerDirective
|
||||||
|
|
||||||
|
:::
|
|
@ -0,0 +1,5 @@
|
||||||
|
::unusedLeafDirective
|
||||||
|
|
||||||
|
Leaf directive in a phrase ::NotALeafDirective
|
||||||
|
|
||||||
|
::NotALeafDirective with a phrase after
|
17
packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/textDirectives.md
generated
Normal file
17
packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/textDirectives.md
generated
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
Simple: textDirective1
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Simple: textDirectiveCode
|
||||||
|
```
|
||||||
|
|
||||||
|
Simple:textDirective2
|
||||||
|
|
||||||
|
Simple:textDirective3[label]
|
||||||
|
|
||||||
|
Simple:textDirective4{age=42}
|
||||||
|
|
||||||
|
Simple:textDirective5
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Simple:textDirectiveCode
|
||||||
|
```
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for container directives: console 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Docusaurus found 1 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/containerDirectives.md"
|
||||||
|
- :::unusedDirective (7:1)
|
||||||
|
Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for container directives: result 1`] = `
|
||||||
|
"<admonition type="danger"><p>Take care of snowstorms...</p></admonition>
|
||||||
|
<div><p>unused directive content</p></div>
|
||||||
|
<p>:::NotAContainerDirective with a phrase after</p>
|
||||||
|
<p>:::</p>
|
||||||
|
<p>Phrase before :::NotAContainerDirective</p>
|
||||||
|
<p>:::</p>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for leaf directives: console 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Docusaurus found 1 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/leafDirectives.md"
|
||||||
|
- ::unusedLeafDirective (1:1)
|
||||||
|
Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for leaf directives: result 1`] = `
|
||||||
|
"<div></div>
|
||||||
|
<p>Leaf directive in a phrase ::NotALeafDirective</p>
|
||||||
|
<p>::NotALeafDirective with a phrase after</p>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for text directives: console 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Docusaurus found 2 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/textDirectives.md"
|
||||||
|
- :textDirective3 (9:7)
|
||||||
|
- :textDirective4 (11:7)
|
||||||
|
Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - client compiler default behavior for text directives: result 1`] = `
|
||||||
|
"<p>Simple: textDirective1</p>
|
||||||
|
<pre><code class="language-sh">Simple: textDirectiveCode
|
||||||
|
</code></pre>
|
||||||
|
<p>Simple:textDirective2</p>
|
||||||
|
<p>Simple<div>label</div></p>
|
||||||
|
<p>Simple<div></div></p>
|
||||||
|
<p>Simple:textDirective5</p>
|
||||||
|
<pre><code class="language-sh">Simple:textDirectiveCode
|
||||||
|
</code></pre>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - server compiler default behavior for container directives: result 1`] = `
|
||||||
|
"<admonition type="danger"><p>Take care of snowstorms...</p></admonition>
|
||||||
|
<div><p>unused directive content</p></div>
|
||||||
|
<p>:::NotAContainerDirective with a phrase after</p>
|
||||||
|
<p>:::</p>
|
||||||
|
<p>Phrase before :::NotAContainerDirective</p>
|
||||||
|
<p>:::</p>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - server compiler default behavior for leaf directives: result 1`] = `
|
||||||
|
"<div></div>
|
||||||
|
<p>Leaf directive in a phrase ::NotALeafDirective</p>
|
||||||
|
<p>::NotALeafDirective with a phrase after</p>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`directives remark plugin - server compiler default behavior for text directives: result 1`] = `
|
||||||
|
"<p>Simple: textDirective1</p>
|
||||||
|
<pre><code class="language-sh">Simple: textDirectiveCode
|
||||||
|
</code></pre>
|
||||||
|
<p>Simple:textDirective2</p>
|
||||||
|
<p>Simple<div>label</div></p>
|
||||||
|
<p>Simple<div></div></p>
|
||||||
|
<p>Simple:textDirective5</p>
|
||||||
|
<pre><code class="language-sh">Simple:textDirectiveCode
|
||||||
|
</code></pre>"
|
||||||
|
`;
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import remark2rehype from 'remark-rehype';
|
||||||
|
import stringify from 'rehype-stringify';
|
||||||
|
import vfile from 'to-vfile';
|
||||||
|
import plugin from '../index';
|
||||||
|
import admonition from '../../admonitions';
|
||||||
|
import type {WebpackCompilerName} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
const processFixture = async (
|
||||||
|
name: string,
|
||||||
|
{compilerName}: {compilerName: WebpackCompilerName},
|
||||||
|
) => {
|
||||||
|
const {remark} = await import('remark');
|
||||||
|
const {default: directives} = await import('remark-directive');
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
|
||||||
|
const file = await vfile.read(filePath);
|
||||||
|
file.data.compilerName = compilerName;
|
||||||
|
|
||||||
|
const result = await remark()
|
||||||
|
.use(directives)
|
||||||
|
.use(admonition)
|
||||||
|
.use(plugin)
|
||||||
|
.use(remark2rehype)
|
||||||
|
.use(stringify)
|
||||||
|
.process(file);
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('directives remark plugin - client compiler', () => {
|
||||||
|
const consoleMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
const options = {compilerName: 'client'} as const;
|
||||||
|
|
||||||
|
it('default behavior for container directives', async () => {
|
||||||
|
const result = await processFixture('containerDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleMock.mock.calls).toMatchSnapshot('console');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default behavior for leaf directives', async () => {
|
||||||
|
const result = await processFixture('leafDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleMock.mock.calls).toMatchSnapshot('console');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default behavior for text directives', async () => {
|
||||||
|
const result = await processFixture('textDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleMock.mock.calls).toMatchSnapshot('console');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('directives remark plugin - server compiler', () => {
|
||||||
|
const consoleMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
const options = {compilerName: 'server'} as const;
|
||||||
|
|
||||||
|
it('default behavior for container directives', async () => {
|
||||||
|
const result = await processFixture('containerDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default behavior for leaf directives', async () => {
|
||||||
|
const result = await processFixture('leafDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default behavior for text directives', async () => {
|
||||||
|
const result = await processFixture('textDirectives', options);
|
||||||
|
expect(result).toMatchSnapshot('result');
|
||||||
|
expect(consoleMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('directives remark plugin - client result === server result', () => {
|
||||||
|
// It is important that client/server outputs are exactly the same
|
||||||
|
// otherwise React hydration mismatches can occur
|
||||||
|
async function testSameResult(name: string) {
|
||||||
|
const resultClient = await processFixture(name, {compilerName: 'client'});
|
||||||
|
const resultServer = await processFixture(name, {compilerName: 'server'});
|
||||||
|
expect(resultClient).toEqual(resultServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('for containerDirectives', async () => {
|
||||||
|
await testSameResult('containerDirectives');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for leafDirectives', async () => {
|
||||||
|
await testSameResult('leafDirectives');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for textDirectives', async () => {
|
||||||
|
await testSameResult('textDirectives');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
import path from 'path';
|
||||||
|
import process from 'process';
|
||||||
|
import visit from 'unist-util-visit';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
import {posixPath} from '@docusaurus/utils';
|
||||||
|
import {transformNode} from '../utils';
|
||||||
|
|
||||||
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
|
import type {Transformer, Processor, Parent} from 'unified';
|
||||||
|
import type {
|
||||||
|
Directive,
|
||||||
|
TextDirective,
|
||||||
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
|
} from 'mdast-util-directive';
|
||||||
|
|
||||||
|
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
|
||||||
|
// This might change soon, likely after TS 5.2
|
||||||
|
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
|
||||||
|
// import type {Plugin} from 'unified';
|
||||||
|
type Plugin = any; // TODO fix this asap
|
||||||
|
|
||||||
|
type DirectiveType = Directive['type'];
|
||||||
|
|
||||||
|
const directiveTypes: DirectiveType[] = [
|
||||||
|
'containerDirective',
|
||||||
|
'leafDirective',
|
||||||
|
'textDirective',
|
||||||
|
];
|
||||||
|
|
||||||
|
const directivePrefixMap: {[key in DirectiveType]: string} = {
|
||||||
|
textDirective: ':',
|
||||||
|
leafDirective: '::',
|
||||||
|
containerDirective: ':::',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDirectiveName(directive: Directive) {
|
||||||
|
const prefix = directivePrefixMap[directive.type];
|
||||||
|
if (!prefix) {
|
||||||
|
throw new Error(
|
||||||
|
`unexpected, no prefix found for directive of type ${directive.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// To simplify we don't display the eventual label/props of directives
|
||||||
|
return `${prefix}${directive.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDirectivePosition(directive: Directive): string | undefined {
|
||||||
|
return directive.position?.start
|
||||||
|
? logger.interpolate`number=${directive.position.start.line}:number=${directive.position.start.column}`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnusedDirectiveMessage(directive: Directive) {
|
||||||
|
const name = formatDirectiveName(directive);
|
||||||
|
const position = formatDirectivePosition(directive);
|
||||||
|
|
||||||
|
return `- ${name} ${position ? `(${position})` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnusedDirectivesMessage({
|
||||||
|
directives,
|
||||||
|
filePath,
|
||||||
|
}: {
|
||||||
|
directives: Directive[];
|
||||||
|
filePath: string;
|
||||||
|
}): string {
|
||||||
|
const supportUrl = 'https://github.com/facebook/docusaurus/pull/9394';
|
||||||
|
const customPath = posixPath(path.relative(process.cwd(), filePath));
|
||||||
|
const warningTitle = logger.interpolate`Docusaurus found ${directives.length} unused Markdown directives in file path=${customPath}`;
|
||||||
|
const customSupportUrl = logger.interpolate`url=${supportUrl}`;
|
||||||
|
const warningMessages = directives
|
||||||
|
.map(formatUnusedDirectiveMessage)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `${warningTitle}
|
||||||
|
${warningMessages}
|
||||||
|
Your content might render in an unexpected way. Visit ${customSupportUrl} to find out why and how to fix it.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logUnusedDirectivesWarning({
|
||||||
|
directives,
|
||||||
|
filePath,
|
||||||
|
}: {
|
||||||
|
directives: Directive[];
|
||||||
|
filePath: string;
|
||||||
|
}) {
|
||||||
|
if (directives.length > 0) {
|
||||||
|
const message = formatUnusedDirectivesMessage({
|
||||||
|
directives,
|
||||||
|
filePath,
|
||||||
|
});
|
||||||
|
logger.warn(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTextDirective(directive: Directive): directive is TextDirective {
|
||||||
|
return directive.type === 'textDirective';
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple text directive is one without any label/props
|
||||||
|
function isSimpleTextDirective(
|
||||||
|
directive: Directive,
|
||||||
|
): directive is TextDirective {
|
||||||
|
if (isTextDirective(directive)) {
|
||||||
|
// Attributes in MDAST = Directive props
|
||||||
|
const hasAttributes =
|
||||||
|
directive.attributes && Object.keys(directive.attributes).length > 0;
|
||||||
|
// Children in MDAST = Directive label
|
||||||
|
const hasChildren = directive.children.length > 0;
|
||||||
|
return !hasAttributes && !hasChildren;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformSimpleTextDirectiveToString(textDirective: Directive) {
|
||||||
|
transformNode(textDirective, {
|
||||||
|
type: 'text',
|
||||||
|
value: `:${textDirective.name}`, // We ignore label/props on purpose here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnusedDirective(directive: Directive) {
|
||||||
|
// If directive data is set (notably hName/hProperties set by admonitions)
|
||||||
|
// this usually means the directive has been handled by another plugin
|
||||||
|
return !directive.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: Plugin = function plugin(this: Processor): Transformer {
|
||||||
|
return (tree, file) => {
|
||||||
|
const unusedDirectives: Directive[] = [];
|
||||||
|
|
||||||
|
visit<Parent>(tree, directiveTypes, (directive: Directive) => {
|
||||||
|
// If directive data is set (notably hName/hProperties set by admonitions)
|
||||||
|
// this usually means the directive has been handled by another plugin
|
||||||
|
if (isUnusedDirective(directive)) {
|
||||||
|
if (isSimpleTextDirective(directive)) {
|
||||||
|
transformSimpleTextDirectiveToString(directive);
|
||||||
|
} else {
|
||||||
|
unusedDirectives.push(directive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only enable these warnings for the client compiler
|
||||||
|
// This avoids emitting duplicate warnings in prod mode
|
||||||
|
// Note: the client compiler is used in both dev/prod modes
|
||||||
|
if (file.data.compilerName === 'client') {
|
||||||
|
logUnusedDirectivesWarning({
|
||||||
|
directives: unusedDirectives,
|
||||||
|
filePath: file.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import type {Parent} from 'unist';
|
import type {Parent, Node} from 'unist';
|
||||||
import type {PhrasingContent, Heading} from 'mdast';
|
import type {PhrasingContent, Heading} from 'mdast';
|
||||||
import type {
|
import type {
|
||||||
MdxJsxAttribute,
|
MdxJsxAttribute,
|
||||||
|
@ -15,6 +15,27 @@ import type {
|
||||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||||
} from 'mdast-util-mdx';
|
} from 'mdast-util-mdx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util to transform one node type to another node type
|
||||||
|
* The input node is mutated in place
|
||||||
|
* @param node the node to mutate
|
||||||
|
* @param newNode what the original node should become become
|
||||||
|
*/
|
||||||
|
export function transformNode<NewNode extends Node>(
|
||||||
|
node: Node,
|
||||||
|
newNode: NewNode,
|
||||||
|
): NewNode {
|
||||||
|
Object.keys(node).forEach((key) => {
|
||||||
|
// @ts-expect-error: unsafe but ok
|
||||||
|
delete node[key];
|
||||||
|
});
|
||||||
|
Object.keys(newNode).forEach((key) => {
|
||||||
|
// @ts-expect-error: unsafe but ok
|
||||||
|
node[key] = newNode[key];
|
||||||
|
});
|
||||||
|
return node as NewNode;
|
||||||
|
}
|
||||||
|
|
||||||
export function stringifyContent(
|
export function stringifyContent(
|
||||||
node: Parent,
|
node: Parent,
|
||||||
toString: (param: unknown) => string, // TODO weird but works
|
toString: (param: unknown) => string, // TODO weird but works
|
||||||
|
|
21
packages/docusaurus-mdx-loader/src/vfile-datamap.d.mts
Normal file
21
packages/docusaurus-mdx-loader/src/vfile-datamap.d.mts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {WebpackCompilerName} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
declare module 'vfile' {
|
||||||
|
/*
|
||||||
|
This map registers the type of the data key of a VFile (TypeScript type).
|
||||||
|
This type can be augmented to register custom data types.
|
||||||
|
See https://github.com/vfile/vfile#datamap
|
||||||
|
*/
|
||||||
|
interface DataMap {
|
||||||
|
frontMatter: {[key: string]: unknown};
|
||||||
|
compilerName: WebpackCompilerName;
|
||||||
|
contentTitle?: string;
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,7 +98,11 @@ export {
|
||||||
createMatcher,
|
createMatcher,
|
||||||
createAbsoluteFilePathMatcher,
|
createAbsoluteFilePathMatcher,
|
||||||
} from './globUtils';
|
} from './globUtils';
|
||||||
export {getFileLoaderUtils} from './webpackUtils';
|
export {
|
||||||
|
getFileLoaderUtils,
|
||||||
|
getWebpackLoaderCompilerName,
|
||||||
|
type WebpackCompilerName,
|
||||||
|
} from './webpackUtils';
|
||||||
export {escapeShellArg} from './shellUtils';
|
export {escapeShellArg} from './shellUtils';
|
||||||
export {loadFreshModule} from './moduleUtils';
|
export {loadFreshModule} from './moduleUtils';
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -11,7 +11,25 @@ import {
|
||||||
WEBPACK_URL_LOADER_LIMIT,
|
WEBPACK_URL_LOADER_LIMIT,
|
||||||
OUTPUT_STATIC_ASSETS_DIR_NAME,
|
OUTPUT_STATIC_ASSETS_DIR_NAME,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import type {RuleSetRule} from 'webpack';
|
import type {RuleSetRule, LoaderContext} from 'webpack';
|
||||||
|
|
||||||
|
export type WebpackCompilerName = 'server' | 'client';
|
||||||
|
|
||||||
|
export function getWebpackLoaderCompilerName(
|
||||||
|
context: LoaderContext<unknown>,
|
||||||
|
): WebpackCompilerName {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
const compilerName = context._compiler?.name;
|
||||||
|
switch (compilerName) {
|
||||||
|
case 'server':
|
||||||
|
case 'client':
|
||||||
|
return compilerName;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Cannot get valid Docusaurus webpack compiler name. Found compilerName=${compilerName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AssetFolder = 'images' | 'files' | 'fonts' | 'medias';
|
type AssetFolder = 'images' | 'files' | 'fonts' | 'medias';
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ customizability
|
||||||
dabit
|
dabit
|
||||||
daishi
|
daishi
|
||||||
datagit
|
datagit
|
||||||
|
datamap
|
||||||
datas
|
datas
|
||||||
dbaeumer
|
dbaeumer
|
||||||
décembre
|
décembre
|
||||||
|
|
Loading…
Add table
Reference in a new issue