Skip to content

Commit 374efcd

Browse files
authored
Lazy loaded shiki languages during syntax highlighting (#10618)
1 parent 31590d4 commit 374efcd

File tree

16 files changed

+169
-110
lines changed

16 files changed

+169
-110
lines changed

.changeset/real-rabbits-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@astrojs/markdown-remark": major
3+
---
4+
5+
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.

packages/astro/components/Code.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const highlighter = await getCachedHighlighter({
8787
wrap,
8888
});
8989
90-
const html = highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
90+
const html = await highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
9191
inline,
9292
attributes: rest as any,
9393
});

packages/astro/src/assets/vite-plugin-assets.ts

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { extname } from 'node:path';
22
import MagicString from 'magic-string';
33
import type * as vite from 'vite';
44
import { normalizePath } from 'vite';
5-
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
5+
import type { AstroPluginOptions, AstroSettings, ImageTransform } from '../@types/astro.js';
66
import { extendManualChunks } from '../core/build/plugins/util.js';
77
import { AstroError, AstroErrorData } from '../core/errors/index.js';
88
import {
@@ -24,6 +24,71 @@ const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
2424

2525
const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})`, 'i');
2626
const assetRegexEnds = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})$`, 'i');
27+
const addStaticImageFactory = (
28+
settings: AstroSettings
29+
): typeof globalThis.astroAsset.addStaticImage => {
30+
return (options, hashProperties, originalFSPath) => {
31+
if (!globalThis.astroAsset.staticImages) {
32+
globalThis.astroAsset.staticImages = new Map<
33+
string,
34+
{
35+
originalSrcPath: string;
36+
transforms: Map<string, { finalPath: string; transform: ImageTransform }>;
37+
}
38+
>();
39+
}
40+
41+
// Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base
42+
const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src;
43+
const fileExtension = extname(ESMImportedImageSrc);
44+
const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
45+
46+
// 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)
47+
const finalOriginalPath = removeBase(
48+
removeBase(ESMImportedImageSrc, settings.config.base),
49+
assetPrefix
50+
);
51+
52+
const hash = hashTransform(options, settings.config.image.service.entrypoint, hashProperties);
53+
54+
let finalFilePath: string;
55+
let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
56+
let transformForHash = transformsForPath?.transforms.get(hash);
57+
58+
// If the same image has already been transformed with the same options, we'll reuse the final path
59+
if (transformsForPath && transformForHash) {
60+
finalFilePath = transformForHash.finalPath;
61+
} else {
62+
finalFilePath = prependForwardSlash(
63+
joinPaths(
64+
isESMImportedImage(options.src) ? '' : settings.config.build.assets,
65+
prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
66+
)
67+
);
68+
69+
if (!transformsForPath) {
70+
globalThis.astroAsset.staticImages.set(finalOriginalPath, {
71+
originalSrcPath: originalFSPath,
72+
transforms: new Map(),
73+
});
74+
transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath)!;
75+
}
76+
77+
transformsForPath.transforms.set(hash, {
78+
finalPath: finalFilePath,
79+
transform: options,
80+
});
81+
}
82+
83+
// The paths here are used for URLs, so we need to make sure they have the proper format for an URL
84+
// (leading slash, prefixed with the base / assets prefix, encoded, etc)
85+
if (settings.config.build.assetsPrefix) {
86+
return encodeURI(joinPaths(assetPrefix, finalFilePath));
87+
} else {
88+
return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath)));
89+
}
90+
};
91+
};
2792

2893
export default function assets({
2994
settings,
@@ -92,73 +157,7 @@ export default function assets({
92157
return;
93158
}
94159

95-
globalThis.astroAsset.addStaticImage = (options, hashProperties, originalFSPath) => {
96-
if (!globalThis.astroAsset.staticImages) {
97-
globalThis.astroAsset.staticImages = new Map<
98-
string,
99-
{
100-
originalSrcPath: string;
101-
transforms: Map<string, { finalPath: string; transform: ImageTransform }>;
102-
}
103-
>();
104-
}
105-
106-
// Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base
107-
const ESMImportedImageSrc = isESMImportedImage(options.src)
108-
? options.src.src
109-
: options.src;
110-
const fileExtension = extname(ESMImportedImageSrc);
111-
const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix);
112-
113-
// 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)
114-
const finalOriginalPath = removeBase(
115-
removeBase(ESMImportedImageSrc, settings.config.base),
116-
assetPrefix
117-
);
118-
119-
const hash = hashTransform(
120-
options,
121-
settings.config.image.service.entrypoint,
122-
hashProperties
123-
);
124-
125-
let finalFilePath: string;
126-
let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
127-
let transformForHash = transformsForPath?.transforms.get(hash);
128-
129-
// If the same image has already been transformed with the same options, we'll reuse the final path
130-
if (transformsForPath && transformForHash) {
131-
finalFilePath = transformForHash.finalPath;
132-
} else {
133-
finalFilePath = prependForwardSlash(
134-
joinPaths(
135-
isESMImportedImage(options.src) ? '' : settings.config.build.assets,
136-
prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
137-
)
138-
);
139-
140-
if (!transformsForPath) {
141-
globalThis.astroAsset.staticImages.set(finalOriginalPath, {
142-
originalSrcPath: originalFSPath,
143-
transforms: new Map(),
144-
});
145-
transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath)!;
146-
}
147-
148-
transformsForPath.transforms.set(hash, {
149-
finalPath: finalFilePath,
150-
transform: options,
151-
});
152-
}
153-
154-
// The paths here are used for URLs, so we need to make sure they have the proper format for an URL
155-
// (leading slash, prefixed with the base / assets prefix, encoded, etc)
156-
if (settings.config.build.assetsPrefix) {
157-
return encodeURI(joinPaths(assetPrefix, finalFilePath));
158-
} else {
159-
return encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath)));
160-
}
161-
};
160+
globalThis.astroAsset.addStaticImage = addStaticImageFactory(settings);
162161
},
163162
// In build, rewrite paths to ESM imported images in code to their final location
164163
async renderChunk(code) {

packages/astro/src/content/vite-plugin-content-assets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ export function astroConfigBuildPlugin(
257257
mutate(chunk, ['server'], newCode);
258258
}
259259
}
260+
261+
ssrPluginContext = undefined;
260262
},
261263
},
262264
};

