Skip to content

Commit 1c49600

Browse files
authored
feat: nextjs and mdx compiler (#6071)
* feat: nextjs and mdx compiler * chore: just some text * chore: some comments * chore: attempts on improving the performance * chore: code review changes and fixes * chore: properly adjust sizes when needed * feat: include simple own optimised syntax highlighter * chore: more text * chore: should always have one child at least * chore: minor doc changes * chore: minor changes on types * chore: removal of legacyMain completely
1 parent ed0ddfa commit 1c49600

18 files changed

+5600
-3123
lines changed

global.d.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

next.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const nextConfig = {
3636
eslint: { dirs: ['.'], ignoreDuringBuilds: true },
3737
// Next.js WebPack Bundler does not know how to handle `.mjs` files on `node_modules`
3838
// This is not an issue when using TurboPack as it uses SWC and it is ESM-only
39-
// Once we migrate to Next.js 14 we might be able to remove this
39+
// Once Next.js uses Turbopack for their build process we can remove this
4040
webpack: function (config) {
4141
config.module.rules.push({
4242
test: /\.m?js$/,
@@ -58,6 +58,8 @@ const nextConfig = {
5858
'@radix-ui/react-toast',
5959
'tailwindcss',
6060
],
61+
// Removes the warning regarding the WebPack Build Worker
62+
webpackBuildWorker: false,
6163
},
6264
};
6365

next.constants.mjs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,6 @@ export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i;
8282
*/
8383
export const DEFAULT_LOCALE_CODE = defaultLocale.code;
8484

85-
/**
86-
* This indicates the path to the Legacy JavaScript File that is used
87-
* on the legacy Website.
88-
*
89-
* @deprecated The Legacy Website is due to be removed soon and this file
90-
* and its usages should be removed
91-
*/
92-
export const LEGACY_JAVASCRIPT_FILE = `${BASE_PATH}/static/js/legacyMain.js`;
93-
9485
/**
9586
* This is a list of all static routes or pages from the Website that we do not
9687
* want to allow to be statically built on our Static Export Build.

next.dynamic.mjs

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import { readFileSync } from 'node:fs';
44
import { join, normalize, sep } from 'node:path';
55

6-
import { serialize } from 'next-mdx-remote/serialize';
76
import { VFile } from 'vfile';
87

98
import { DEFAULT_LOCALE_CODE, MD_EXTENSION_REGEX } from './next.constants.mjs';
109
import { getMarkdownFiles } from './next.helpers.mjs';
1110
import { availableLocales } from './next.locales.mjs';
12-
import { nextRehypePlugins, nextRemarkPlugins } from './next.mdx.mjs';
11+
import { compileMDX } from './next.mdx.compiler.mjs';
1312

1413
// allows us to run a glob to get markdown files based on a language folder
1514
const getPathsByLanguage = async (locale = DEFAULT_LOCALE_CODE, ignored = []) =>
@@ -148,31 +147,15 @@ export const generateStaticProps = async (source = '', filename = '') => {
148147
// Gets the file extension of the file, to determine which parser and plugins to use
149148
const fileExtension = filename.endsWith('.mdx') ? 'mdx' : 'md';
150149

151-
// This act as a MDX "compiler" but, lightweight. It parses the Markdown
152-
// string source into a React Component tree, and then it serializes it
153-
// it also supports Remark plugins, and MDX components
154-
// Note.: We use the filename extension to define the mode of execution
155-
const { compiledSource } = await serialize(sourceAsVirtualFile, {
156-
parseFrontmatter: true,
157-
mdxOptions: {
158-
rehypePlugins: nextRehypePlugins(fileExtension),
159-
remarkPlugins: nextRemarkPlugins(fileExtension),
160-
format: fileExtension,
161-
},
162-
});
163-
164-
// After the MDX gets processed with the remarkPlugins, some extra `data` that might come along
165-
// the `frontmatter` comes from `serialize` built-in support to `remark-frontmatter`
166-
const { headings, matter: rawFrontmatter } = sourceAsVirtualFile.data;
167-
168-
// This serialises the Frontmatter into a JSON object that is compatible with the
169-
// `getStaticProps` supported data type for props. (No prop value can be an object or not a primitive)
170-
const frontmatter = JSON.parse(JSON.stringify(rawFrontmatter));
171-
172-
// this defines the basic props that should be passed back to the `DynamicPage` component
173-
// We only want the `compiledSource` as we use `MDXProvider` for custom components along the journey
174-
// And then we want the frontmatter and heading information from the VFile `data`
175-
staticProps.props = { content: compiledSource, headings, frontmatter };
150+
// This compiles our MDX source (VFile) into a final MDX-parsed VFile
151+
// that then is passed as a string to the MDXProvider which will run the MDX Code
152+
const { content, headings, frontmatter } = await compileMDX(
153+
sourceAsVirtualFile,
154+
fileExtension
155+
);
156+
157+
// Passes the compiled MDX Source to the MDX Provider and some extra data
158+
staticProps.props = { content: String(content), headings, frontmatter };
176159
staticProps.notFound = false;
177160
}
178161

next.mdx.compiler.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
import { compile, runSync } from '@mdx-js/mdx';
4+
import * as jsxRuntime from 'react/jsx-runtime';
5+
import { matter } from 'vfile-matter';
6+
7+
import { NEXT_REHYPE_PLUGINS, NEXT_REMARK_PLUGINS } from './next.mdx.mjs';
8+
9+
/**
10+
* This is our custom simple MDX Compiler that is used to compile Markdown and MDX
11+
* this returns a serializable VFile as a string that then gets passed to our MDX Provider
12+
*
13+
* @param {import('vfile').VFile} source
14+
* @param {'md' | 'mdx'} fileExtension
15+
* @returns {Promise<{ content: import('vfile').VFile; headings: import('@vcarl/remark-headings').Heading[]; frontmatter: Record<string, any>}>}
16+
*/
17+
export async function compileMDX(source, fileExtension) {
18+
// Parses the Frontmatter to the VFile and removes from the original source
19+
// cleaning the frontmatter to the source that is going to be parsed by the MDX Compiler
20+
matter(source, { strip: true });
21+
22+
// This is a minimal MDX Compiler that is lightweight and only parses the MDX
23+
const compiledSource = await compile(source, {
24+
rehypePlugins: NEXT_REHYPE_PLUGINS,
25+
remarkPlugins: NEXT_REMARK_PLUGINS,
26+
format: fileExtension,
27+
// This instructs the MDX compiler to generate a minimal JSX-body
28+
// to be consumed within MDX's `run` method, instead of a standalone React Application
29+
outputFormat: 'function-body',
30+
// Ensure compatibility with Server Components
31+
providerImportSource: undefined,
32+
});
33+
34+
// Retrieve some parsed data from the VFile metadata
35+
// such as frontmatter and Markdown headings
36+
const { headings, matter: frontmatter } = source.data;
37+
38+
return { content: compiledSource, headings, frontmatter };
39+
}
40+
41+
/**
42+
* This evaluates our MDX VFile into actual JSX eval'd code
43+
* which is actually used by the MDX Provider
44+
*
45+
* @param {string} source
46+
* @returns {import('mdx/types').MDXContent}
47+
*/
48+
export function runMDX(source) {
49+
const { default: content } = runSync(source, jsxRuntime);
50+
51+
return content;
52+
}

next.mdx.mjs

Lines changed: 16 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,29 @@
11
'use strict';
22

3-
/// <reference types="remark-parse" />
4-
/// <reference types="remark-stringify" />
3+
import remarkHeadings from '@vcarl/remark-headings';
4+
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
5+
import rehypeSlug from 'rehype-slug';
6+
import remarkGfm from 'remark-gfm';
57

6-
/**
7-
* @typedef {import('mdast').Root} Root
8-
* @typedef {import('unified').Processor<Root>} Processor
9-
*/
10-
11-
import * as remarkHeadings from '@vcarl/remark-headings';
12-
import * as mdastAutoLink from 'mdast-util-gfm-autolink-literal';
13-
import * as mdastTable from 'mdast-util-gfm-table';
14-
import * as rehypeAutolinkHeadings from 'rehype-autolink-headings';
15-
import * as rehypeRaw from 'rehype-raw';
16-
import * as rehypeShikiji from 'rehype-shikiji';
17-
import * as rehypeSlug from 'rehype-slug';
18-
19-
import { LANGUAGES, DEFAULT_THEME } from './shiki.config.mjs';
20-
21-
/**
22-
* This function is used to add individual `mdast` plugins to the unified/mdx
23-
* processor with the intent of being able to customize plugins
24-
*
25-
* @returns {void}
26-
*/
27-
function nextMdastPlugins() {
28-
const self = /** @type {Processor} */ (this);
29-
const data = self.data();
30-
31-
const fromMarkdownExtensions =
32-
data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
33-
34-
const toMarkdownExtensions =
35-
data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
36-
37-
// Converts plain URLs on Markdown to HTML Anchor Tags
38-
fromMarkdownExtensions.push(mdastAutoLink.gfmAutolinkLiteralFromMarkdown());
39-
toMarkdownExtensions.push(mdastAutoLink.gfmAutolinkLiteralToMarkdown());
40-
41-
// Converts plain Markdown Tables (GFM) to HTML Tables
42-
fromMarkdownExtensions.push(mdastTable.gfmTableFromMarkdown);
43-
toMarkdownExtensions.push(mdastTable.gfmTableToMarkdown());
44-
}
8+
import rehypeShikiji from './next.mdx.shiki.mjs';
459

4610
/**
4711
* Provides all our Rehype Plugins that are used within MDX
4812
*
49-
* @param {'md' | 'mdx'} fileExtension
50-
* @returns {import('unified').Plugin[]}
13+
* @type {import('unified').Plugin[]}
5114
*/
52-
export function nextRehypePlugins(fileExtension) {
53-
const rehypePlugins = [
54-
// Generates `id` attributes for headings (H1, ...)
55-
rehypeSlug.default,
56-
[
57-
// Automatically add anchor links to headings (H1, ...)
58-
rehypeAutolinkHeadings.default,
59-
{
60-
behaviour: 'append',
61-
properties: { ariaHidden: true, tabIndex: -1, class: 'anchor' },
62-
},
63-
],
64-
[
65-
// Syntax Highlighter for Code Blocks
66-
rehypeShikiji.default,
67-
{ theme: DEFAULT_THEME, langs: LANGUAGES },
68-
],
69-
];
70-
71-
if (fileExtension === 'md') {
72-
// We add this plugin at the top of the array as it is supposed to parse raw HTML
73-
// before any other plugins (such as adding headings, etc)
74-
// before any of the other plugins being applied
75-
rehypePlugins.unshift(rehypeRaw.default);
76-
}
77-
78-
return rehypePlugins;
79-
}
15+
export const NEXT_REHYPE_PLUGINS = [
16+
// Generates `id` attributes for headings (H1, ...)
17+
rehypeSlug,
18+
// Automatically add anchor links to headings (H1, ...)
19+
[rehypeAutolinkHeadings, { properties: { tabIndex: -1, class: 'anchor' } }],
20+
// Adds our syntax highlighter (Shikiji) to Codeboxes
21+
rehypeShikiji,
22+
];
8023

8124
/**
8225
* Provides all our Remark Plugins that are used within MDX
8326
*
84-
* @param {'md' | 'mdx'} fileExtension
85-
* @returns {import('unified').Plugin[]}
27+
* @type {import('unified').Plugin[]}
8628
*/
87-
export function nextRemarkPlugins() {
88-
return [remarkHeadings.default, nextMdastPlugins];
89-
}
29+
export const NEXT_REMARK_PLUGINS = [remarkGfm, remarkHeadings];

next.mdx.shiki.mjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
import classNames from 'classnames';
4+
import { toString } from 'hast-util-to-string';
5+
import { getHighlighterCore } from 'shikiji/core';
6+
import { getWasmInlined } from 'shikiji/wasm';
7+
import { visit } from 'unist-util-visit';
8+
9+
import { LANGUAGES, DEFAULT_THEME } from './shiki.config.mjs';
10+
11+
// This creates a memoized minimal Shikiji Syntax Highlighter
12+
const memoizedShikiji = await getHighlighterCore({
13+
themes: [DEFAULT_THEME],
14+
langs: LANGUAGES,
15+
loadWasm: getWasmInlined,
16+
});
17+
18+
// This is what Remark will use as prefix within a <pre> className
19+
// to attribute the current language of the <pre> element
20+
const languagePrefix = 'language-';
21+
22+
export default function rehypeShikiji() {
23+
return async function (tree) {
24+
visit(tree, 'element', (node, index, parent) => {
25+
// We only want to process <pre>...</pre> elements
26+
if (!parent || index == null || node.tagName !== 'pre') {
27+
return;
28+
}
29+
30+
// We want the contents of the <pre> element, hence we attempt to get the first child
31+
const preElement = node.children[0];
32+
33+
// If thereÄs nothing inside the <pre> element... What are we doing here?
34+
if (!preElement || !preElement.properties) {
35+
return;
36+
}
37+
38+
// Ensure that we're not visiting a <code> element but it's inner contents
39+
// (keep iterating further down until we reach where we want)
40+
if (preElement.type !== 'element' || preElement.tagName !== 'code') {
41+
return;
42+
}
43+
44+
// Get the <pre> element class names
45+
const preClassNames = preElement.properties.className;
46+
47+
// The current classnames should be an array and it should have a length
48+
if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
49+
return;
50+
}
51+
52+
// We want to retrieve the language class name from the class names
53+
const codeLanguage = preClassNames.find(
54+
c => typeof c === 'string' && c.startsWith(languagePrefix)
55+
);
56+
57+
// If we didn't find any `language-` classname then we shouldn't highlight
58+
if (typeof codeLanguage !== 'string') {
59+
return;
60+
}
61+
62+
// Retrieve the whole <pre> contents as a parsed DOM string
63+
const preElementContents = toString(preElement);
64+
65+
// Grabs the relevant alias/name of the language
66+
const languageId = codeLanguage.slice(languagePrefix.length);
67+
68+
// Parses the <pre> contents and returns a HAST tree with the highlighted code
69+
const { children } = memoizedShikiji.codeToHast(preElementContents, {
70+
theme: DEFAULT_THEME,
71+
lang: languageId,
72+
});
73+
74+
// Adds the original language back to the <pre> element
75+
children[0].properties.class = classNames(
76+
children[0].properties.class,
77+
codeLanguage
78+
);
79+
80+
// Replaces the <pre> element with the updated one
81+
parent.children.splice(index, 1, ...children);
82+
});
83+
};
84+
}

0 commit comments

Comments
 (0)