From cf4a742a00a8ab817e506fce8d1dc78616e1c62f Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 14:16:42 +0000
Subject: [PATCH 01/10] feat: extract static image helper

This moves the static image helper outside of the build hooks such that
the scope is no longer captured.

Since the static image helper exists on `window`, none of the function's
scope will be garbage collected at any point. Part of said scope is
`resolvedConfig`, which holds package caches, plugins, etc. These will
also be retained.

By moving the function into a factory, we no longer hold a reference to
`resolvedConfig`, leading to lower memory usage.
---
 .../astro/src/assets/vite-plugin-assets.ts    | 135 +++++++++---------
 1 file changed, 67 insertions(+), 68 deletions(-)

diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index fe92b2538eea..a696e5619e15 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -2,7 +2,7 @@ import { extname } from 'node:path';
 import MagicString from 'magic-string';
 import type * as vite from 'vite';
 import { normalizePath } from 'vite';
-import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
+import type { AstroPluginOptions, AstroSettings, ImageTransform } from '../@types/astro.js';
 import { extendManualChunks } from '../core/build/plugins/util.js';
 import { AstroError, AstroErrorData } from '../core/errors/index.js';
 import {
@@ -24,6 +24,71 @@ const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
 
 const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})`, 'i');
 const assetRegexEnds = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})$`, 'i');
+const addStaticImageFactory = (
+	settings: AstroSettings
+): typeof globalThis.astroAsset.addStaticImage => {
+	return (options, hashProperties, originalFSPath) => {
+		if (!globalThis.astroAsset.staticImages) {
+			globalThis.astroAsset.staticImages = new Map<
+				string,
+				{
+					originalSrcPath: string;
+					transforms: Map<string, { finalPath: string; transform: ImageTransform }>;
+				}
+			>();
+		}
+
+		// Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base
+		const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src;
+		const fileExtension = extname(ESMImportedImageSrc);
+		const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
+
+		// This is the path to the original image, from the dist root, without the base or the asset prefix (e.g. /_astro/image.hash.png)
+		const finalOriginalPath = removeBase(
+			removeBase(ESMImportedImageSrc, settings.config.base),
+			assetPrefix
+		);
+
+		const hash = hashTransform(options, settings.config.image.service.entrypoint, hashProperties);
+
+		let finalFilePath: string;
+		let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
+		let transformForHash = transformsForPath?.transforms.get(hash);
+
+		// If the same image has already been transformed with the same options, we'll reuse the final path
+		if (transformsForPath && transformForHash) {
+			finalFilePath = transformForHash.finalPath;
+		} else {
+			finalFilePath = prependForwardSlash(
+				joinPaths(
+					isESMImportedImage(options.src) ? '' : settings.config.build.assets,
+					prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
+				)
+			);
+
+			if (!transformsForPath) {
+				globalThis.astroAsset.staticImages.set(finalOriginalPath, {
+					originalSrcPath: originalFSPath,
+					transforms: new Map(),
+				});
+				transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath)!;
+			}
+
+			transformsForPath.transforms.set(hash, {
+				finalPath: finalFilePath,
+				transform: options,
+			});
+		}
+
+		// The paths here are used for URLs, so we need to make sure they have the proper format for an URL
+		// (leading slash, prefixed with the base / assets prefix, encoded, etc)
+		if (settings.config.build.assetsPrefix) {
+			return encodeURI(joinPaths(assetPrefix, finalFilePath));
+		} else {
+			return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath)));
+		}
+	};
+};
 
 export default function assets({
 	settings,
@@ -92,73 +157,7 @@ export default function assets({
 					return;
 				}
 
-				globalThis.astroAsset.addStaticImage = (options, hashProperties, originalFSPath) => {
-					if (!globalThis.astroAsset.staticImages) {
-						globalThis.astroAsset.staticImages = new Map<
-							string,
-							{
-								originalSrcPath: string;
-								transforms: Map<string, { finalPath: string; transform: ImageTransform }>;
-							}
-						>();
-					}
-
-					// Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base
-					const ESMImportedImageSrc = isESMImportedImage(options.src)
-						? options.src.src
-						: options.src;
-					const fileExtension = extname(ESMImportedImageSrc);
-					const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
-
-					// This is the path to the original image, from the dist root, without the base or the asset prefix (e.g. /_astro/image.hash.png)
-					const finalOriginalPath = removeBase(
-						removeBase(ESMImportedImageSrc, settings.config.base),
-						assetPrefix
-					);
-
-					const hash = hashTransform(
-						options,
-						settings.config.image.service.entrypoint,
-						hashProperties
-					);
-
-					let finalFilePath: string;
-					let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
-					let transformForHash = transformsForPath?.transforms.get(hash);
-
-					// If the same image has already been transformed with the same options, we'll reuse the final path
-					if (transformsForPath && transformForHash) {
-						finalFilePath = transformForHash.finalPath;
-					} else {
-						finalFilePath = prependForwardSlash(
-							joinPaths(
-								isESMImportedImage(options.src) ? '' : settings.config.build.assets,
-								prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
-							)
-						);
-
-						if (!transformsForPath) {
-							globalThis.astroAsset.staticImages.set(finalOriginalPath, {
-								originalSrcPath: originalFSPath,
-								transforms: new Map(),
-							});
-							transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath)!;
-						}
-
-						transformsForPath.transforms.set(hash, {
-							finalPath: finalFilePath,
-							transform: options,
-						});
-					}
-
-					// The paths here are used for URLs, so we need to make sure they have the proper format for an URL
-					// (leading slash, prefixed with the base / assets prefix, encoded, etc)
-					if (settings.config.build.assetsPrefix) {
-						return encodeURI(joinPaths(assetPrefix, finalFilePath));
-					} else {
-						return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath)));
-					}
-				};
+				globalThis.astroAsset.addStaticImage = addStaticImageFactory(settings);
 			},
 			// In build, rewrite paths to ESM imported images in code to their final location
 			async renderChunk(code) {

From 1986ddae8934bd43d387c418e9e31496f3e09efd Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 14:23:36 +0000
Subject: [PATCH 02/10] feat: discard plugin context after build

This discards the SSR plugin context once a build has finished, so we're
no longer holding it in memory and it can be garbage collected.
---
 packages/astro/src/content/vite-plugin-content-assets.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts
index a57fb10562e6..591cad3c70f6 100644
--- a/packages/astro/src/content/vite-plugin-content-assets.ts
+++ b/packages/astro/src/content/vite-plugin-content-assets.ts
@@ -257,6 +257,8 @@ export function astroConfigBuildPlugin(
 						mutate(chunk, ['server'], newCode);
 					}
 				}