packages/astro/src/vite-plugin-markdown/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,17 @@ const astroErrorModulePath = normalizePath(
3232
);
3333

3434
export default function markdown({ settings, logger }: AstroPluginOptions): Plugin {
35-
let processor: MarkdownProcessor;
35+
let processor: MarkdownProcessor | undefined;
3636

3737
return {
3838
enforce: 'pre',
3939
name: 'astro:markdown',
4040
async buildStart() {
4141
processor = await createMarkdownProcessor(settings.config.markdown);
4242
},
43+
buildEnd() {
44+
processor = undefined;
45+
},
4346
// Why not the "transform" hook instead of "load" + readFile?
4447
// A: Vite transforms all "import.meta.env" references to their values before
4548
// 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
5255

5356
const fileURL = pathToFileURL(fileId);
5457

55-
const renderResult = await processor
58+
const renderResult = await processor!
5659
.render(raw.content, {
5760
// @ts-expect-error passing internal prop
5861
fileURL,

packages/astro/test/astro-markdown-shiki.test.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,26 +80,42 @@ describe('Astro Markdown Shiki', () => {
8080
});
8181
});
8282

83-
describe('Custom langs', () => {
83+
describe('Languages', () => {
8484
let fixture;
85+
let $;
8586

8687
before(async () => {
8788
fixture = await loadFixture({ root: './fixtures/astro-markdown-shiki/langs/' });
8889
await fixture.build();
89-
});
90-
91-
it('Markdown file', async () => {
9290
const html = await fixture.readFile('/index.html');
93-
const $ = cheerio.load(html);
91+
$ = cheerio.load(html);
92+
});
9493

95-
const segments = $('.line').get(6).children;
94+
it('custom language', async () => {
95+
const lang = $('.astro-code').get(0);
96+
const segments = $('.line', lang).get(6).children;
9697
assert.equal(segments.length, 2);
9798
assert.equal(segments[0].attribs.style, 'color:#79B8FF');
9899
assert.equal(segments[1].attribs.style, 'color:#E1E4E8');
100+
});
99101

102+
it('handles unknown languages', () => {
100103
const unknownLang = $('.astro-code').get(1);
101104
assert.ok(unknownLang.attribs.style.includes('background-color:#24292e;color:#e1e4e8;'));
102105
});
106+
107+
it('handles lazy loaded languages', () => {
108+
const lang = $('.astro-code').get(2);
109+
const segments = $('.line', lang).get(0).children;
110+
assert.equal(segments.length, 7);
111+
assert.equal(segments[0].attribs.style, 'color:#F97583');
112+
assert.equal(segments[1].attribs.style, 'color:#79B8FF');
113+
assert.equal(segments[2].attribs.style, 'color:#F97583');
114+
assert.equal(segments[3].attribs.style, 'color:#79B8FF');
115+
assert.equal(segments[4].attribs.style, 'color:#F97583');
116+
assert.equal(segments[5].attribs.style, 'color:#79B8FF');
117+
assert.equal(segments[6].attribs.style, 'color:#E1E4E8');
118+
});
103119
});
104120

105121
describe('Wrapping behaviours', () => {

packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ fin
2424
```unknown
2525
This language does not exist
2626
```
27+
28+
```ts
29+
const someTypeScript: number = 5;
30+
```

packages/create-astro/test/fixtures/not-empty/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"build": "astro build",
77
"preview": "astro preview"
88
}
9-
}
9+
}

packages/integrations/markdoc/components/Renderer.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type Props = {
1212
const { stringifiedAst, config } = Astro.props as Props;
1313
1414
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
15-
const content = Markdoc.transform(ast, config);
15+
const content = await Markdoc.transform(ast, config);
1616
---
1717

1818
{

packages/integrations/markdoc/src/extensions/shiki.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ export default async function shiki(config?: ShikiConfig): Promise<AstroMarkdocC
1111
nodes: {
1212
fence: {
1313
attributes: Markdoc.nodes.fence.attributes!,
14-
transform({ attributes }) {
14+
async transform({ attributes }) {
1515
// NOTE: The `meta` from fence code, e.g. ```js {1,3-4}, isn't quite supported by Markdoc.
1616
// Only the `js` part is parsed as `attributes.language` and the rest is ignored. This means
1717
// some Shiki transformers may not work correctly as it relies on the `meta`.
1818
const lang = typeof attributes.language === 'string' ? attributes.language : 'plaintext';
19-
const html = highlighter.highlight(attributes.content, lang);
19+
const html = await highlighter.highlight(attributes.content, lang);
2020

2121
// Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML
2222
return unescapeHTML(html) as any;

0 commit comments

Comments
 (0)