Using vite-plugin-ssr with mdx-js, solving ESM only library problems, understanding vite configuration and writing vite plugins
Some debugging sessions take longer than you imagine. What makes it awesome at the end is what you learn from it.
vite-plugin-ssr is vite plugin which allows us to build websites with Server Side Rendering (SSR), Client Side Rendering (CSR), Single Page Applications (SPA) and Static Site Generation (SSG) all in one. This plugin is like Next.js but provides more control over each page and for any of your favorite frontend framework. Please visit the website to learn how to use this plugin.
In this tutorial we'll learn how to setup mdx-js library for vite project for building markdown based websites and to prerender them using vite-plugin-ssr to generate static websites.
The vite-plugin-ssr github repo contains example projects which you can clone and start with. For example react-full example already provides a setup for working with mdx-js library. The intention of this tutorial is to show how to solve some of the problems I encountered while using the mdx-js library and vite-plugin-ssr prerender feature.
Project setup
First of all, we need to setup a vite + vite-plugin-ssr based project. To scaffold a vite-plugin-ssr project simply execute
npm init vite-plugin-ssr
Give your project a name (I named it nn-blog) and select the frontend framework (in this example react) you would like to use. Once the command runs simply go to your project folder and install all dependencies.
cd nn-blog
npm install
Then run the dev server with npm run dev
. Congratulations, you've just setup a
vite + vite-plugin-ssr based project. The setup comes initialized with a git
repo, so you can start modifying the code around. And you'll notice how
blazingly fast the vite dev server is.
Once you understand the filesystem routing concepts of vite-plugin-ssr, create some pages and experiment. When you're ready let's start with adding mdx-js.
Adding mdx-js to vite project
mdx-js is a library which converts markdown content to jsx compatible content that you can then use with your jsx based libraries such as react, preact, vue.
MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. vite uses rollup under the hood to build bundles for production. So for installing mdx-js to a vite project, we should use
@mdx-js/rollup
and for handling custom MDX components we can use@mdx-js/react
for react based projects.
npm install @mdx-js/rollup @mdx-js/react
Once the libraries are installed, add mdx-js to vite plugins in vite.config.js
file and config the mdx plugin to use @mdx-js/react as an proiderImportSource.
import react from '@vitejs/plugin-react'
import ssr from 'vite-plugin-ssr/plugin'
+import mdx from "@mdx-js/rollup"
export default {
- plugins: [react(), ssr()]
+ plugins: [react(), mdx({
+ providerImportSource: "@mdx-js/react"
+ }), ssr()],
}
Solving problem 1 - require() of ES Module is not supported
Now after updating the vite.config.js
if we try to run npm run dev
we'll be
given this confusing error
failed to load config from /workspace/example/nn-blog/vite.config.js
/workspace/example/nn-blog/vite.config.js:61509
undefined
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /workspace/example/nn-blog/node_modules/@mdx-js/rollup/index.js from /workspace/example/nn-blog/vite.config.js not supported.
This problems occurs in the following order.
npm run dev
runsnode ./server/index.js
file which is a commonjs file- The script creates vite dev server using
vite.createServer
- The vite dev server converts
vite.config.js
to CJS module first and then loads the config from this file. - As CJS module tries to
require("@mdx-js/rollup")
plugin which is a ESM only module the error will be generated.
To solve this problem, we should inform vite to skip building config file to CJS. This can be achieved by adding
+ "type": "module",
}
to package.json
file.
Solving problem 2 - require() is not defined in ES module scope
Once we inform node to enable ES modules, we cannot use require
syntax in
.js
files. This is exactly what you'll get when you run npm run dev
file:///workspace/example/nn-blog/server/index.js:1
const express = require('express')
^
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/workspace/example/nn-blog/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
Luckily, the error itself gave us a solution. But you need to first stop
scratching your head and learn to read those lines in to identify the solution.
If you look carefully what we need is just to rename our index.js
file to
index.cjs
and 💣
Solving problem 3 - Cannot find module
node:internal/modules/cjs/loader:936
throw err;
^
Error: Cannot find module '/workspace/example/nn-blog/server'
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
at Function.Module._load (node:internal/modules/cjs/loader:778:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Wait, where is our file gone? Node says it can't find it, but it's there right in the server folder.
May be if you're patient enough or highly talented nerd enough, you'll
understand that node is trying to load server
module and not
server/index.js
. The /index.js
file comes into picture as part of the
CJS module loading sequence of node.
So, we need to add a package.json
file with the following value
{
"main": "index.cjs"
}
And ✨ congratulations, you are now ready to go.
Adding a markdown page
Now go to pages directory and any markdown content with .md
or .mdx
extention. For example, for creating a /naveennamani
root, add
pages/naveennamani.page.mdx
or pages/naveennamani/index.page.mdx
or
pages/index/naveennamani.page.mdx
file. (I prefer the last filename for this
example).
Once you create the file add any markdown content, hit
localhost:3000/naveennamani
url to see your markdown content getting converted
into html. For using react components inside your mdx files simply import them
and use.
# Hello world
import { Counter } from "./Counter";
<Counter />
This will show a heading with an interactive counter that is also shown on home page.
Prerendering and inventing new problems
When you stop the dev server and want to build your awesome website as a static
content, you can use vite-plugin-ssr prerender feature. Just add the following
script to package.json
"scripts": {
...
"prerender": "npm run build && vite-plugin-ssr prerender"
}
Now when you run npm run prerender
, you'll see that dist\client
and
dist\server
folders are created and build files are populated there. But
prerendering is failing with
/workspace/example/nn-blog/dist/server/assets/naveennamani.page.04918628.js:4
var react = require("@mdx-js/react");
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /workspace/example/nn-blog/node_modules/@mdx-js/react/index.js from /workspace/example/nn-blog/dist/server/assets/naveennamani.page.04918628.js not supported.
Isn't that the same problem we solved earlier? Yes. But why again? 😢 This time the problem is created in the following order.
- When you run
npm run build
it runsvite build
andvite build --ssr
with the first command building assets fordist\client
and second command fordist\server
. - While
dist\client
assets are allesm
modules,dist\client
build output arecjs
modules. - So, again
@mdx-js/react
which is a ESM only module is failed to import throughrequire
.
This time, we can generate ES modules instead of CJS modules by configuring
build options in vite.config.js
as follows
import react from '@vitejs/plugin-react'
import ssr from 'vite-plugin-ssr/plugin'
import mdx from "@mdx-js/rollup"
+ import { defineConfig } from 'vite'
+ export default defineConfig({
plugins: [react(), mdx({
providerImportSource: "@mdx-js/react"
}), ssr()],
+ build: {
+ rollupOptions: {
+ output: {
+ format: "es"
+ }
+ }
+ }
+ })
When you run npm run prerender
again, you can see that dist\server
folder
contains files which are ES modules. But you still get this complicated error.
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/workspace/example/nn-blog/node_modules/react/jsx-runtime.js' imported from /workspace/example/nn-blog/dist/server/assets/index.page.0262694b.js
Did you mean to import react/jsx-runtime.js.js?
Writing a vite plugin to solve our problems
At first sight, the error seems like a spelling mistake. But if you google, there is a long list of comments in the official react repo (issue #20235). The problem can be simply solved by adding .js extension to the import, but how to do that automatically?
Let's write a vite plugin to do that for us. Writing a vite plugin is very simple if you follow the Vite plugin API.
This is what I come with.
export default function fix_ssr_esm_modules(replacements) {
function transform(code, id, ssr) {
if (ssr)
// ssr is true when `vite build --ssr` is run
return replacements.reduce((prevCode, { find, replacement }) => {
return prevCode.replaceAll(find, replacement);
}, code);
}
return {
// configuration of our plugin used by vite
name: "vite-plugin-fix-ssr-esm-modules",
apply: "build", // execute only for build tasks
enforce: "post", // execute after build finished
transform: transform // transformation function that returns transformed code
};
}
Now place the code in fix_ssr_esm_modules.js file and then import and use this
plugin in vite.config.js
file as follows.
+ import fix_ssr_esm_modules from "./fix_ssr_esm_imports.js";
export default defineConfig({
plugins: [
react(),
mdx({
providerImportSource: "@mdx-js/react",
}),
ssr(),
+ fix_ssr_esm_modules([
+ { find: "react/jsx-runtime.js", replacement: "react/jsx-runtime.js.js" },
+ { find: "react-dom/server.js", replacement: "react-dom/server.js.js" },
+ ]),
],
build: {
rollupOptions: {
output: {
format: "es",
},
},
},
});
The plugin transforms the build files and replaces the import as given as options to the plugin.
Now you can run npm run prerender
and serve the files in dist\client
statically using npx serve
. Congratulations 🌟, you just built a static
site using vite-plugin-ssr.
Final touch
The final version of the source code of the project is available in github naveennamani/vite-ssr-mdx.
There is a small inconsistency with
server/index.js
file, but that's an alternative I found while I'm writing this article.
Sorry for the long post, if you come here after all, here is a potato for you.