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 = {
|
||||
testDir: './tests',
|
||||
|
||||
timeout: 60000,
|
||||
|
||||
reporter: [['list'], ['@argos-ci/playwright/reporter']],
|
||||
|
||||
// Run website production built
|
||||
|
|
|
@ -36,8 +36,12 @@ function isBlacklisted(pathname: string) {
|
|||
}
|
||||
// Some paths explicitly blacklisted
|
||||
const BlacklistedPathnames: string[] = [
|
||||
'/feature-requests', // Flaky because of Canny widget
|
||||
'/community/canary', // Flaky because of dynamic canary version fetched from npm
|
||||
// Flaky because of Canny widget
|
||||
'/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 (
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
parseFrontMatter,
|
||||
escapePath,
|
||||
getFileLoaderUtils,
|
||||
getWebpackLoaderCompilerName,
|
||||
} from '@docusaurus/utils';
|
||||
import stringifyObject from 'stringify-object';
|
||||
import preprocessor from './preprocessor';
|
||||
|
@ -134,10 +135,12 @@ export async function mdxLoader(
|
|||
this: LoaderContext<Options>,
|
||||
fileString: string,
|
||||
): Promise<void> {
|
||||
const compilerName = getWebpackLoaderCompilerName(this);
|
||||
const callback = this.async();
|
||||
const filePath = this.resourcePath;
|
||||
const reqOptions: Options = this.getOptions();
|
||||
const {query} = this;
|
||||
|
||||
ensureMarkdownConfig(reqOptions);
|
||||
|
||||
const {frontMatter} = parseFrontMatter(fileString);
|
||||
|
@ -165,6 +168,7 @@ export async function mdxLoader(
|
|||
content: preprocessedContent,
|
||||
filePath,
|
||||
frontMatter,
|
||||
compilerName,
|
||||
});
|
||||
} catch (errorUnknown) {
|
||||
const error = errorUnknown as Error;
|
||||
|
|
|
@ -15,8 +15,10 @@ import details from './remark/details';
|
|||
import head from './remark/head';
|
||||
import mermaid from './remark/mermaid';
|
||||
import transformAdmonitions from './remark/admonitions';
|
||||
import unusedDirectivesWarning from './remark/unusedDirectives';
|
||||
import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin';
|
||||
import {getFormat} from './format';
|
||||
import type {WebpackCompilerName} from '@docusaurus/utils';
|
||||
import type {MDXFrontMatter} from './frontMatter';
|
||||
import type {Options} from './loader';
|
||||
import type {AdmonitionOptions} from './remark/admonitions';
|
||||
|
@ -37,10 +39,12 @@ type SimpleProcessor = {
|
|||
content,
|
||||
filePath,
|
||||
frontMatter,
|
||||
compilerName,
|
||||
}: {
|
||||
content: string;
|
||||
filePath: string;
|
||||
frontMatter: {[key: string]: unknown};
|
||||
compilerName: WebpackCompilerName;
|
||||
}) => Promise<SimpleProcessorResult>;
|
||||
};
|
||||
|
||||
|
@ -123,6 +127,7 @@ async function createProcessorFactory() {
|
|||
gfm,
|
||||
options.markdownConfig.mdx1Compat.comments ? comment : null,
|
||||
...(options.remarkPlugins ?? []),
|
||||
unusedDirectivesWarning,
|
||||
].filter((plugin): plugin is MDXPlugin => Boolean(plugin));
|
||||
|
||||
// codeCompatPlugin needs to be applied last after user-provided plugins
|
||||
|
@ -167,12 +172,13 @@ async function createProcessorFactory() {
|
|||
});
|
||||
|
||||
return {
|
||||
process: async ({content, filePath, frontMatter}) => {
|
||||
process: async ({content, filePath, frontMatter, compilerName}) => {
|
||||
const vfile = new VFile({
|
||||
value: content,
|
||||
path: filePath,
|
||||
data: {
|
||||
frontMatter,
|
||||
compilerName,
|
||||
},
|
||||
});
|
||||
return mdxProcessor.process(vfile).then((result) => ({
|
||||
|
|
|
@ -35,6 +35,7 @@ const plugin: Plugin = function plugin(
|
|||
const {toString} = await import('mdast-util-to-string');
|
||||
visit(root, 'heading', (headingNode: Heading, index, parent) => {
|
||||
if (headingNode.depth === 1) {
|
||||
vfile.data.compilerName;
|
||||
vfile.data.contentTitle = toString(headingNode);
|
||||
if (removeContentTitle) {
|
||||
parent!.children.splice(index, 1);
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import visit from 'unist-util-visit';
|
||||
import {transformNode} from '../utils';
|
||||
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
import type {Code} from 'mdast';
|
||||
|
@ -16,10 +18,10 @@ import type {Code} from 'mdast';
|
|||
// by theme-mermaid itself
|
||||
export default function plugin(): Transformer {
|
||||
return (root) => {
|
||||
visit(root, 'code', (node: Code, index, parent) => {
|
||||
visit(root, 'code', (node: Code) => {
|
||||
if (node.lang === 'mermaid') {
|
||||
// TODO migrate to mdxJsxFlowElement? cf admonitions
|
||||
parent!.children.splice(index, 1, {
|
||||
transformNode(node, {
|
||||
type: 'mermaidCodeBlock',
|
||||
data: {
|
||||
hName: 'mermaid',
|
||||
|
|
|
@ -20,7 +20,7 @@ import visit from 'unist-util-visit';
|
|||
import escapeHtml from 'escape-html';
|
||||
import sizeOf from 'image-size';
|
||||
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
|
||||
import type {Transformer} from 'unified';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
|
@ -110,14 +110,12 @@ ${(err as Error).message}`;
|
|||
}
|
||||
}
|
||||
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
jsxNode.type = 'mdxJsxTextElement';
|
||||
jsxNode.name = 'img';
|
||||
jsxNode.attributes = attributes;
|
||||
jsxNode.children = [];
|
||||
transformNode(jsxNode, {
|
||||
type: 'mdxJsxTextElement',
|
||||
name: 'img',
|
||||
attributes,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '@docusaurus/utils';
|
||||
import visit from 'unist-util-visit';
|
||||
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
|
||||
import type {Transformer} from 'unified';
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
|
@ -90,14 +90,12 @@ async function toAssetRequireNode(
|
|||
|
||||
const {children} = node;
|
||||
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
jsxNode.type = 'mdxJsxTextElement';
|
||||
jsxNode.name = 'a';
|
||||
jsxNode.attributes = attributes;
|
||||
jsxNode.children = children;
|
||||
transformNode(jsxNode, {
|
||||
type: 'mdxJsxTextElement',
|
||||
name: 'a',
|
||||
attributes,
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
||||
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 type {Parent} from 'unist';
|
||||
import type {Parent, Node} from 'unist';
|
||||
import type {PhrasingContent, Heading} from 'mdast';
|
||||
import type {
|
||||
MdxJsxAttribute,
|
||||
|
@ -15,6 +15,27 @@ import type {
|
|||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
} 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(
|
||||
node: Parent,
|
||||
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,
|
||||
createAbsoluteFilePathMatcher,
|
||||
} from './globUtils';
|
||||
export {getFileLoaderUtils} from './webpackUtils';
|
||||
export {
|
||||
getFileLoaderUtils,
|
||||
getWebpackLoaderCompilerName,
|
||||
type WebpackCompilerName,
|
||||
} from './webpackUtils';
|
||||
export {escapeShellArg} from './shellUtils';
|
||||
export {loadFreshModule} from './moduleUtils';
|
||||
export {
|
||||
|
|
|
@ -11,7 +11,25 @@ import {
|
|||
WEBPACK_URL_LOADER_LIMIT,
|
||||
OUTPUT_STATIC_ASSETS_DIR_NAME,
|
||||
} 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';
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ customizability
|
|||
dabit
|
||||
daishi
|
||||
datagit
|
||||
datamap
|
||||
datas
|
||||
dbaeumer
|
||||
décembre
|
||||
|
|
Loading…
Add table
Reference in a new issue