+
+				ssrPluginContext = undefined;
 			},
 		},
 	};

From ec02b09c0c4b8a6237ec8aa5f40ff8ba4501db0e Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 14:25:03 +0000
Subject: [PATCH 03/10] feat: discard markdown processor on build end

This discards the markdown processor once a build ends, allowing it to
be garbage collected.
---
 packages/astro/src/vite-plugin-markdown/index.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 52250ad99668..a1b3887ff6a9 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -32,7 +32,7 @@ const astroErrorModulePath = normalizePath(
 );
 
 export default function markdown({ settings, logger }: AstroPluginOptions): Plugin {
-	let processor: MarkdownProcessor;
+	let processor: MarkdownProcessor | undefined;
 
 	return {
 		enforce: 'pre',
@@ -40,6 +40,9 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
 		async buildStart() {
 			processor = await createMarkdownProcessor(settings.config.markdown);
 		},
+		buildEnd() {
+			processor = undefined;
+		},
 		// Why not the "transform" hook instead of "load" + readFile?
 		// A: Vite transforms all "import.meta.env" references to their values before
 		// passing to the transform hook. This lets us get the truly raw value
@@ -52,7 +55,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
 
 				const fileURL = pathToFileURL(fileId);
 
-				const renderResult = await processor
+				const renderResult = await processor!
 					.render(raw.content, {
 						// @ts-expect-error passing internal prop
 						fileURL,

From 60607f1bf94b5381f9b3a9778a22c9292ab93d69 Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 14:26:25 +0000
Subject: [PATCH 04/10] feat: make shiki highlighting async

When highlighting code via shiki, we default to enabling all available
languages. This results in shiki internally loading each individual
language module, evaluating it, and keeping hold of it in memory.

Instead, we should be able to load the bare minimum and (try) lazily load any
missing languages later on. By doing this, we're not unnecessarily
loading up dozens of large modules and will only load what the user
consumes.

Since traversal of the AST via unist is synchronous, we first do the
traversal to find the nodes and later asynchronously _process_ the nodes
(i.e. execute the highlighter and mutate the AST).
---
 packages/markdown/remark/src/highlight.ts    | 27 ++++++++++++++----
 packages/markdown/remark/src/rehype-prism.ts |  4 ++-
 packages/markdown/remark/src/rehype-shiki.ts |  2 +-
 packages/markdown/remark/src/shiki.ts        | 29 ++++++++++++++------
 4 files changed, 45 insertions(+), 17 deletions(-)

diff --git a/packages/markdown/remark/src/highlight.ts b/packages/markdown/remark/src/highlight.ts
index 31f11119fa3a..8bc7c492d12d 100644
--- a/packages/markdown/remark/src/highlight.ts
+++ b/packages/markdown/remark/src/highlight.ts
@@ -1,10 +1,10 @@
-import type { Element, Root } from 'hast';
+import type { Element, Parent, Root } from 'hast';
 import { fromHtml } from 'hast-util-from-html';
 import { toText } from 'hast-util-to-text';
 import { removePosition } from 'unist-util-remove-position';
 import { visitParents } from 'unist-util-visit-parents';
 
-type Highlighter = (code: string, language: string, options?: { meta?: string }) => string;
+type Highlighter = (code: string, language: string, options?: { meta?: string }) => Promise<string>;
 
 const languagePattern = /\blanguage-(\S+)\b/;
 
@@ -17,7 +17,14 @@ const languagePattern = /\blanguage-(\S+)\b/;
  *   A fnction which receives the code and language, and returns the HTML of a syntax
  *   highlighted `<pre>` element.
  */
-export function highlightCodeBlocks(tree: Root, highlighter: Highlighter) {
+export async function highlightCodeBlocks(tree: Root, highlighter: Highlighter) {
+	const nodes: Array<{
+		node: Element;
+		language: string;
+		parent: Element;
+		grandParent: Parent;
+	}> = [];
+
 	// We’re looking for `<code>` elements
 	visitParents(tree, { type: 'element', tagName: 'code' }, (node, ancestors) => {
 		const parent = ancestors.at(-1);
@@ -55,17 +62,25 @@ export function highlightCodeBlocks(tree: Root, highlighter: Highlighter) {
 			return;
 		}
 
+		nodes.push({
+			node,
+			language: languageMatch?.[1] || 'plaintext',
+			parent,
+			grandParent: ancestors.at(-2)!,
+		});
+	});
+
+	for (const { node, language, grandParent, parent } of nodes) {
 		const meta = (node.data as any)?.meta ?? node.properties.metastring ?? undefined;
 		const code = toText(node, { whitespace: 'pre' });
-		const html = highlighter(code, languageMatch?.[1] || 'plaintext', { meta });
+		const html = await highlighter(code, language, { meta });
 		// The replacement returns a root node with 1 child, the `<pr>` element replacement.
 		const replacement = fromHtml(html, { fragment: true }).children[0] as Element;
 		// We just generated this node, so any positional information is invalid.
 		removePosition(replacement);
 
 		// We replace the parent in its parent with the new `<pre>` element.
-		const grandParent = ancestors.at(-2)!;
 		const index = grandParent.children.indexOf(parent);
 		grandParent.children[index] = replacement;
-	});
+	}
 }
diff --git a/packages/markdown/remark/src/rehype-prism.ts b/packages/markdown/remark/src/rehype-prism.ts
index 4305a067677f..24e891daa075 100644
--- a/packages/markdown/remark/src/rehype-prism.ts
+++ b/packages/markdown/remark/src/rehype-prism.ts
@@ -7,6 +7,8 @@ export const rehypePrism: Plugin<[], Root> = () => (tree) => {
 	highlightCodeBlocks(tree, (code, language) => {
 		let { html, classLanguage } = runHighlighterWithAstro(language, code);
 
-		return `<pre class="${classLanguage}"><code is:raw class="${classLanguage}">${html}</code></pre>`;
+		return Promise.resolve(
+			`<pre class="${classLanguage}"><code is:raw class="${classLanguage}">${html}</code></pre>`
+		);
 	});
 };
diff --git a/packages/markdown/remark/src/rehype-shiki.ts b/packages/markdown/remark/src/rehype-shiki.ts
index dd146f110af9..fdab3ddf3517 100644
--- a/packages/markdown/remark/src/rehype-shiki.ts
+++ b/packages/markdown/remark/src/rehype-shiki.ts
@@ -11,6 +11,6 @@ export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => {
 		highlighterAsync ??= createShikiHighlighter(config);
 		const highlighter = await highlighterAsync;
 
-		highlightCodeBlocks(tree, highlighter.highlight);
+		await highlightCodeBlocks(tree, highlighter.highlight);
 	};
 };
diff --git a/packages/markdown/remark/src/shiki.ts b/packages/markdown/remark/src/shiki.ts
index fc35c6e92b33..c4fef6f7a4f7 100644
--- a/packages/markdown/remark/src/shiki.ts
+++ b/packages/markdown/remark/src/shiki.ts
@@ -1,5 +1,10 @@
 import type { Properties } from 'hast';
-import { bundledLanguages, createCssVariablesTheme, getHighlighter, isSpecialLang } from 'shiki';
+import {
+	type BundledLanguage,
+	createCssVariablesTheme,
+	getHighlighter,
+	isSpecialLang,
+} from 'shiki';
 import { visit } from 'unist-util-visit';
 import type { ShikiConfig } from './types.js';
 
@@ -15,7 +20,7 @@ export interface ShikiHighlighter {
 			 */
 			meta?: string;
 		}
-	): string;
+	): Promise<string>;
 }
 
 // TODO: Remove this special replacement in Astro 5
@@ -43,18 +48,24 @@ export async function createShikiHighlighter({
 	theme = theme === 'css-variables' ? cssVariablesTheme() : theme;
 
 	const highlighter = await getHighlighter({
-		langs: langs.length ? langs : Object.keys(bundledLanguages),
+		langs: ['plaintext', ...langs],
 		themes: Object.values(themes).length ? Object.values(themes) : [theme],
 	});
 
-	const loadedLanguages = highlighter.getLoadedLanguages();
-
 	return {
-		highlight(code, lang = 'plaintext', options) {
+		async highlight(code, lang = 'plaintext', options) {
+			const loadedLanguages = highlighter.getLoadedLanguages();
+
 			if (!isSpecialLang(lang) && !loadedLanguages.includes(lang)) {
-				// eslint-disable-next-line no-console
-				console.warn(`[Shiki] The language "${lang}" doesn't exist, falling back to "plaintext".`);
-				lang = 'plaintext';
+				try {
+					await highlighter.loadLanguage(lang as BundledLanguage);
+				} catch (_err) {
+					// eslint-disable-next-line no-console
+					console.warn(
+						`[Shiki] The language "${lang}" doesn't exist, falling back to "plaintext".`
+					);
+					lang = 'plaintext';
+				}
 			}
 
 			const themeOptions = Object.values(themes).length ? { themes } : { theme };

From f45205bdfb31c9f2a51f64f02268f1a2cc51c80e Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 16:07:57 +0000
Subject: [PATCH 05/10] test: add a test for lazily loaded shiki languages

This adds a test for ensuring highlighting happens when using a lazily
loaded language (typescript in this case).
---
 .../astro/test/astro-markdown-shiki.test.js   | 28 +++++++++++++++----
 .../langs/src/pages/index.md                  |  4 +++
 .../test/fixtures/not-empty/package.json      |  2 +-
 packages/markdown/remark/test/shiki.test.js   |  8 +++---
 4 files changed, 31 insertions(+), 11 deletions(-)

diff --git a/packages/astro/test/astro-markdown-shiki.test.js b/packages/astro/test/astro-markdown-shiki.test.js
index 982b30e8b561..24ab7d2b3026 100644
--- a/packages/astro/test/astro-markdown-shiki.test.js
+++ b/packages/astro/test/astro-markdown-shiki.test.js
@@ -80,26 +80,42 @@ describe('Astro Markdown Shiki', () => {
 		});
 	});
 
-	describe('Custom langs', () => {
+	describe('Languages', () => {
 		let fixture;
+		let $;
 
 		before(async () => {
 			fixture = await loadFixture({ root: './fixtures/astro-markdown-shiki/langs/' });
 			await fixture.build();
-		});
-
-		it('Markdown file', async () => {
 			const html = await fixture.readFile('/index.html');
-			const $ = cheerio.load(html);
+			$ = cheerio.load(html);
+		});
 
-			const segments = $('.line').get(6).children;
+		it('custom language', async () => {
+			const lang = $('.astro-code').get(0);
+			const segments = $('.line', lang).get(6).children;
 			assert.equal(segments.length, 2);
 			assert.equal(segments[0].attribs.style, 'color:#79B8FF');
 			assert.equal(segments[1].attribs.style, 'color:#E1E4E8');
+		});
 
+		it('handles unknown languages', () => {
 			const unknownLang = $('.astro-code').get(1);
 			assert.ok(unknownLang.attribs.style.includes('background-color:#24292e;color:#e1e4e8;'));
 		});
+
+		it('handles lazy loaded languages', () => {
+			const lang = $('.astro-code').get(2);
+			const segments = $('.line', lang).get(0).children;
+			assert.equal(segments.length, 7);
+			assert.equal(segments[0].attribs.style, 'color:#F97583');
+			assert.equal(segments[1].attribs.style, 'color:#79B8FF');
+			assert.equal(segments[2].attribs.style, 'color:#F97583');
+			assert.equal(segments[3].attribs.style, 'color:#79B8FF');
+			assert.equal(segments[4].attribs.style, 'color:#F97583');
+			assert.equal(segments[5].attribs.style, 'color:#79B8FF');
+			assert.equal(segments[6].attribs.style, 'color:#E1E4E8');
+		});
 	});
 
 	describe('Wrapping behaviours', () => {
diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
index d2d756b95dc1..535f44877791 100644
--- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
+++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md
@@ -24,3 +24,7 @@ fin
 ```unknown
 This language does not exist
 ```
+
+```ts
+const someTypeScript: number = 5;
+```
diff --git a/packages/create-astro/test/fixtures/not-empty/package.json b/packages/create-astro/test/fixtures/not-empty/package.json
index 516149e6d005..d3f61d640c00 100644
--- a/packages/create-astro/test/fixtures/not-empty/package.json
+++ b/packages/create-astro/test/fixtures/not-empty/package.json
@@ -6,4 +6,4 @@
     "build": "astro build",
     "preview": "astro preview"
   }
-}
+}
\ No newline at end of file
diff --git a/packages/markdown/remark/test/shiki.test.js b/packages/markdown/remark/test/shiki.test.js
index 601b7fabf692..d856b54b7f25 100644
--- a/packages/markdown/remark/test/shiki.test.js
+++ b/packages/markdown/remark/test/shiki.test.js
@@ -33,7 +33,7 @@ describe('shiki syntax highlighting', () => {
 	it('createShikiHighlighter works', async () => {
 		const highlighter = await createShikiHighlighter();
 
-		const html = highlighter.highlight('const foo = "bar";', 'js');
+		const html = await highlighter.highlight('const foo = "bar";', 'js');
 
 		assert.match(html, /astro-code github-dark/);
 		assert.match(html, /background-color:#24292e;color:#e1e4e8;/);
@@ -42,7 +42,7 @@ describe('shiki syntax highlighting', () => {
 	it('diff +/- text has user-select: none', async () => {
 		const highlighter = await createShikiHighlighter();
 
-		const html = highlighter.highlight(
+		const html = await highlighter.highlight(
 			`\
 - const foo = "bar";
 + const foo = "world";`,
@@ -57,7 +57,7 @@ describe('shiki syntax highlighting', () => {
 	it('renders attributes', async () => {
 		const highlighter = await createShikiHighlighter();
 
-		const html = highlighter.highlight(`foo`, 'js', {
+		const html = await highlighter.highlight(`foo`, 'js', {
 			attributes: { 'data-foo': 'bar', autofocus: true },
 		});
 
@@ -79,7 +79,7 @@ describe('shiki syntax highlighting', () => {
 			],
 		});
 
-		const html = highlighter.highlight(`foo`, 'js', {
+		const html = await highlighter.highlight(`foo`, 'js', {
 			meta: '{1,3-4}',
 		});
 

From 74cdd2f000814fa8bdabb415781cc4d0b13549d9 Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 16:37:16 +0000
Subject: [PATCH 06/10] fix: make prism highlighting async

Aligning the prism plugin with the shiki plugin - exporting an async
visitor since the code block highlighter now returns a promise.
---
 packages/markdown/remark/src/rehype-prism.ts | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/packages/markdown/remark/src/rehype-prism.ts b/packages/markdown/remark/src/rehype-prism.ts
index 24e891daa075..2729948ddddf 100644
--- a/packages/markdown/remark/src/rehype-prism.ts
+++ b/packages/markdown/remark/src/rehype-prism.ts
@@ -3,12 +3,14 @@ import type { Root } from 'hast';
 import type { Plugin } from 'unified';
 import { highlightCodeBlocks } from './highlight.js';
 
-export const rehypePrism: Plugin<[], Root> = () => (tree) => {
-	highlightCodeBlocks(tree, (code, language) => {
-		let { html, classLanguage } = runHighlighterWithAstro(language, code);
+export const rehypePrism: Plugin<[], Root> = () => {
+	return async (tree) => {
+		await highlightCodeBlocks(tree, (code, language) => {
+			let { html, classLanguage } = runHighlighterWithAstro(language, code);
 
-		return Promise.resolve(
-			`<pre class="${classLanguage}"><code is:raw class="${classLanguage}">${html}</code></pre>`
-		);
-	});
+			return Promise.resolve(
+				`<pre class="${classLanguage}"><code is:raw class="${classLanguage}">${html}</code></pre>`
+			);
+		});
+	};
 };

From 5e9b2589ed483db2f8ac108e9db9b30dba5d014e Mon Sep 17 00:00:00 2001
From: James Garbutt <43081j@users.noreply.github.com>
Date: Sat, 30 Mar 2024 17:11:04 +0000
Subject: [PATCH 07/10] fix: make markdoc transforms async

Markdoc actually supports async transforms already. Now that the shiki
highlighting is async, we also need to make our transform async.

Do note that typescript will be unhappy until markdoc/markdoc#495 is
resolved.
---
 packages/integrations/markdoc/components/Renderer.astro   | 2 +-
 packages/integrations/markdoc/src/extensions/shiki.ts     | 4 ++--
 .../integrations/markdoc/test/syntax-highlighting.test.js | 8 ++++----
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/packages/integrations/markdoc/components/Renderer.astro b/packages/integrations/markdoc/components/Renderer.astro
index 4b0dbb3a09fa..c26d92ad737a 100644
--- a/packages/integrations/markdoc/components/Renderer.astro
+++ b/packages/integrations/markdoc/components/Renderer.astro
@@ -12,7 +12,7 @@ type Props = {
 const { stringifiedAst, config } = Astro.props as Props;
 
 const ast = Markdoc.Ast.fromJSON(stringifiedAst);
-const content = Markdoc.transform(ast, config);
+const content = await Markdoc.transform(ast, config);
 ---
 
 {
diff --git a/packages/integrations/markdoc/src/extensions/shiki.ts b/packages/integrations/markdoc/src/extensions/shiki.ts
index a39eb69a9f5f..04fc8e8673aa 100644
--- a/packages/integrations/markdoc/src/extensions/shiki.ts
+++ b/packages/integrations/markdoc/src/extensions/shiki.ts
@@ -11,12 +11,12 @@ export default async function shiki(config?: ShikiConfig): Promise<AstroMarkdocC
 		nodes: {
 			fence: {
 				attributes: Markdoc.nodes.fence.attributes!,
-				transform({ attributes }) {
+				async transform({ attributes }) {
 					// NOTE: The `meta` from fence code, e.g. ```js {1,3-4}, isn't quite supported by Markdoc.
 					// Only the `js` part is parsed as `attributes.language` and the rest is ignored. This means
 					// some Shiki transformers may not work correctly as it relies on the `meta`.
 					const lang = typeof attributes.language === 'string' ? attributes.language : 'plaintext';
-					const html = highlighter.highlight(attributes.content, lang);
+					const html = await highlighter.highlight(attributes.content, lang);
 
 					// Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML
 					return unescapeHTML(html) as any;
diff --git a/packages/integrations/markdoc/test/syntax-highlighting.test.js b/packages/integrations/markdoc/test/syntax-highlighting.test.js
index bab309c87104..7b2016808e6b 100644
--- a/packages/integrations/markdoc/test/syntax-highlighting.test.js
+++ b/packages/integrations/markdoc/test/syntax-highlighting.test.js
@@ -23,7 +23,7 @@ describe('Markdoc - syntax highlighting', () => {
 	describe('shiki', () => {
 		it('transforms with defaults', async () => {
 			const ast = Markdoc.parse(entry);
-			const content = Markdoc.transform(ast, await getConfigExtendingShiki());
+			const content = await Markdoc.transform(ast, await getConfigExtendingShiki());
 
 			assert.equal(content.children.length, 2);
 			for (const codeBlock of content.children) {
@@ -36,7 +36,7 @@ describe('Markdoc - syntax highlighting', () => {
 		});
 		it('transforms with `theme` property', async () => {
 			const ast = Markdoc.parse(entry);
-			const content = Markdoc.transform(
+			const content = await Markdoc.transform(
 				ast,
 				await getConfigExtendingShiki({
 					theme: 'dracula',
@@ -53,7 +53,7 @@ describe('Markdoc - syntax highlighting', () => {
 		});
 		it('transforms with `wrap` property', async () => {
 			const ast = Markdoc.parse(entry);
-			const content = Markdoc.transform(
+			const content = await Markdoc.transform(
 				ast,
 				await getConfigExtendingShiki({
 					wrap: true,
@@ -76,7 +76,7 @@ describe('Markdoc - syntax highlighting', () => {
 			const config = await setupConfig({
 				extends: [prism()],
 			});
-			const content = Markdoc.transform(ast, config);
+			const content = await Markdoc.transform(ast, config);
 
 			assert.equal(content.children.length, 2);
 			const [tsBlock, cssBlock] = content.children;

From 4a48b2c2752d84ce9fb0e9fd85b007a6e34bca59 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:29:31 +0100
Subject: [PATCH 08/10] fix: await highlight result in code component

---
 packages/astro/components/Code.astro | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro
index 43cc847bb8fa..f0cb26326516 100644
--- a/packages/astro/components/Code.astro
+++ b/packages/astro/components/Code.astro
@@ -87,7 +87,7 @@ const highlighter = await getCachedHighlighter({
 	wrap,
 });
 
-const html = highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
+const html = await highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
 	inline,
 	attributes: rest as any,
 });

From ee0002eee975863da7be3198f5cd94e73ca22850 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 1 Apr 2024 14:41:26 +0100
Subject: [PATCH 09/10] chore: add changeset

---
 .changeset/real-rabbits-bake.md | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 .changeset/real-rabbits-bake.md

diff --git a/.changeset/real-rabbits-bake.md b/.changeset/real-rabbits-bake.md
new file mode 100644
index 000000000000..a2b48b1e1b87
--- /dev/null
+++ b/.changeset/real-rabbits-bake.md
@@ -0,0 +1,7 @@
+---
+"@astrojs/markdown-remark": major
+---
+
+This changes the `markdown-remark` package to lazily load shiki languages by
+default (only preloading `plaintext`). Additionally, highlighting is now an
+async task due to this.

From aaddd4028151c264fa6c0e6f36e32d7a02f32371 Mon Sep 17 00:00:00 2001
From: Bjorn Lu <bjornlu.dev@gmail.com>
Date: Mon, 1 Apr 2024 22:52:36 +0800
Subject: [PATCH 10/10] Update .changeset/real-rabbits-bake.md

---
 .changeset/real-rabbits-bake.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.changeset/real-rabbits-bake.md b/.changeset/real-rabbits-bake.md
index a2b48b1e1b87..f750b56e347d 100644
--- a/.changeset/real-rabbits-bake.md
+++ b/.changeset/real-rabbits-bake.md
@@ -2,6 +2,4 @@
 "@astrojs/markdown-remark": major
 ---
 
-This changes the `markdown-remark` package to lazily load shiki languages by
-default (only preloading `plaintext`). Additionally, highlighting is now an
-async task due to this.
+Updates Shiki syntax highlighting to lazily load shiki languages by default (only preloading `plaintext`). Additionally, the `createShikiHighlighter()` API now returns an asynchronous `highlight()` function due to this.