Skip to content

Commit 922aae3

Browse files
authored
fix(rehype-shiki): only import every grammar without a highlighter (#9212)
The plugin imported `index.mjs` statically, and that module imports every grammar Shiki bundles (`LANGS`) as soon as it's loaded, so a caller passing its own highlighter still paid for all of them: 260 grammar modules on each thread that loads the plugin. It's now imported only when the plugin has to create a highlighter itself. This lets doc-kit, which highlights with a highlighter that loads grammars on demand, use the plugin rather than a copy of it. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0d68f9f commit 922aae3

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@node-core/rehype-shiki': patch
3+
---
4+
5+
Only import every bundled grammar when the plugin isn't given a highlighter

‎packages/rehype-shiki/src/__tests__/plugin.test.mjs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import assert from 'node:assert/strict';
2+
import { registerHooks } from 'node:module';
23
import { describe, it, mock } from 'node:test';
34

5+
// The modules loaded from here on, to tell whether the default highlighter,
6+
// which imports every grammar Shiki bundles, was loaded
7+
const loaded = new Set();
8+
9+
registerHooks({
10+
load(url, context, nextLoad) {
11+
loaded.add(new URL(url).pathname);
12+
13+
return nextLoad(url, context);
14+
},
15+
});
16+
17+
const defaultHighlighter = new URL('../index.mjs', import.meta.url).pathname;
18+
419
// Simplified mocks - only mock what's actually needed
520
mock.module('../index.mjs', {
621
defaultExport: () => ({ highlightToHast: mock.fn(() => ({ children: [] })) }),
@@ -23,6 +38,15 @@ describe('rehypeShikiji', async () => {
2338
const { default: rehypeShikiji } = await import('../plugin.mjs');
2439
const mockTree = { type: 'root', children: [] };
2540

41+
// Runs first, as the cases after it load the default highlighter
42+
it('only loads the default highlighter without a highlighter', async () => {
43+
await rehypeShikiji({ highlighter: { highlightToHast: mock.fn() } });
44+
assert.ok(!loaded.has(defaultHighlighter));
45+
46+
await rehypeShikiji();
47+
assert.ok(loaded.has(defaultHighlighter));
48+
});
49+
2650
it('calls visit twice', async () => {
2751
mockVisit.mock.resetCalls();
2852
(await rehypeShikiji())(mockTree);

‎packages/rehype-shiki/src/plugin.mjs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import classNames from 'classnames';
44
import { toString } from 'hast-util-to-string';
55
import { SKIP, visit } from 'unist-util-visit';
66

7-
import createHighlighter from '#rs/index.mjs';
8-
97
// This is what Remark will use as prefix within a <pre> className
108
// to attribute the current language of the <pre> element
119
const languagePrefix = 'language-';
@@ -57,8 +55,15 @@ function isCodeBlock(node) {
5755
* @param {import('#rs/index.mjs').HighlighterOptions & { highlighter: import('#rs/highlighter.mjs').SyntaxHighlighter }} options
5856
*/
5957
export default async function rehypeShikiji(options) {
60-
const highlighter =
61-
options?.highlighter ?? (await createHighlighter(options));
58+
let highlighter = options?.highlighter;
59+
60+
// Loaded only when no highlighter is given, as it imports every grammar
61+
// Shiki bundles as soon as it's loaded
62+
if (!highlighter) {
63+
const { default: createHighlighter } = await import('#rs/index.mjs');
64+
65+
highlighter = await createHighlighter(options);
66+
}
6267

6368
return function (tree) {
6469
visit(tree, 'element', (_, index, parent) => {

0 commit comments

Comments
 (0)