Skip to content

Commit 758de93

Browse files
BYKCopilot
authored andcommitted
ci: Bring back MDX caching with public folder cache (#14187)
Old version ignored the assets put under `public/mdx-images` causing missing images. This version fixes the issue and brings build times back down to around 11 minutes. May need a cache-busting redeploy once this merges (seen issues on `develop-docs`) but once we do that, the errors should go away. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 070bf5b commit 758de93

File tree

3 files changed

+98
-9
lines changed

3 files changed

+98
-9
lines changed

.gitignore

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ npm-debug.log*
44
yarn-debug.log*
55
yarn-error.log*
66

7-
# Ignore generated export markdown files
8-
/public/md-exports/
9-
107
# Runtime data
118
pids
129
*.pid
@@ -96,6 +93,8 @@ public/page-data
9693
# tsbuildinfo file generated by CI
9794
tsconfig.tsbuildinfo
9895

96+
# Ignore generated files
97+
/public/md-exports/
9998
public/mdx-images/*
10099

101100
# yalc

scripts/generate-md-exports.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async function createWork() {
110110
})
111111
);
112112
continuationToken = response.NextContinuationToken;
113-
for (const {Key, ETag} of response.Contents) {
113+
for (const {Key, ETag} of response.Contents || []) {
114114
existingFilesOnR2.set(Key, ETag.slice(1, -1)); // Remove quotes from ETag
115115
}
116116
} while (continuationToken);

src/mdx.ts

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import {cache} from 'react';
21
import matter from 'gray-matter';
32
import {s} from 'hastscript';
43
import yaml from 'js-yaml';
54
import {bundleMDX} from 'mdx-bundler';
6-
import {access, opendir, readFile} from 'node:fs/promises';
5+
import {BinaryLike, createHash} from 'node:crypto';
6+
import {createReadStream, createWriteStream, mkdirSync} from 'node:fs';
7+
import {access, cp, mkdir, opendir, readFile} from 'node:fs/promises';
78
import path from 'node:path';
9+
// @ts-expect-error ts(2305) -- For some reason "compose" is not recognized in the types
10+
import {compose, Readable} from 'node:stream';
11+
import {json} from 'node:stream/consumers';
12+
import {pipeline} from 'node:stream/promises';
13+
import {
14+
constants as zlibConstants,
15+
createBrotliCompress,
16+
createBrotliDecompress,
17+
} from 'node:zlib';
818
import {limitFunction} from 'p-limit';
919
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
1020
import rehypePresetMinify from 'rehype-preset-minify';
@@ -48,6 +58,34 @@ const root = process.cwd();
4858
// Functions which looks like AWS Lambda and we get `EMFILE` errors when trying to open
4959
// so many files at once.
5060
const FILE_CONCURRENCY_LIMIT = 200;
61+
const CACHE_COMPRESS_LEVEL = 4;
62+
const CACHE_DIR = path.join(root, '.next', 'cache', 'mdx-bundler');
63+
mkdirSync(CACHE_DIR, {recursive: true});
64+
65+
const md5 = (data: BinaryLike) => createHash('md5').update(data).digest('hex');
66+
67+
async function readCacheFile<T>(file: string): Promise<T> {
68+
const reader = createReadStream(file);
69+
const decompressor = createBrotliDecompress();
70+
71+
return (await json(compose(reader, decompressor))) as T;
72+
}
73+
74+
async function writeCacheFile(file: string, data: string) {
75+
const bufferData = Buffer.from(data);
76+
await pipeline(
77+
Readable.from(bufferData),
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]: bufferData.length,
84+
},
85+
}),
86+
createWriteStream(file)
87+
);
88+
}
5189

5290
function formatSlug(slug: string) {
5391
return slug.replace(/\.(mdx|md)/, '');
@@ -484,6 +522,36 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
484522
);
485523
}
486524

525+
let cacheKey: string | null = null;
526+
let cacheFile: string | null = null;
527+
let assetsCacheDir: string | null = null;
528+
const outdir = path.join(root, 'public', 'mdx-images');
529+
await mkdir(outdir, {recursive: true});
530+
531+
if (process.env.CI) {
532+
cacheKey = md5(source);
533+
cacheFile = path.join(CACHE_DIR, `${cacheKey}.br`);
534+
assetsCacheDir = path.join(CACHE_DIR, cacheKey);
535+
536+
try {
537+
const [cached, _] = await Promise.all([
538+
readCacheFile<SlugFile>(cacheFile),
539+
cp(assetsCacheDir, outdir, {recursive: true}),
540+
]);
541+
return cached;
542+
} catch (err) {
543+
if (
544+
err.code !== 'ENOENT' &&
545+
err.code !== 'ABORT_ERR' &&
546+
err.code !== 'Z_BUF_ERROR'
547+
) {
548+
// If cache is corrupted, ignore and proceed
549+
// eslint-disable-next-line no-console
550+
console.warn(`Failed to read MDX cache: ${cacheFile}`, err);
551+
}
552+
}
553+
}
554+
487555
process.env.ESBUILD_BINARY_PATH = path.join(
488556
root,
489557
'node_modules',
@@ -578,8 +646,12 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
578646
'.svg': 'dataurl',
579647
};
580648
// Set the `outdir` to a public location for this bundle.
581-
// this where this images will be copied
582-
options.outdir = path.join(root, 'public', 'mdx-images');
649+
// this is where these images will be copied
650+
// the reason we use the cache folder when it's
651+
// enabled is because mdx-images is a dumping ground
652+
// for all images, so we cannot filter it out only
653+
// for this specific slug easily
654+
options.outdir = assetsCacheDir || outdir;
583655

584656
// Set write to true so that esbuild will output the files.
585657
options.write = true;
@@ -609,12 +681,30 @@ export async function getFileBySlug(slug: string): Promise<SlugFile> {
609681
},
610682
};
611683

684+
if (assetsCacheDir && cacheFile) {
685+
await cp(assetsCacheDir, outdir, {recursive: true});
686+
writeCacheFile(cacheFile, JSON.stringify(resultObj)).catch(e => {
687+
// eslint-disable-next-line no-console
688+
console.warn(`Failed to write MDX cache: ${cacheFile}`, e);
689+
});
690+
}
691+
612692
return resultObj;
613693
}
614694

695+
const fileBySlugCache = new Map<string, Promise<SlugFile>>();
696+
615697
/**
616698
* Cache the result of {@link getFileBySlug}.
617699
*
618700
* This is useful for performance when rendering the same file multiple times.
619701
*/
620-
export const getFileBySlugWithCache = cache(getFileBySlug);
702+
export function getFileBySlugWithCache(slug: string): Promise<SlugFile> {
703+
let cached = fileBySlugCache.get(slug);
704+
if (!cached) {
705+
cached = getFileBySlug(slug);
706+
fileBySlugCache.set(slug, cached);
707+
}
708+
709+
return cached;
710+
}

0 commit comments

Comments
 (0)