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:
ozaki 2023-10-24 16:15:49 +02:00 committed by GitHub
parent 56cc8e8ffa
commit c6762a2542
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 506 additions and 26 deletions

View file

@ -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

View file

@ -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 (

View file

@ -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;

View file

@ -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) => ({

View file

@ -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);

View file

@ -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',

View file

@ -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) {

View file

@ -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) {

View file

@ -0,0 +1,19 @@
:::danger
Take care of snowstorms...
:::
:::unusedDirective
unused directive content
:::
:::NotAContainerDirective with a phrase after
:::
Phrase before :::NotAContainerDirective
:::

View file

@ -0,0 +1,5 @@
::unusedLeafDirective
Leaf directive in a phrase ::NotALeafDirective
::NotALeafDirective with a phrase after

View file

@ -0,0 +1,17 @@
Simple: textDirective1
```sh
Simple: textDirectiveCode
```
Simple:textDirective2
Simple:textDirective3[label]
Simple:textDirective4{age=42}
Simple:textDirective5
```sh
Simple:textDirectiveCode
```

View file

@ -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>"
`;

View file

@ -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');
});
});

View file

@ -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;

View file

@ -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

View 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;
}
}

View file

@ -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 {

View file

@ -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';

View file

@ -62,6 +62,7 @@ customizability
dabit dabit
daishi daishi
datagit datagit
datamap
datas datas
dbaeumer dbaeumer
décembre décembre