-
-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ESM in Gatsby #40
Comments
It looks like Gatsby doesn’t support actual ESM. How to use ESM with different tools is outside of the scope of this project. I’ll add a note in the readme to this gist: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c. |
Edit: Also see the comment below for multiple improvements to this post :) For reference in case anybody else stumbles upon this and wants to replace import path from 'path';
export default {
mode: 'production',
entry: './index.js',
output: {
path: path.resolve('D:\\Code\\xdm', 'dist'),
filename: 'bundle.js',
library: {
name: 'xdm',
type: 'commonjs',
},
},
}; and this can be used in a Gatsby plugin with something like const { xdm } = require("./xdm-bundle.js"); or, if using import { xdm } from './xdm'; Adding In case it helps anyone else I'll just document what I did to replicate (some of) the functionality of In the exports.onCreateNode = async api => {
const {
node,
actions,
loadNodeContent,
createContentDigest,
createNodeId,
} = api;
const { createNodeField, createNode, createParentChildLink } = actions;
if (node.internal.type === `File` && node.ext === '.mdx') {
const content = await loadNodeContent(node);
const xdmNode = await createXdmNode(
{
id: createNodeId(`${node.id} >>> Xdm`),
node,
content,
},
api
);
createNode(xdmNode);
createParentChildLink({ parent: node, child: xdmNode });
}
} The implementation of import { createContentDigest } from 'gatsby-core-utils';
import graymatter from 'gray-matter';
import remarkAutolinkHeadings from 'remark-autolink-headings';
import remarkExternalLinks from 'remark-external-links';
import remarkFrontmatter from 'remark-frontmatter';
import gfm from 'remark-gfm';
import { remarkMdxFrontmatter } from 'remark-mdx-frontmatter';
import remarkHtmlNodes from '../mdx-plugins/remark-html-nodes.js';
import remarkToC from '../mdx-plugins/remark-toc';
import getGatsbyImage from './wrapped-gatsby-img-plugin';
import { xdm } from './xdm';
export async function createXdmNode({ id, node, content }, api) {
let xdmNode: any = {
id,
children: [],
parent: node.id,
internal: {
content: content,
type: `Xdm`,
},
};
let compiledResult;
const tableOfContents = [];
const gatsbyImage = getGatsbyImage({
...api,
xdmNode,
});
try {
compiledResult = await xdm.compile(content, {
remarkPlugins: [
gfm,
remarkFrontmatter,
remarkMdxFrontmatter,
[remarkToC, { tableOfContents }],
gatsbyImage,
remarkHtmlNodes,
],
rehypePlugins: [],
});
compiledResult = String(compiledResult);
} catch (e) {
// add the path of the file to simplify debugging error messages
e.message += `${node.absolutePath}: ${e.message}`;
throw e;
}
compiledResult = compiledResult.replace(
/import .* from "react\/jsx-runtime";/,
''
);
compiledResult = compiledResult.replace(
`function MDXContent(_props) {`,
'function MDXContent(_Fragment, _jsx, _jsxs, _props) {'
);
compiledResult = compiledResult.replace(
'export default MDXContent',
'return MDXContent'
);
compiledResult = compiledResult.replace('export const ', 'const ');
// // extract all the exports
// const { frontmatter, ...nodeExports } = extractExports(
// code,
// node.absolutePath
// )
const { data: frontmatter } = graymatter(content);
xdmNode = {
...xdmNode,
body: compiledResult,
frontmatter,
toc: tableOfContents,
};
// xdmNode.exports = nodeExports
// Add path to the markdown file path
if (node.internal.type === `File`) {
xdmNode.fileAbsolutePath = node.absolutePath;
}
xdmNode.internal.contentDigest = createContentDigest(xdmNode);
return xdmNode;
} Some things to note:
remarkToC implementation: const mdastToString = require('mdast-util-to-string');
const Slugger = require('github-slugger');
module.exports = ({ tableOfContents }) => {
const slugger = new Slugger();
function process(node) {
if (node.type === 'heading') {
const val = {
depth: node.depth,
value: mdastToString(node),
slug: slugger.slug(mdastToString(node), false),
};
tableOfContents.push(val);
}
for (let child of node.children || []) {
process(child, curLang);
}
}
return node => {
process(node);
};
}; There might be a neater way to extract data that doesn't involve making a fake plugin? Also you can get all heading nodes in a simpler way with another unified plugin that I forgot (my specific use-case was slightly more complicated and required information about other nodes as well, which is why the implementation above is recursive). To get
const interopDefault = exp =>
exp && typeof exp === `object` && `default` in exp ? exp[`default`] : exp;
const getPlugin = ({
xdmNode,
getNode,
getNodesByType,
reporter,
cache,
pathPrefix,
...helpers
}) => {
async function transformer(markdownAST) {
const requiredPlugin = interopDefault(require('./custom-gatsby-img.js'));
await requiredPlugin(
{
markdownAST,
markdownNode: xdmNode,
getNode,
getNodesByType,
get files() {
return getNodesByType(`File`);
},
pathPrefix,
reporter,
cache,
...helpers,
},
{
maxWidth: 832,
quality: 100,
disableBgImageOnAlpha: true,
}
);
return markdownAST;
}
return [() => transformer, {}];
};
module.exports = stuff => getPlugin(stuff); The second object passed into There's still a small problem -- module.exports = () => {
function process(node) {
if (node.type === 'html') {
node.type = 'mdxJsxTextElement';
node.name = 'RAWHTML';
node.children = [
{
type: 'text',
value: node.value,
},
];
}
for (let child of node.children || []) {
process(child);
}
}
return node => {
process(node);
};
}; (above can be implemented better w/ a proper library). Also, make sure that any image assets you reference in your markdown files are loaded by {
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/assets`,
name: `assets`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/content`,
name: `content`,
},
}, This will work, since assets (the images) are loaded before the markdown. However, flipping the order of the two will cause images to fail silently (but it will sometimes work during development, which causes major debugging headaches...) To render the markdown returned from import * as React from 'react';
import {
Fragment as _Fragment,
jsx as _jsx,
jsxs as _jsxs,
} from 'react/jsx-runtime';
import { components } from './MDXComponents';
const Markdown = (props: { body: any }) => {
const fn = new Function(props.body)();
return (
<div className="markdown">{fn(_Fragment, _jsx, _jsxs, { components })}</div>
);
};
export default React.memo(Markdown); (I think there might be a better way to do this? see In your MDX components, make sure to also include the const RAWHTML = ({ children }) => {
return <div dangerouslySetInnerHTML={{ __html: children }} />;
}; You might also need to create schema definitions for Xdm nodes: exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
type Xdm implements Node {
body: String
fileAbsolutePath: String
frontmatter: XdmFrontmatter
isIncomplete: Boolean
toc: TableOfContents
}
type XdmFrontmatter implements Node {
id: String
title: String
author: String
description: String
prerequisites: [String]
redirects: [String]
}
`;
createTypes(typeDefs);
}; (some of these are specific to my project, adjust as needed)
const { getOptions } = require('loader-utils');
const { xdm } = require('./xdm');
module.exports = function (code) {
const callback = this.async();
xdm
.compile(
{ contents: code, path: this.resourcePath },
{
remarkPlugins: [],
rehypePlugins: [],
...getOptions(this),
}
)
.then(file => {
callback(null, file.contents, file.map);
return file;
}, callback);
}; Then, in exports.onCreateWebpackConfig = ({ actions, stage, loaders, plugins }) => {
actions.setWebpackConfig({
module: {
rules: [
{
test: /\.mdx$/,
use: [
loaders.js(),
{
loader: path.resolve(__dirname, 'src/gatsby/webpack-xdm.js'),
options: {},
},
],
},
],
},
});
}; Note that this loader doesn't let you use Gatsby's image processing. I believe (but haven't tried) that you get the image processing working by creating a wrapper around Again, this was mostly a proof-of-concept so I didn't bother to make the code neat/maintainable. Hopefully somebody will come up with a better solution to this soon 🙏 Useful links in case someone else wants to attempt this:
It's also possible to create a browser "playground" with I don't have hard benchmarks, but my build time nearly (?) halved (in gatsby v3 and webpack 5 at least) after implementing these changes. Playground render performance improved by ~66%. I think I'm mostly bottlenecked by katex at this point (before the babel transforms from mdx were the primary bottleneck for me). |
|
Thanks for the suggestions!
For The use case for this is because optimally, during development, each MDX file would be compiled on-demand rather than compiling every MDX file when the development server starts, since compiling many MDX files can take a while (especially with extensive latex). However, the frontmatter + exported values of every MDX file would be extracted (ideally efficiently) when the development server starts, since this information is needed to generate page information. Frontmatter can be extracted with graymatter. I haven't figured out how to efficiently extract exported values though. If there isn't a neat way to handle this, it's not a problem -- XDM is fast enough that the this optimization isn't that important, and Gatsby caches nodes already anyway, so the performance difference is negligible after the first run. I'm mostly just curious to see if this was possible :P |
[1] that’s what frontmatter is: it‘s static, you don’t need to know if the file is MDX, or markdown, or something entirely different. The frontmatter can be accessed without compiling the file. And graymatter (or [2] is done by |
I'm facing a similar issue with the latest |
@kimbaudi How to use ESM is outside the scope of this project. The comments here show a way to make it work. Did you try them? |
@wooorm I tried compiling module.exports = async () => {
const { visit } = await import('unist-util-visit')
} There is probably a way to get it working and I just haven't figured it out. Thanks for all your work. |
I don’t quite understand what And, in the thread above, there are references to a project that has what you want working. So the solution you’re looking for is linked above? |
both I understand how to use ESM is outside the scope of this project. I was just commenting that I am facing similar issue w/ |
ahh, okay! I was assuming this was about xdm 😅 |
Is it possible to use this library with Gatsby? I can't seem to import
xdm
in Gatsby.The following
gatsby-node.js
file:Fails with
Error: TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.
,Importing
xdm
with esm:fails with
SyntaxError: Cannot use import statement outside a module
.Adding
type: "module"
topackage.json
doesn't seem to work with Gatsby (fails withTypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension "" for D:\Code\test-site\.cache\tmp-3340-dY2ATXgGrBY4
or a similar error)Adding esm to Gatsby with the
esm
package fails withError [ERR_REQUIRE_ESM]: Must use import to load ES Module
.Is it possible to use
xdm
with Gatsby as an alternative togatsby-plugin-mdx
?The text was updated successfully, but these errors were encountered: