@@ -3,8 +3,19 @@ import matter from 'gray-matter';
33import { s } from 'hastscript' ;
44import yaml from 'js-yaml' ;
55import { bundleMDX } from 'mdx-bundler' ;
6- import { access , opendir , readFile } from 'node:fs/promises' ;
6+ import { BinaryLike , createHash } from 'node:crypto' ;
7+ import { createReadStream , createWriteStream , mkdirSync } from 'node:fs' ;
8+ import { access , cp , opendir , readFile } from 'node:fs/promises' ;
79import path from 'node:path' ;
10+ // @ts -expect-error ts(2305) -- For some reason "compose" is not recognized in the types
11+ import { compose , Readable } from 'node:stream' ;
12+ import { json } from 'node:stream/consumers' ;
13+ import { pipeline } from 'node:stream/promises' ;
14+ import {
15+ constants as zlibConstants ,
16+ createBrotliCompress ,
17+ createBrotliDecompress ,
18+ } from 'node:zlib' ;
819import { limitFunction } from 'p-limit' ;
920import rehypeAutolinkHeadings from 'rehype-autolink-headings' ;
1021import rehypePresetMinify from 'rehype-preset-minify' ;
@@ -48,6 +59,33 @@ const root = process.cwd();
4859// Functions which looks like AWS Lambda and we get `EMFILE` errors when trying to open
4960// so many files at once.
5061const FILE_CONCURRENCY_LIMIT = 200 ;
62+ const CACHE_COMPRESS_LEVEL = 4 ;
63+ const CACHE_DIR = path . join ( root , '.next' , 'cache' , 'mdx-bundler' ) ;
64+ mkdirSync ( CACHE_DIR , { recursive : true } ) ;
65+
66+ const md5 = ( data : BinaryLike ) => createHash ( 'md5' ) . update ( data ) . digest ( 'hex' ) ;
67+
68+ async function readCacheFile < T > ( file : string ) : Promise < T > {
69+ const reader = createReadStream ( file ) ;
70+ const decompressor = createBrotliDecompress ( ) ;
71+
72+ return ( await json ( compose ( reader , decompressor ) ) ) as T ;
73+ }
74+
75+ async function writeCacheFile ( file : string , data : string ) {
76+ await pipeline (
77+ Readable . from ( data ) ,
78+ createBrotliCompress ( {
79+ chunkSize : 32 * 1024 ,
80+ params : {
81+ [ zlibConstants . BROTLI_PARAM_MODE ] : zlibConstants . BROTLI_MODE_TEXT ,
82+ [ zlibConstants . BROTLI_PARAM_QUALITY ] : CACHE_COMPRESS_LEVEL ,
83+ [ zlibConstants . BROTLI_PARAM_SIZE_HINT ] : data . length ,
84+ } ,
85+ } ) ,
86+ createWriteStream ( file )
87+ ) ;
88+ }
5189
5290function formatSlug ( slug : string ) {
5391 return slug . replace ( / \. ( m d x | m d ) / , '' ) ;
@@ -484,6 +522,20 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
484522 ) ;
485523 }
486524
525+ const cacheKey = md5 ( source ) ;
526+ const cacheFile = path . join ( CACHE_DIR , `${ cacheKey } .br` ) ;
527+
528+ try {
529+ const cached = await readCacheFile < SlugFile > ( cacheFile ) ;
530+ return cached ;
531+ } catch ( err ) {
532+ if ( err . code !== 'ENOENT' && err . code !== 'ABORT_ERR' && err . code !== 'Z_BUF_ERROR' ) {
533+ // If cache is corrupted, ignore and proceed
534+ // eslint-disable-next-line no-console
535+ console . warn ( `Failed to read MDX cache: ${ cacheFile } ` , err ) ;
536+ }
537+ }
538+
487539 process . env . ESBUILD_BINARY_PATH = path . join (
488540 root ,
489541 'node_modules' ,
@@ -496,6 +548,8 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
496548
497549 // cwd is how mdx-bundler knows how to resolve relative paths
498550 const cwd = path . dirname ( sourcePath ) ;
551+ const assetsCacheDir = path . join ( CACHE_DIR , cacheKey ) ;
552+ const outdir = path . join ( root , 'public' , 'mdx-images' ) ;
499553
500554 const result = await bundleMDX < Platform > ( {
501555 source,
@@ -577,9 +631,10 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
577631 // inline svgs
578632 '.svg' : 'dataurl' ,
579633 } ;
634+ options . metafile = true ;
580635 // Set the `outdir` to a public location for this bundle.
581636 // this where this images will be copied
582- options . outdir = path . join ( root , 'public' , 'mdx-images' ) ;
637+ options . outdir = assetsCacheDir ;
583638
584639 // Set write to true so that esbuild will output the files.
585640 options . write = true ;
@@ -609,6 +664,15 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
609664 } ,
610665 } ;
611666
667+ await cp ( assetsCacheDir , outdir , {
668+ recursive : true ,
669+ force : true ,
670+ } ) ;
671+ writeCacheFile ( cacheFile , JSON . stringify ( resultObj ) ) . catch ( e => {
672+ // eslint-disable-next-line no-console
673+ console . warn ( `Failed to write MDX cache: ${ cacheFile } ` , e ) ;
674+ } ) ;
675+
612676 return resultObj ;
613677}
614678
0 commit comments