@docusaurus/mdx-loader 2.0.0-beta.16 → 2.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/lib/index.d.ts +25 -16
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +2 -169
  4. package/lib/index.js.map +1 -1
  5. package/lib/loader.d.ts +39 -0
  6. package/lib/loader.d.ts.map +1 -0
  7. package/lib/loader.js +194 -0
  8. package/lib/loader.js.map +1 -0
  9. package/lib/remark/headings/index.d.ts.map +1 -1
  10. package/lib/remark/headings/index.js +3 -5
  11. package/lib/remark/headings/index.js.map +1 -1
  12. package/lib/remark/toc/index.d.ts +2 -2
  13. package/lib/remark/toc/index.d.ts.map +1 -1
  14. package/lib/remark/toc/index.js +7 -7
  15. package/lib/remark/toc/index.js.map +1 -1
  16. package/lib/remark/transformImage/index.d.ts.map +1 -1
  17. package/lib/remark/transformImage/index.js +18 -15
  18. package/lib/remark/transformImage/index.js.map +1 -1
  19. package/lib/remark/transformLinks/index.d.ts.map +1 -1
  20. package/lib/remark/transformLinks/index.js +14 -14
  21. package/lib/remark/transformLinks/index.js.map +1 -1
  22. package/lib/remark/unwrapMdxCodeBlocks/index.js +2 -2
  23. package/lib/remark/unwrapMdxCodeBlocks/index.js.map +1 -1
  24. package/lib/remark/utils/index.d.ts.map +1 -1
  25. package/lib/remark/utils/index.js +4 -4
  26. package/lib/remark/utils/index.js.map +1 -1
  27. package/package.json +17 -17
  28. package/src/deps.d.ts +16 -15
  29. package/src/index.ts +23 -213
  30. package/src/loader.ts +253 -0
  31. package/src/remark/headings/index.ts +3 -6
  32. package/src/remark/toc/index.ts +7 -7
  33. package/src/remark/transformImage/index.ts +12 -5
  34. package/src/remark/transformLinks/index.ts +8 -3
  35. package/src/remark/unwrapMdxCodeBlocks/index.ts +1 -1
  36. package/src/remark/utils/index.ts +1 -2
  37. package/src/mdx-loader.d.ts +0 -18
