├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── babel.config.cjs ├── funding.yml ├── index.css ├── license ├── node-loader-wo-react.config.js ├── node-loader.config.js ├── og.png ├── package.json ├── patches ├── png2bin+0.2.3.patch └── react-server-dom-webpack+0.0.0-experimental-7ec4c5597.patch ├── readme.md ├── screenshot.png ├── script ├── bundle.js ├── generate.server.js └── prerender.js └── src ├── content.server.mdx ├── counter.client.js ├── index.client.js ├── phyllotaxis.server.js └── root.client.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | persist-credentials: false 13 | - uses: dcodeIO/setup-node-nvm@master 14 | with: 15 | node-version: node 16 | - run: npm install --legacy-peer-deps 17 | - run: npm run build 18 | - uses: JamesIves/github-pages-deploy-action@3.7.1 19 | with: 20 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 21 | BRANCH: dist 22 | FOLDER: build 23 | SINGLE_COMMIT: true 24 | COMMIT_MESSAGE: . 25 | GIT_CONFIG_NAME: Titus Wormer 26 | GIT_CONFIG_EMAIL: tituswormer@gmail.com 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | build/ 4 | node_modules/ 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.json 3 | *.mdx 4 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-react', {runtime: 'automatic'}]] 3 | } 4 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ff-sans: system-ui; 3 | --ff-mono: 'San Francisco Mono', 'Monaco', 'Consolas', 'Lucida Console', 4 | 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 5 | 6 | --gray-0: #fafbfc; 7 | --gray-2: #e1e4e8; 8 | --gray-4: #959da5; 9 | --gray-6: #586069; 10 | --gray-8: #2f363d; 11 | --gray-10: #1b1f23; 12 | 13 | --blue-4: #2188ff; 14 | --blue-5: #0366d6; 15 | --hl: var(--blue-5); 16 | } 17 | 18 | * { 19 | line-height: calc(1em + 1ex); 20 | box-sizing: border-box; 21 | } 22 | 23 | html { 24 | color-scheme: light dark; 25 | -ms-text-size-adjust: 100%; 26 | -webkit-text-size-adjust: 100%; 27 | word-wrap: break-word; 28 | font-kerning: normal; 29 | font-family: var(--ff-sans); 30 | font-feature-settings: 'kern', 'liga', 'clig', 'calt'; 31 | } 32 | 33 | button, 34 | input { 35 | font-family: inherit; 36 | font-size: inherit; 37 | } 38 | 39 | kbd, 40 | pre, 41 | code { 42 | font-family: var(--ff-mono); 43 | font-feature-settings: normal; 44 | font-size: smaller; 45 | } 46 | 47 | body { 48 | background-color: var(--gray-0); 49 | margin: 0; 50 | } 51 | 52 | main { 53 | width: 100%; 54 | max-width: calc(36 * (1em + 1ex)); 55 | margin: calc(2em + 2ex) auto; 56 | padding: 0 calc(1em + 1ex); 57 | } 58 | 59 | h1, 60 | h2, 61 | h3, 62 | h4, 63 | h5, 64 | h6, 65 | strong, 66 | th { 67 | font-weight: 600; 68 | letter-spacing: 0.0125em; 69 | } 70 | 71 | h1, 72 | h2, 73 | h3, 74 | h4, 75 | h5, 76 | h6, 77 | p, 78 | ol, 79 | ul, 80 | hr, 81 | pre, 82 | table, 83 | blockquote { 84 | margin: calc(1em + 1ex) 0; 85 | } 86 | 87 | summary { 88 | cursor: pointer; 89 | } 90 | 91 | h1 { 92 | font-size: 2.5em; 93 | margin-top: calc(0.4em + 0.7ex); 94 | margin-bottom: calc(0.4em + 0.7ex); 95 | padding-bottom: calc(0.2em + 0.2ex); 96 | } 97 | 98 | h2 { 99 | font-size: 2em; 100 | margin-top: calc(0.5 * (1em + 1.25ex)); 101 | margin-bottom: calc(0.5 * (1em + 1.25ex)); 102 | padding-bottom: calc(0.25 * (1em + 1ex)); 103 | } 104 | 105 | h1, 106 | h2 { 107 | border-bottom: 1px solid var(--gray-2); 108 | } 109 | 110 | h3 { 111 | font-size: 1.5em; 112 | margin-top: calc(0.6667 * (1em + 1.16667ex)); 113 | margin-bottom: calc(0.6667 * (1em + 1.16667ex)); 114 | } 115 | 116 | h4 { 117 | font-size: 1.25em; 118 | margin-top: calc(0.8 * (1em + 1.1ex)); 119 | margin-bottom: calc(0.8 * (1em + 1.1ex)); 120 | } 121 | 122 | h6 { 123 | color: var(--gray-4); 124 | } 125 | 126 | img, 127 | svg { 128 | max-width: 100%; 129 | background-color: transparent; 130 | } 131 | 132 | img[align='right'] { 133 | padding-left: calc(1em + 1ex); 134 | } 135 | 136 | img[align='left'] { 137 | padding-right: calc(1em + 1ex); 138 | } 139 | 140 | kbd { 141 | background-color: var(--gray-0); 142 | border: 1px solid var(--gray-2); 143 | border-radius: 3px; 144 | box-shadow: inset 0 -1px 0 var(--gray-4); 145 | color: var(--gray-8); 146 | padding: 0.2em 0.4em; 147 | vertical-align: middle; 148 | } 149 | 150 | pre { 151 | word-wrap: normal; 152 | background-color: rgba(0, 0, 0, 0.04); 153 | overflow: auto; 154 | padding: calc(1em + 1ex); 155 | margin-left: calc(-1 * (1em + 1ex)); 156 | margin-right: calc(-1 * (1em + 1ex)); 157 | font-size: inherit; 158 | } 159 | 160 | blockquote pre, 161 | li pre { 162 | border-radius: 3px; 163 | margin-left: 0; 164 | margin-right: 0; 165 | } 166 | 167 | code { 168 | background-color: rgba(0, 0, 0, 0.04); 169 | border-radius: 3px; 170 | padding: 0.2em 0.4em; 171 | } 172 | 173 | pre code { 174 | background-color: transparent; 175 | padding: 0; 176 | white-space: pre; 177 | word-break: normal; 178 | overflow: visible; 179 | word-wrap: normal; 180 | } 181 | 182 | hr { 183 | background-color: rgba(0, 0, 0, 0.04); 184 | border: 0; 185 | border-radius: 3px; 186 | height: calc(0.25 * (1em + 1ex)); 187 | } 188 | 189 | table { 190 | border-collapse: collapse; 191 | border-spacing: 0; 192 | display: block; 193 | overflow: auto; 194 | width: 100%; 195 | font-variant-numeric: lining-nums; 196 | } 197 | 198 | tr { 199 | background-color: var(--gray-0); 200 | border-top: 1px solid var(--gray-4); 201 | } 202 | 203 | tr:nth-child(2n) { 204 | background-color: var(--gray-2); 205 | } 206 | 207 | td, 208 | th { 209 | border: 1px solid var(--gray-4); 210 | padding: 0.4em 0.8em; 211 | } 212 | 213 | blockquote { 214 | color: var(--gray-8); 215 | padding-left: calc(1 * (1em + 1ex)); 216 | position: relative; 217 | } 218 | 219 | blockquote::before { 220 | content: ''; 221 | display: block; 222 | width: calc(0.25 * (1em + 1ex)); 223 | height: 100%; 224 | background-color: var(--gray-2); 225 | border-radius: 3px; 226 | position: absolute; 227 | left: 0; 228 | } 229 | 230 | ol, 231 | ul { 232 | padding-left: 0; 233 | } 234 | 235 | ul ul, 236 | ol ul, 237 | ul ol, 238 | ol ol { 239 | margin-top: 0; 240 | margin-bottom: 0; 241 | } 242 | 243 | ul { 244 | list-style-type: circle; 245 | } 246 | 247 | ol { 248 | list-style-type: decimal; 249 | } 250 | 251 | ul ul { 252 | list-style-type: disc; 253 | } 254 | 255 | ul ul ul { 256 | list-style-type: square; 257 | } 258 | 259 | ol ol { 260 | list-style-type: lower-roman; 261 | } 262 | 263 | ol ol ol { 264 | list-style-type: lower-alpha; 265 | } 266 | 267 | li { 268 | word-wrap: break-all; 269 | margin-top: calc(0.25 * (1em + 1ex)); 270 | margin-bottom: calc(0.25 * (1em + 1ex)); 271 | margin-left: calc(1 * (1em + 1ex)); 272 | } 273 | 274 | .task-list-item { 275 | list-style-type: none; 276 | margin-left: 0; 277 | } 278 | 279 | .task-list-item input { 280 | margin: 0; 281 | margin-right: calc(0.25 * (1em + 1ex)); 282 | } 283 | 284 | dt { 285 | font-style: italic; 286 | margin-bottom: 0; 287 | } 288 | 289 | dt + dt { 290 | margin-top: 0; 291 | } 292 | 293 | dd { 294 | margin-top: 0; 295 | padding: 0 calc(1em + 1ex); 296 | } 297 | 298 | a { 299 | color: var(--hl); 300 | transition: 200ms; 301 | transition-property: color; 302 | } 303 | 304 | a:hover, 305 | a:focus { 306 | color: inherit; 307 | } 308 | 309 | button { 310 | outline: 0; 311 | padding: calc(0.25 * (1em + 1ex)) calc(0.5 * (1em + 1ex)); 312 | border-radius: 3px; 313 | border: 1px solid var(--gray-2); 314 | color: var(--gray-10); 315 | transition: 200ms; 316 | transition-property: color, background-color, border-color, box-shadow; 317 | font-weight: 400; 318 | background-color: var(--gray-0); 319 | } 320 | 321 | button:hover, 322 | button:focus, 323 | button:active { 324 | border-color: var(--hl); 325 | } 326 | 327 | button:active { 328 | background-color: var(--hl); 329 | color: var(--gray-0); 330 | } 331 | 332 | button:active, 333 | button:focus { 334 | box-shadow: 0 0 0 0.2em rgba(3, 102, 214, 0.3); /* --blue-5 */ 335 | } 336 | 337 | /* Fix that confetti. */ 338 | canvas { 339 | position: fixed !important; 340 | inset: 0 !important; 341 | } 342 | 343 | #payload { 344 | margin: calc(1em + 1em) auto; 345 | } 346 | 347 | @media (prefers-color-scheme: dark) { 348 | :root { 349 | --hl: var(--blue-4); 350 | } 351 | 352 | body { 353 | background-color: var(--gray-10); 354 | color: var(--gray-2); 355 | } 356 | 357 | h1, 358 | h2 { 359 | border-bottom-color: var(--gray-6); 360 | } 361 | 362 | kbd { 363 | background-color: rgba(0, 0, 0, 0.2); 364 | border-color: var(--gray-8); 365 | box-shadow: inset 0 -1px 0 black; 366 | color: var(--gray-2); 367 | } 368 | 369 | pre { 370 | background-color: rgba(0, 0, 0, 0.2); 371 | } 372 | 373 | code { 374 | background-color: rgba(0, 0, 0, 0.2); 375 | } 376 | 377 | hr { 378 | background-color: rgba(0, 0, 0, 0.2); 379 | } 380 | 381 | tr { 382 | background-color: var(--gray-10); 383 | border-top-color: var(--gray-6); 384 | } 385 | 386 | tr:nth-child(2n) { 387 | background-color: var(--gray-8); 388 | } 389 | 390 | td, 391 | th { 392 | border-color: var(--gray-6); 393 | } 394 | 395 | blockquote { 396 | color: var(--gray-4); 397 | } 398 | 399 | blockquote::before { 400 | background-color: rgba(0, 0, 0, 0.2); 401 | } 402 | 403 | button { 404 | color: var(--gray-0); 405 | border-color: currentcolor; 406 | background-color: transparent; 407 | } 408 | 409 | button:hover, 410 | button:focus, 411 | button:active { 412 | border-color: var(--hl); 413 | } 414 | 415 | button:active { 416 | background-color: var(--hl); 417 | color: var(--gray-0); 418 | } 419 | } 420 | 421 | @media (min-width: 40em) { 422 | html { 423 | font-size: 1.125em; 424 | } 425 | } 426 | 427 | @media (min-width: 64em) { 428 | blockquote, 429 | ol, 430 | ul { 431 | margin-left: calc(-1 * (1em + 1ex)); 432 | } 433 | 434 | ol blockquote, 435 | ul blockquote, 436 | blockquote blockquote, 437 | ol ol, 438 | ul ol, 439 | blockquote ol, 440 | ol ul, 441 | ul ul, 442 | blockquote ul { 443 | margin-left: 0; 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Titus Wormer 4 | Copyright (c) Facebook, Inc. and its affiliates. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /node-loader-wo-react.config.js: -------------------------------------------------------------------------------- 1 | import * as xdm from 'xdm/esm-loader.js' 2 | import * as babel from '@node-loader/babel' 3 | 4 | const loader = {loaders: [xdm, babel]} 5 | 6 | export default loader 7 | -------------------------------------------------------------------------------- /node-loader.config.js: -------------------------------------------------------------------------------- 1 | import * as xdm from 'xdm/esm-loader.js' 2 | import * as babel from '@node-loader/babel' 3 | import * as serverDomWebpack from 'react-server-dom-webpack/node-loader' 4 | 5 | const loader = {loaders: [serverDomWebpack, xdm, babel]} 6 | 7 | export default loader 8 | -------------------------------------------------------------------------------- /og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooorm/server-components-mdx-demo/8a81109c591c483ddebd65b7da1f540f24b41458/og.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "server-components-mdx-demo", 4 | "version": "0.0.0", 5 | "description": "React server components + MDX", 6 | "license": "MIT", 7 | "keywords": [ 8 | "xdm", 9 | "mdx", 10 | "markdown", 11 | "jsx" 12 | ], 13 | "repository": "wooorm/server-components-mdx-demo", 14 | "bugs": "https://github.com/wooorm/server-components-mdx-demo/issues", 15 | "homepage": "https://wooorm.com/server-components-mdx-demo/", 16 | "funding": { 17 | "type": "github", 18 | "url": "https://github.com/sponsors/wooorm" 19 | }, 20 | "type": "module", 21 | "files": [ 22 | "src/" 23 | ], 24 | "engines": { 25 | "node": ">=12.0.0" 26 | }, 27 | "dependencies": { 28 | "@babel/core": "^7.0.0", 29 | "@node-loader/babel": "^1.0.0", 30 | "@node-loader/core": "^1.0.0", 31 | "babel-loader": "^8.0.0", 32 | "babel-preset-react-app": "^10.0.0", 33 | "bin2png": "^0.2.3", 34 | "patch-package": "^6.0.0", 35 | "png2bin": "^0.2.3", 36 | "react": "0.0.0-experimental-7ec4c5597", 37 | "react-confetti": "^6.0.0", 38 | "react-dom": "0.0.0-experimental-7ec4c5597", 39 | "react-fetch": "0.0.0-experimental-7ec4c5597", 40 | "react-server-dom-webpack": "0.0.0-experimental-7ec4c5597", 41 | "webpack": "^4.0.0", 42 | "xdm": "^2.0.0" 43 | }, 44 | "devDependencies": { 45 | "eslint-config-xo-react": "^0.25.0", 46 | "eslint-plugin-react": "^7.0.0", 47 | "eslint-plugin-react-hooks": "^4.0.0", 48 | "prettier": "^2.0.0", 49 | "xo": "^0.44.0" 50 | }, 51 | "scripts": { 52 | "postinstall": "patch-package", 53 | "build:dev": "NODE_ENV=development node script/bundle.js && node --no-warnings --experimental-loader @node-loader/core --conditions=react-server script/generate.server.js && NODE_LOADER_CONFIG=node-loader-wo-react.config.js node --no-warnings --experimental-loader @node-loader/core script/prerender.js", 54 | "build:prod": "NODE_ENV=production node script/bundle.js && node --no-warnings --experimental-loader @node-loader/core --conditions=react-server script/generate.server.js && NODE_LOADER_CONFIG=node-loader-wo-react.config.js node --no-warnings --experimental-loader @node-loader/core script/prerender.js", 55 | "build": "npm run build:prod", 56 | "format": "prettier . -w --loglevel warn && xo --fix" 57 | }, 58 | "prettier": { 59 | "tabWidth": 2, 60 | "useTabs": false, 61 | "singleQuote": true, 62 | "bracketSpacing": false, 63 | "semi": false, 64 | "trailingComma": "none" 65 | }, 66 | "xo": { 67 | "envs": [ 68 | "shared-node-browser" 69 | ], 70 | "extends": "xo-react", 71 | "extensions": [ 72 | "cjs" 73 | ], 74 | "prettier": true, 75 | "rules": { 76 | "camelcase": "off", 77 | "node/file-extension-in-import": "off", 78 | "react/prop-types": "off", 79 | "react/react-in-jsx-scope": "off", 80 | "react/function-component-definition": [ 81 | "error", 82 | { 83 | "namedComponents": "function-declaration", 84 | "unnamedComponents": "function-expression" 85 | } 86 | ] 87 | }, 88 | "overrides": [ 89 | { 90 | "files": [ 91 | "src/**/*.client.js" 92 | ], 93 | "envs": [ 94 | "browser" 95 | ] 96 | } 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /patches/png2bin+0.2.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/png2bin/png2bin.js b/node_modules/png2bin/png2bin.js 2 | index b4be423..ce4e7da 100644 3 | --- a/node_modules/png2bin/png2bin.js 4 | +++ b/node_modules/png2bin/png2bin.js 5 | @@ -8,8 +8,8 @@ async function png2bin(imgEl) { 6 | var document = getDocument(imgEl); 7 | var canvas = document.createElement("canvas"); 8 | var img = await imgLoad(imgEl); 9 | - canvas.width = img.width; 10 | - canvas.height = img.height; 11 | + canvas.width = img.naturalWidth; 12 | + canvas.height = img.naturalHeight; 13 | var ctx = canvas.getContext("2d"); 14 | ctx.drawImage(img, 0, 0); 15 | var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; 16 | -------------------------------------------------------------------------------- /patches/react-server-dom-webpack+0.0.0-experimental-7ec4c5597.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js b/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js 2 | index 184ed95..02b0f90 100644 3 | --- a/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js 4 | +++ b/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js 5 | @@ -68,7 +68,7 @@ module.exports = function register() { 6 | } 7 | }; 8 | 9 | - require.extensions['.client.js'] = function (module, path) { 10 | + require.extensions['.client.mdx'] = require.extensions['.client.js'] = function (module, path) { 11 | var moduleId = url.pathToFileURL(path).href; 12 | var moduleReference = { 13 | $$typeof: MODULE_REFERENCE, 14 | @@ -84,11 +84,11 @@ module.exports = function register() { 15 | Module._resolveFilename = function (request, parent, isMain, options) { 16 | var resolved = originalResolveFilename.apply(this, arguments); 17 | 18 | - if (resolved.endsWith('.server.js')) { 19 | - if (parent && parent.filename && !parent.filename.endsWith('.server.js')) { 20 | + if (resolved.endsWith('.server.mdx') || resolved.endsWith('.server.js')) { 21 | + if (parent && parent.filename && !parent.filename.endsWith('.server.mdx') && !parent.filename.endsWith('.server.js')) { 22 | var reason; 23 | 24 | - if (request.endsWith('.server.js')) { 25 | + if (request.endsWith('.server.mdx') || request.endsWith('.server.js')) { 26 | reason = "\"" + request + "\""; 27 | } else { 28 | reason = "\"" + request + "\" (which expands to \"" + resolved + "\")"; 29 | diff --git a/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js b/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js 30 | index 01e3fcf..0551267 100644 31 | --- a/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js 32 | +++ b/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js 33 | @@ -58,7 +58,7 @@ class ReactFlightWebpackPlugin { 34 | this.clientReferences = [{ 35 | directory: '.', 36 | recursive: true, 37 | - include: /\.client\.(js|ts|jsx|tsx)$/ 38 | + include: /\.client\.(js|ts|jsx|tsx|mdx)$/ 39 | }]; 40 | } else if (typeof options.clientReferences === 'string' || !isArray(options.clientReferences)) { 41 | this.clientReferences = [options.clientReferences]; 42 | @@ -146,7 +146,7 @@ class ReactFlightWebpackPlugin { 43 | // TODO: Hook into deps instead of the target module. 44 | // That way we know by the type of dep whether to include. 45 | // It also resolves conflicts when the same module is in multiple chunks. 46 | - if (!/\.client\.js$/.test(mod.resource)) { 47 | + if (!/\.client\.(js|mdx)$/.test(mod.resource)) { 48 | return; 49 | } 50 | 51 | diff --git a/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js b/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js 52 | index 843eb86..1379126 100644 53 | --- a/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js 54 | +++ b/node_modules/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js 55 | @@ -72,13 +72,13 @@ async function resolve(specifier, context, defaultResolve) { 56 | 57 | var resolved = await defaultResolve(specifier, context, defaultResolve); 58 | 59 | - if (resolved.url.endsWith('.server.js')) { 60 | + if (resolved.url.endsWith('.server.mdx') || resolved.url.endsWith('.server.js')) { 61 | var parentURL = context.parentURL; 62 | 63 | - if (parentURL && !parentURL.endsWith('.server.js')) { 64 | + if (parentURL && !parentURL.endsWith('.server.mdx') && !parentURL.endsWith('.server.js')) { 65 | var reason; 66 | 67 | - if (specifier.endsWith('.server.js')) { 68 | + if (specifier.endsWith('.server.mdx') || specifier.endsWith('.server.js')) { 69 | reason = "\"" + specifier + "\""; 70 | } else { 71 | reason = "\"" + specifier + "\" (which expands to \"" + resolved.url + "\")"; 72 | @@ -233,7 +233,7 @@ async function parseExportNamesInto(transformedSource, names, parentURL, default 73 | async function transformSource(source, context, defaultTransformSource) { 74 | var transformed = await defaultTransformSource(source, context, defaultTransformSource); 75 | 76 | - if (context.format === 'module' && context.url.endsWith('.client.js')) { 77 | + if (context.format === 'module' && (context.url.endsWith('.client.mdx') || context.url.endsWith('.client.js'))) { 78 | var transformedSource = transformed.source; 79 | 80 | if (typeof transformedSource !== 'string') { 81 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # server-components-mdx-demo 2 | 3 | [![screenshot](screenshot.png)](https://wooorm.com/server-components-mdx-demo/) 4 | 5 | [Go to demo »](https://wooorm.com/server-components-mdx-demo/) 6 | 7 | ## Build 8 | 9 | Clone this repo, `cd` into it, make sure you’re using Node 12+, and then: 10 | 11 | ```sh 12 | npm i --legacy-peer-deps 13 | npm run build 14 | ``` 15 | 16 | ## Run 17 | 18 | Then start an HTTP server in `build/`. 19 | 20 | ```sh 21 | npx serve build/ 22 | ``` 23 | 24 | ## License 25 | 26 | MIT 27 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooorm/server-components-mdx-demo/8a81109c591c483ddebd65b7da1f540f24b41458/screenshot.png -------------------------------------------------------------------------------- /script/bundle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'node:fs' 3 | import url from 'node:url' 4 | import path from 'node:path' 5 | import process from 'node:process' 6 | import webpack from 'webpack' 7 | import ReactServerWebpackPlugin from 'react-server-dom-webpack/plugin' 8 | 9 | const production = process.env.NODE_ENV === 'production' 10 | 11 | fs.mkdirSync('build', {recursive: true}) 12 | fs.copyFileSync('index.css', 'build/index.css') 13 | fs.copyFileSync('og.png', 'build/og.png') 14 | 15 | webpack( 16 | { 17 | mode: production ? 'production' : 'development', 18 | devtool: production ? 'source-map' : 'cheap-module-source-map', 19 | entry: [ 20 | path.resolve( 21 | url.fileURLToPath(import.meta.url), 22 | '../../src/index.client.js' 23 | ) 24 | ], 25 | output: { 26 | path: path.resolve(url.fileURLToPath(import.meta.url), '../../build'), 27 | filename: 'index.js' 28 | }, 29 | module: { 30 | rules: [ 31 | {test: /\.mdx$/, use: 'xdm/webpack.cjs'}, 32 | {test: /\.js$/, use: 'babel-loader', exclude: /node_modules/} 33 | ] 34 | }, 35 | plugins: [new ReactServerWebpackPlugin({isServer: false})] 36 | }, 37 | onbundle 38 | ) 39 | 40 | function onbundle(error, stats) { 41 | const info = stats && stats.toJson() 42 | 43 | if (error) throw error 44 | 45 | if (stats.hasErrors()) { 46 | for (error of info.errors) console.error(error) 47 | throw new Error('Finished running webpack with errors') 48 | } 49 | 50 | console.log('Bundled w/ webpack') 51 | } 52 | -------------------------------------------------------------------------------- /script/generate.server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'node:fs' 3 | import process from 'node:process' 4 | import React from 'react' 5 | import {pipeToNodeWritable} from 'react-server-dom-webpack/writer' 6 | import Content from '../src/content.server.mdx' 7 | 8 | const manifest = fs.readFileSync('build/react-client-manifest.json') 9 | 10 | pipeToNodeWritable( 11 | React.createElement(Content), 12 | fs.createWriteStream('build/content.nljson'), 13 | JSON.parse(manifest) 14 | ) 15 | 16 | process.on('exit', () => console.log('Generated content')) 17 | -------------------------------------------------------------------------------- /script/prerender.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {Buffer} from 'node:buffer' 3 | import {promises as fs} from 'node:fs' 4 | import path from 'node:path' 5 | import url from 'node:url' 6 | import React from 'react' 7 | import {renderToString} from 'react-dom/server.js' 8 | import {createFromReadableStream} from 'react-server-dom-webpack' 9 | import {bin2png} from 'bin2png' 10 | import {Root} from '../src/root.client.js' 11 | 12 | main() 13 | 14 | async function main() { 15 | const buf = await fs.readFile('build/content.nljson') 16 | const data = JSON.parse(await fs.readFile('build/react-client-manifest.json')) 17 | const b64 = Buffer.from(await bin2png(buf)).toString('base64') 18 | const ignore = new Set(['index.client.js', 'root.client.js']) 19 | 20 | // We have to fake webpack for SSR. 21 | // Luckily only a few parts of its API need to be faked. 22 | const cache = {} 23 | global.__webpack_require__ = (id) => cache[id] 24 | global.__webpack_chunk_load__ = () => Promise.resolve() 25 | 26 | // Populate the cache with all client modules. 27 | await Promise.all( 28 | Object.keys(data) 29 | .filter((d) => !ignore.has(path.basename(d))) 30 | .map(async (d) => { 31 | // eslint-disable-next-line node/no-unsupported-features/es-syntax 32 | cache[data[d]['*'].id] = await import(url.fileURLToPath(d)) 33 | }) 34 | ) 35 | 36 | // Create a browser stream that RSC needs for getting it’s content. 37 | const response = createFromReadableStream({ 38 | getReader() { 39 | const enc = new TextEncoder() 40 | let done 41 | return { 42 | read() { 43 | if (done) return Promise.resolve({done}) 44 | done = true 45 | return Promise.resolve({value: enc.encode(String(buf))}) 46 | } 47 | } 48 | } 49 | }) 50 | 51 | // Finally, actually perform the SSR, retrying if there is anything suspended. 52 | let result 53 | 54 | /* eslint-disable no-constant-condition, no-await-in-loop */ 55 | while (true) { 56 | result = renderToString(React.createElement(Root, {response})) 57 | if (!result.includes('')) break 58 | await sleep(64) 59 | } 60 | /* eslint-enable no-constant-condition, no-await-in-loop */ 61 | 62 | await fs.writeFile( 63 | 'build/index.html', 64 | ` 65 | 66 | 67 | 68 | 69 | React server components + MDX | wooorm.com 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
${result}
93 | 94 | ` 95 | ) 96 | 97 | console.log('Prerendered content') 98 | } 99 | 100 | function sleep(ms) { 101 | return new Promise(executor) 102 | function executor(resolve) { 103 | setTimeout(resolve, ms) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/content.server.mdx: -------------------------------------------------------------------------------- 1 | import {Counter} from './counter.client.js' 2 | import {Phyllotaxis} from './phyllotaxis.server.js' 3 | 4 | export default function wrapper({components, ...props}) { 5 | return
6 | } 7 | 8 | # React server components + MDX 9 | 10 | Hi! 👋 11 | This is a demo of [React server components][sc] with MDX to show that they can 12 | work together. 13 | The source code is [on GitHub][gh]. 14 | 15 | What you’re reading right now is \<[Content][] \/\>, an MDX file as 16 | a component! 17 | 18 | > 💁‍♀️ It’s going to take like at least a year for the stuff discussed and shown 19 | > here to become stable.\ 20 | > This demo is showing cool stuff coming in the future, don’t depend on this! 21 | 22 | ## What? 23 | 24 | Server components can be boiled down to doing the work on a server and 25 | being very smart about which components and data are sent to the client. 26 | It’s a rather seamless authoring experience. 27 | This demo is compiled ahead of time so “server” can also be a build step. 28 | MDX is a mix between markdown and JSX. 29 | It’s a great way for combining content with components. 30 | 31 | For an example, what you’re reading here is MDX as a server component (so it’s 32 | compiled on the server), but it includes dynamic client components. 33 | Try it out: 34 | 35 | . 36 | 37 | The above \<[Counter][] \/\> is a client component: it’s sent to 38 | the client. 39 | As this whole MDX document is a server component, it can include more server 40 | components. 41 | Here is an example of a [phyllotaxis][phyllotaxis-wiki] as a server component, 42 | \<[Phyllotaxis][] \/\>, which is static: 43 | 44 | 45 | 46 | Whether an `.mdx` (or `.js`) file is a *server* or *client* component is 47 | defined by their extension (respectively, `xxx.server.{js,mdx}` or 48 | `xxx.client.{js,mdx}`). 49 | You can open your dev tools to see that only the `` and the embedded 50 | `` are sent to clients (in `0.index.js`). 51 | The rest is compiled away. 52 | 53 | So how is this different from MDX, currently? 54 | Well: an `.mdx` file is treated like any other component right now. 55 | So sure, it can be rendered on the server, but then it can’t include interactive 56 | things (such as ``). 57 | Or it can be rendered on the client, but then a lot of otherwise static things 58 | (`` and all these paragraphs) are rendered there, too. 59 | This is solved with RSC. 60 | 61 | ## Why? 62 | 63 | React server components run on the server and have zero impact on bundle size. 64 | They seamlessly integrate with client components. 65 | The hot sauce™ (as in, tooling) that combines them results in a perfect hybrid 66 | blend. 67 | MDX is nice on top for more content heavy things, because it’s so much nicer to 68 | write `*emphasis*` than `emphasis` for pages such as this one. 69 | 70 | ## How? 71 | 72 | First, `react-server-dom-webpack` needs to be [patched][] to treat MDX files the 73 | way it treats JS files. 74 | Hopefully the React team will allow `.mdx` or make extensions configurable. 75 | 76 | The second step makes sure you can require/import `.mdx` files in Node and 77 | depends on whether you’re using CJS or ESM. 78 | In CJS (which has `require` calls, *discouraged*), add a 79 | require('[xdm/register.cjs][register]') call somewhere in your 80 | server next to 81 | where you’re doing `require('react-server-dom-webpack/node-register')`. 82 | In ESM (*recommended*), import [`xdm/esm-loader.js`][loader], 83 | `@node-loader/babel`, and `react-server-dom-webpack/node-loader`, then combine 84 | them in a `node-loader.config.js` [like so][node-loader], and finally run your 85 | server with `--experimental-loader @node-loader/core`. 86 | 87 | Lastly, make sure webpack can bundle `.mdx` files. 88 | Add something along the lines of \{test: /\\.mdx$/, use: 89 | '[xdm/webpack.cjs][webpack]'} to `module.rules` in the webpack config. 90 | 91 | ## A few more things! 92 | 93 | > 💁‍♀️ This section walks through some things I added which are currently complex 94 | > to solve with RSC. 95 | > The wiring I’m doing here is supposed to be **much easier** when RSC ships, 96 | > and/or **handled for you** by something like Next. 97 | 98 | The [React notes][scd] demo by the React team sends an empty HTML shell to the 99 | client, just like a client-only app. 100 | The intended solution (which isn’t built out yet) is to send rendered HTML, 101 | which is then hydrated by the client. 102 | This demo shows that that can work: this page is rendered on the server and 103 | later hydrated, so that users (even those without JavaScript on) can immediately 104 | start reading. 🕚 ⬅️ 🚙 💨 105 | 106 | The first thing the client in React notes does is ask the server: “hey, what 107 | data and components do I need to render this page?” 108 | The server responds by streaming the needed data and URLs for the needed 109 | components. 110 | Can that first network request be removed? 111 | So I tried embedded that first response *into* the HTML to save a roundtrip. 112 | One idea on how to do it, is to embed the payload in a 113 | ``. 114 | It’s a pretty good idea but there’s a big catch: you can’t include the 115 | characters `` inside a script. 116 | Character references (`<`) don’t work in them either. 117 | So that didn’t work. 118 | Instead, this demo embeds the payload at the bottom of the page in hidden PNG 119 | image (🤯). 120 | Using a PNG compresses a bit worse than a `