|
| 1 | +import {BinaryLike, createHash} from 'crypto'; |
| 2 | + |
1 | 3 | import {cache} from 'react'; |
2 | 4 | import matter from 'gray-matter'; |
3 | 5 | import {s} from 'hastscript'; |
4 | 6 | import yaml from 'js-yaml'; |
5 | 7 | import {bundleMDX} from 'mdx-bundler'; |
| 8 | +import {createReadStream, createWriteStream, mkdirSync} from 'node:fs'; |
6 | 9 | import {access, opendir, readFile} from 'node:fs/promises'; |
7 | 10 | import path from 'node:path'; |
| 11 | +// @ts-expect-error ts(2305) -- For some reason "compose" is not recognized in the types |
| 12 | +import {compose, Readable} from 'node:stream'; |
| 13 | +import {json} from 'node:stream/consumers'; |
| 14 | +import {pipeline} from 'node:stream/promises'; |
| 15 | +import { |
| 16 | + constants as zlibConstants, |
| 17 | + createBrotliCompress, |
| 18 | + createBrotliDecompress, |
| 19 | +} from 'node:zlib'; |
8 | 20 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'; |
9 | 21 | import rehypePresetMinify from 'rehype-preset-minify'; |
10 | 22 | import rehypePrismDiff from 'rehype-prism-diff'; |
@@ -41,6 +53,33 @@ type SlugFile = { |
41 | 53 | }; |
42 | 54 |
|
43 | 55 | const root = process.cwd(); |
| 56 | +const CACHE_COMPRESS_LEVEL = 4; |
| 57 | +const CACHE_DIR = path.join(root, '.next', 'cache', 'mdx-bundler'); |
| 58 | +mkdirSync(CACHE_DIR, {recursive: true}); |
| 59 | + |
| 60 | +const md5 = (data: BinaryLike) => createHash('md5').update(data).digest('hex'); |
| 61 | + |
| 62 | +async function readCacheFile<T>(file: string): Promise<T> { |
| 63 | + const reader = createReadStream(file); |
| 64 | + const decompressor = createBrotliDecompress(); |
| 65 | + |
| 66 | + return (await json(compose(reader, decompressor))) as T; |
| 67 | +} |
| 68 | + |
| 69 | +async function writeCacheFile(file: string, data: string) { |
| 70 | + await pipeline( |
| 71 | + Readable.from(data), |
| 72 | + createBrotliCompress({ |
| 73 | + chunkSize: 32 * 1024, |
| 74 | + params: { |
| 75 | + [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT, |
| 76 | + [zlibConstants.BROTLI_PARAM_QUALITY]: CACHE_COMPRESS_LEVEL, |
| 77 | + [zlibConstants.BROTLI_PARAM_SIZE_HINT]: data.length, |
| 78 | + }, |
| 79 | + }), |
| 80 | + createWriteStream(file) |
| 81 | + ); |
| 82 | +} |
44 | 83 |
|
45 | 84 | function formatSlug(slug: string) { |
46 | 85 | return slug.replace(/\.(mdx|md)/, ''); |
@@ -436,6 +475,24 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> { |
436 | 475 | ); |
437 | 476 | } |
438 | 477 |
|
| 478 | + let cacheKey: string | null = null; |
| 479 | + let cacheFile: string | null = null; |
| 480 | + if (process.env.CI === '1') { |
| 481 | + cacheKey = md5(source); |
| 482 | + cacheFile = path.join(CACHE_DIR, cacheKey); |
| 483 | + |
| 484 | + try { |
| 485 | + const cached = await readCacheFile<SlugFile>(cacheFile); |
| 486 | + return cached; |
| 487 | + } catch (err) { |
| 488 | + if (err.code !== 'ENOENT' && err.code !== 'ABORT_ERR') { |
| 489 | + // If cache is corrupted, ignore and proceed |
| 490 | + // eslint-disable-next-line no-console |
| 491 | + console.warn(`Failed to read MDX cache: ${cacheFile}`, err); |
| 492 | + } |
| 493 | + } |
| 494 | + } |
| 495 | + |
439 | 496 | process.env.ESBUILD_BINARY_PATH = path.join( |
440 | 497 | root, |
441 | 498 | 'node_modules', |
@@ -561,6 +618,13 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> { |
561 | 618 | }, |
562 | 619 | }; |
563 | 620 |
|
| 621 | + if (cacheFile) { |
| 622 | + writeCacheFile(cacheFile, JSON.stringify(resultObj)).catch(e => { |
| 623 | + // eslint-disable-next-line no-console |
| 624 | + console.warn(`Failed to write MDX cache: ${cacheFile}`, e); |
| 625 | + }); |
| 626 | + } |
| 627 | + |
564 | 628 | return resultObj; |
565 | 629 | } |
566 | 630 |
|
|
0 commit comments