package/src/loader.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import fs from 'fs-extra';
9
+ import {createCompiler} from '@mdx-js/mdx';
10
+ import logger from '@docusaurus/logger';
11
+ import emoji from 'remark-emoji';
12
+ import {
13
+ parseFrontMatter,
14
+ parseMarkdownContentTitle,
15
+ escapePath,
16
+ getFileLoaderUtils,
17
+ } from '@docusaurus/utils';
18
+ import stringifyObject from 'stringify-object';
19
+ import headings from './remark/headings';
20
+ import toc from './remark/toc';
21
+ import unwrapMdxCodeBlocks from './remark/unwrapMdxCodeBlocks';
22
+ import transformImage from './remark/transformImage';
23
+ import transformLinks from './remark/transformLinks';
24
+ import type {LoaderContext} from 'webpack';
25
+ import type {Processor, Plugin} from 'unified';
26
+
27
+ const {
28
+ loaders: {inlineMarkdownImageFileLoader},
29
+ } = getFileLoaderUtils();
30
+
31
+ const pragma = `
32
+ /* @jsxRuntime classic */
33
+ /* @jsx mdx */
34
+ /* @jsxFrag mdx.Fragment */
35
+ `;
36
+
37
+ const DEFAULT_OPTIONS: MDXOptions = {
38
+ rehypePlugins: [],
39
+ remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
40
+ beforeDefaultRemarkPlugins: [],
41
+ beforeDefaultRehypePlugins: [],
42
+ };
43
+
44
+ const compilerCache = new Map<string | Options, [Processor, Options]>();
45
+
46
+ export type MDXPlugin =
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ [Plugin<any[]>, any] | Plugin<any[]>;
49
+ export type MDXOptions = {
50
+ remarkPlugins: MDXPlugin[];
51
+ rehypePlugins: MDXPlugin[];
52
+ beforeDefaultRemarkPlugins: MDXPlugin[];
53
+ beforeDefaultRehypePlugins: MDXPlugin[];
54
+ };
55
+
56
+ export type Options = MDXOptions & {
57
+ staticDirs: string[];
58
+ siteDir: string;
59
+ isMDXPartial?: (filePath: string) => boolean;
60
+ isMDXPartialFrontMatterWarningDisabled?: boolean;
61
+ removeContentTitle?: boolean;
62
+ metadataPath?: string | ((filePath: string) => string);
63
+ createAssets?: (metadata: {
64
+ frontMatter: {[key: string]: unknown};
65
+ metadata: {[key: string]: unknown};
66
+ }) => {[key: string]: unknown};
67
+ filepath: string;
68
+ };
69
+
70
+ /**
71
+ * When this throws, it generally means that there's no metadata file associated
72
+ * with this MDX document. It can happen when using MDX partials (usually
73
+ * starting with _). That's why it's important to provide the `isMDXPartial`
74
+ * function in config
75
+ */
76
+ async function readMetadataPath(metadataPath: string) {
77
+ try {
78
+ return await fs.readFile(metadataPath, 'utf8');
79
+ } catch (err) {
80
+ logger.error`MDX loader can't read MDX metadata file path=${metadataPath}. Maybe the isMDXPartial option function was not provided?`;
81
+ throw err;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Converts assets an object with Webpack require calls code.
87
+ * This is useful for mdx files to reference co-located assets using relative
88
+ * paths. Those assets should enter the Webpack assets pipeline and be hashed.
89
+ * For now, we only handle that for images and paths starting with `./`:
90
+ *
91
+ * `{image: "./myImage.png"}` => `{image: require("./myImage.png")}`
92
+ */
93
+ function createAssetsExportCode(assets: {[key: string]: unknown}) {
94
+ if (Object.keys(assets).length === 0) {
95
+ return 'undefined';
96
+ }
97
+
98
+ // TODO implementation can be completed/enhanced
99
+ function createAssetValueCode(assetValue: unknown): string | undefined {
100
+ if (Array.isArray(assetValue)) {
101
+ const arrayItemCodes = assetValue.map(
102
+ (item) => createAssetValueCode(item) ?? 'undefined',
103
+ );
104
+ return `[${arrayItemCodes.join(', ')}]`;
105
+ }
106
+ // Only process string values starting with ./
107
+ // We could enhance this logic and check if file exists on disc?
108
+ if (typeof assetValue === 'string' && assetValue.startsWith('./')) {
109
+ // TODO do we have other use-cases than image assets?
110
+ // Probably not worth adding more support, as we want to move to Webpack 5 new asset system (https://github.com/facebook/docusaurus/pull/4708)
111
+ const inlineLoader = inlineMarkdownImageFileLoader;
112
+ return `require("${inlineLoader}${escapePath(assetValue)}").default`;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ const assetEntries = Object.entries(assets);
118
+
119
+ const codeLines = assetEntries
120
+ .map(([key, value]) => {
121
+ const assetRequireCode = createAssetValueCode(value);
122
+ return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined;
123
+ })
124
+ .filter(Boolean);
125
+
126
+ return `{\n${codeLines.join('\n')}\n}`;
127
+ }
128
+
129
+ export async function mdxLoader(
130
+ this: LoaderContext<Options>,
131
+ fileString: string,
132
+ ): Promise<void> {
133
+ const callback = this.async();
134
+ const filePath = this.resourcePath;
135
+ const reqOptions = this.getOptions() ?? {};
136
+
137
+ const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);
138
+
139
+ const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, {
140
+ removeContentTitle: reqOptions.removeContentTitle,
141
+ });
142
+
143
+ const hasFrontMatter = Object.keys(frontMatter).length > 0;
144
+
145
+ if (!compilerCache.has(this.query)) {
146
+ const options: Options = {
147
+ ...reqOptions,
148
+ remarkPlugins: [
149
+ ...(reqOptions.beforeDefaultRemarkPlugins ?? []),
150
+ ...DEFAULT_OPTIONS.remarkPlugins,
151
+ [
152
+ transformImage,
153
+ {
154
+ staticDirs: reqOptions.staticDirs,
155
+ siteDir: reqOptions.siteDir,
156
+ },
157
+ ],
158
+ [
159
+ transformLinks,
160
+ {
161
+ staticDirs: reqOptions.staticDirs,
162
+ siteDir: reqOptions.siteDir,
163
+ },
164
+ ],
165
+ ...(reqOptions.remarkPlugins ?? []),
166
+ ],
167
+ rehypePlugins: [
168
+ ...(reqOptions.beforeDefaultRehypePlugins ?? []),
169
+ ...DEFAULT_OPTIONS.rehypePlugins,
170
+ ...(reqOptions.rehypePlugins ?? []),
171
+ ],
172
+ };
173
+ compilerCache.set(this.query, [createCompiler(options), options]);
174
+ }
175
+
176
+ const [compiler, options] = compilerCache.get(this.query)!;
177
+
178
+ let result: string;
179
+ try {
180
+ result = await compiler
181
+ .process({
182
+ contents: content,
183
+ path: this.resourcePath,
184
+ })
185
+ .then((res) => res.toString());
186
+ } catch (err) {
187
+ return callback(err as Error);
188
+ }
189
+
190
+ // MDX partials are MDX files starting with _ or in a folder starting with _
191
+ // Partial are not expected to have associated metadata files or front matter
192
+ const isMDXPartial = options.isMDXPartial?.(filePath);
193
+ if (isMDXPartial && hasFrontMatter) {
194
+ const errorMessage = `Docusaurus MDX partial files should not contain front matter.
195
+ Those partial files use the _ prefix as a convention by default, but this is configurable.
196
+ File at ${filePath} contains front matter that will be ignored:
197
+ ${JSON.stringify(frontMatter, null, 2)}`;
198
+
199
+ if (!options.isMDXPartialFrontMatterWarningDisabled) {
200
+ const shouldError = process.env.NODE_ENV === 'test' || process.env.CI;
201
+ if (shouldError) {
202
+ return callback(new Error(errorMessage));
203
+ }
204
+ logger.warn(errorMessage);
205
+ }
206
+ }
207
+
208
+ function getMetadataPath(): string | undefined {
209
+ if (!isMDXPartial) {
210
+ // Read metadata for this MDX and export it.
211
+ if (options.metadataPath && typeof options.metadataPath === 'function') {
212
+ return options.metadataPath(filePath);
213
+ }
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ const metadataPath = getMetadataPath();
219
+ if (metadataPath) {
220
+ this.addDependency(metadataPath);
221
+ }
222
+
223
+ const metadataJsonString = metadataPath
224
+ ? await readMetadataPath(metadataPath)
225
+ : undefined;
226
+
227
+ const metadata = metadataJsonString
228
+ ? JSON.parse(metadataJsonString)
229
+ : undefined;
230
+
231
+ const assets =
232
+ reqOptions.createAssets && metadata
233
+ ? reqOptions.createAssets({frontMatter, metadata})
234
+ : undefined;
235
+
236
+ const exportsCode = `
237
+ export const frontMatter = ${stringifyObject(frontMatter)};
238
+ export const contentTitle = ${stringifyObject(contentTitle)};
239
+ ${metadataJsonString ? `export const metadata = ${metadataJsonString};` : ''}
240
+ ${assets ? `export const assets = ${createAssetsExportCode(assets)};` : ''}
241
+ `;
242
+
243
+ const code = `
244
+ ${pragma}
245
+ import React from 'react';
246
+ import { mdx } from '@mdx-js/react';
247
+
248
+ ${exportsCode}
249
+ ${result}
250
+ `;
251
+
252
+ return callback(null, code);
253
+ }
@@ -9,9 +9,8 @@
9
9
 
10
10
  import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
11
11
  import visit from 'unist-util-visit';
12
- import toString from 'mdast-util-to-string';
12
+ import mdastToString from 'mdast-util-to-string';
13
13
  import type {Transformer} from 'unified';
14
- import type {Parent} from 'unist';
15
14
  import type {Heading, Text} from 'mdast';
16
15
 
17
16
  export default function plugin(): Transformer {
@@ -30,10 +29,8 @@ export default function plugin(): Transformer {
30
29
  const headingTextNodes = headingNode.children.filter(
31
30
  ({type}) => !['html', 'jsx'].includes(type),
32
31
  );
33
- const heading = toString(
34
- headingTextNodes.length > 0
35
- ? ({children: headingTextNodes} as Parent)
36
- : headingNode,
32
+ const heading = mdastToString(
33
+ headingTextNodes.length > 0 ? headingTextNodes : headingNode,
37
34
  );
38
35
 
39
36
  // Support explicit heading IDs
@@ -13,7 +13,7 @@ import toString from 'mdast-util-to-string';
13
13
  import visit from 'unist-util-visit';
14
14
  import {toValue} from '../utils';
15
15
 
16
- import type {TOCItem} from '@docusaurus/types';
16
+ import type {TOCItem} from '../..';
17
17
  import type {Node, Parent} from 'unist';
18
18
  import type {Heading, Literal} from 'mdast';
19
19
  import type {Transformer} from 'unified';
@@ -27,9 +27,9 @@ const isImport = (child: Node): child is Literal => child.type === 'import';
27
27
  const hasImports = (index: number) => index > -1;
28
28
  const isExport = (child: Node): child is Literal => child.type === 'export';
29
29
 
30
- interface PluginOptions {
30
+ type PluginOptions = {
31
31
  name?: string;
32
- }
32
+ };
33
33
 
34
34
  const isTarget = (child: Literal, name: string) => {
35
35
  let found = false;
@@ -76,10 +76,10 @@ export default function plugin(options: PluginOptions = {}): Transformer {
76
76
  return (root) => {
77
77
  const headings: TOCItem[] = [];
78
78
 
79
- visit(root, 'heading', (child: Heading, _index, parent) => {
79
+ visit(root, 'heading', (child: Heading, index, parent) => {
80
80
  const value = toString(child);
81
81
 
82
- // depth:1 headings are titles and not included in the TOC
82
+ // depth: 1 headings are titles and not included in the TOC
83
83
  if (parent !== root || !value || child.depth < 2) {
84
84
  return;
85
85
  }
@@ -93,8 +93,8 @@ export default function plugin(options: PluginOptions = {}): Transformer {
93
93
  const {children} = root as Parent<Literal>;
94
94
  const targetIndex = getOrCreateExistingTargetIndex(children, name);
95
95
 
96
- if (headings && headings.length) {
97
- children[targetIndex].value = `export const ${name} = ${stringifyObject(
96
+ if (headings.length) {
97
+ children[targetIndex]!.value = `export const ${name} = ${stringifyObject(
98
98
  headings,
99
99
  )};`;
100
100
  }
@@ -67,8 +67,12 @@ async function toImageRequireNode(
67
67
  height = ` height="${size.height}"`;
68
68
  }
69
69
  } catch (err) {
70
- logger.error`The image at path=${imagePath} can't be read correctly. Please ensure it's a valid image.
70
+ // Workaround for https://github.com/yarnpkg/berry/pull/3889#issuecomment-1034469784
71
+ // TODO remove this check once fixed in Yarn PnP
72
+ if (!process.versions.pnp) {
73
+ logger.warn`The image at path=${imagePath} can't be read correctly. Please ensure it's a valid image.
71
74
  ${(err as Error).message}`;
75
+ }
72
76
  }
73
77
 
74
78
  Object.keys(jsxNode).forEach(
@@ -76,7 +80,7 @@ ${(err as Error).message}`;
76
80
  );
77
81
 
78
82
  (jsxNode as Literal).type = 'jsx';
79
- jsxNode.value = `<img loading="lazy" ${alt}src={${src}}${title}${width}${height} />`;
83
+ jsxNode.value = `<img ${alt}src={${src}}${title}${width}${height} />`;
80
84
  }
81
85
 
82
86
  async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
@@ -99,7 +103,7 @@ async function getImageAbsolutePath(
99
103
  await ensureImageFileExist(imageFilePath, filePath);
100
104
  return imageFilePath;
101
105
  } else if (path.isAbsolute(imagePath)) {
102
- // absolute paths are expected to exist in the static folder
106
+ // Absolute paths are expected to exist in the static folder.
103
107
  const possiblePaths = staticDirs.map((dir) => path.join(dir, imagePath));
104
108
  const imageFilePath = await findAsyncSequential(
105
109
  possiblePaths,
@@ -116,8 +120,11 @@ async function getImageAbsolutePath(
116
120
  }
117
121
  return imageFilePath;
118
122
  }
119
- // relative paths are resolved against the source file's folder
120
- const imageFilePath = path.join(path.dirname(filePath), imagePath);
123
+ // Relative paths are resolved against the source file's folder.
124
+ const imageFilePath = path.join(
125
+ path.dirname(filePath),
126
+ decodeURIComponent(imagePath),
127
+ );
121
128
  await ensureImageFileExist(imageFilePath, filePath);
122
129
  return imageFilePath;
123
130
  }
@@ -34,7 +34,9 @@ type Context = PluginOptions & {
34
34
  filePath: string;
35
35
  };
36
36
 
37
- // transform the link node to a jsx link with a require() call
37
+ /**
38
+ * Transforms the link node to a JSX `<a>` element with a `require()` call.
39
+ */
38
40
  function toAssetRequireNode(node: Link, assetPath: string, filePath: string) {
39
41
  const jsxNode = node as Literal & Partial<Link>;
40
42
  let relativeAssetPath = posixPath(
@@ -106,7 +108,7 @@ async function getAssetAbsolutePath(
106
108
 
107
109
  async function processLinkNode(node: Link, context: Context) {
108
110
  if (!node.url) {
109
- // try to improve error feedback
111
+ // Try to improve error feedback
110
112
  // see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675
111
113
  const title = node.title || (node.children[0] as Literal)?.value || '?';
112
114
  const line = node?.position?.start?.line || '?';
@@ -130,7 +132,10 @@ async function processLinkNode(node: Link, context: Context) {
130
132
  return;
131
133
  }
132
134
 
133
- const assetPath = await getAssetAbsolutePath(parsedUrl.pathname, context);
135
+ const assetPath = await getAssetAbsolutePath(
136
+ decodeURIComponent(parsedUrl.pathname),
137
+ context,
138
+ );
134
139
  if (assetPath) {
135
140
  toAssetRequireNode(node, assetPath, context.filePath);
136
141
  }
@@ -17,7 +17,7 @@ import type {Code, Parent} from 'mdast';
17
17
  // See https://github.com/facebook/docusaurus/pull/4278
18
18
  export default function plugin(this: Processor): Transformer {
19
19
  return (root) => {
20
- visit(root, 'code', (node: Code, _index, parent) => {
20
+ visit(root, 'code', (node: Code, index, parent) => {
21
21
  if (node.lang === 'mdx-code-block') {
22
22
  const newChildren = (this.parse(node.value) as Parent).children;
23
23
 
@@ -31,7 +31,6 @@ export function toValue(node: PhrasingContent | Heading): string {
31
31
  case 'link':
32
32
  return stringifyContent(node);
33
33
  default:
34
+ return toString(node);
34
35
  }
35
-
36
- return toString(node);
37
36
  }
@@ -1,18 +0,0 @@
1
- /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
- import type {Plugin} from 'unified';
9
-
10
- export type RemarkOrRehypePlugin =
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- [Plugin<any[]>, Record<string, unknown>] | Plugin<any[]>;
13
- export type RemarkAndRehypePluginOptions = {
14
- remarkPlugins: RemarkOrRehypePlugin[];
15
- rehypePlugins: RemarkOrRehypePlugin[];
16
- beforeDefaultRemarkPlugins: RemarkOrRehypePlugin[];
17
- beforeDefaultRehypePlugins: RemarkOrRehypePlugin[];
18
- };