Skip to content

Commit fd41e43

Browse files
committed
fix cloudflare support
1 parent 19ddf40 commit fd41e43

File tree

7 files changed

+155
-117
lines changed

7 files changed

+155
-117
lines changed

apps/site/next.mdx.plugins.mjs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,24 @@ export const REHYPE_PLUGINS = [
1717
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
1818
// Transforms sequential code elements into code tabs and
1919
// adds our syntax highlighter (Shikiji) to Codeboxes
20-
rehypeShikiji,
20+
[
21+
rehypeShikiji,
22+
{
23+
twoslash: true,
24+
// We use the faster WASM engine on the server instead of the web-optimized version.
25+
//
26+
// Currently we fall back to the JavaScript RegEx engine
27+
// on Cloudflare workers because `shiki/wasm` requires loading via
28+
// `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support
29+
// for security reasons.
30+
//
31+
// TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment
32+
// variable for detection instead of current method, which will enable better
33+
// tree-shaking.
34+
// Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615
35+
wasm: !('Cloudflare' in global),
36+
},
37+
],
2138
];
2239

2340
/**

packages/rehype-shiki/src/__tests__/highlighter.test.mjs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,7 @@ mock.module('shiki/themes/nord.mjs', {
2020
});
2121

2222
describe('createHighlighter', async () => {
23-
const { createHighlighter } = await import('../highlighter.mjs');
24-
25-
describe('getLanguageDisplayName', () => {
26-
it('returns display name for known languages', () => {
27-
const langs = [
28-
{ name: 'javascript', displayName: 'JavaScript', aliases: ['js'] },
29-
];
30-
const highlighter = createHighlighter({ langs });
31-
32-
assert.strictEqual(
33-
highlighter.getLanguageDisplayName('javascript'),
34-
'JavaScript'
35-
);
36-
assert.strictEqual(
37-
highlighter.getLanguageDisplayName('js'),
38-
'JavaScript'
39-
);
40-
});
41-
42-
it('returns original name for unknown languages', () => {
43-
const highlighter = createHighlighter({ langs: [] });
44-
assert.strictEqual(
45-
highlighter.getLanguageDisplayName('unknown'),
46-
'unknown'
47-
);
48-
});
49-
});
23+
const { default: createHighlighter } = await import('../highlighter.mjs');
5024

5125
describe('highlightToHtml', () => {
5226
it('extracts inner HTML from code tag', () => {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, it, mock } from 'node:test';
33

44
// Simplified mocks - only mock what's actually needed
55
mock.module('../index.mjs', {
6-
namedExports: { highlightToHast: mock.fn(() => ({ children: [] })) },
6+
defaultExport: () => ({ highlightToHast: mock.fn(() => ({ children: [] })) }),
77
});
88

99
mock.module('classnames', {
@@ -23,13 +23,13 @@ describe('rehypeShikiji', async () => {
2323
const { default: rehypeShikiji } = await import('../plugin.mjs');
2424
const mockTree = { type: 'root', children: [] };
2525

26-
it('calls visit twice', () => {
26+
it('calls visit twice', async () => {
2727
mockVisit.mock.resetCalls();
28-
rehypeShikiji()(mockTree);
28+
await rehypeShikiji()(mockTree);
2929
assert.strictEqual(mockVisit.mock.calls.length, 2);
3030
});
3131

32-
it('creates CodeTabs for multiple code blocks', () => {
32+
it('creates CodeTabs for multiple code blocks', async () => {
3333
const parent = {
3434
children: [
3535
{
@@ -61,7 +61,7 @@ describe('rehypeShikiji', async () => {
6161
}
6262
});
6363

64-
rehypeShikiji()(mockTree);
64+
await rehypeShikiji()(mockTree);
6565
assert.ok(parent.children.some(child => child.tagName === 'CodeTabs'));
6666
});
6767
});

packages/rehype-shiki/src/highlighter.mjs

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,35 @@ import shikiNordTheme from 'shiki/themes/nord.mjs';
33

44
const DEFAULT_THEME = {
55
// We are updating this color because the background color and comment text color
6-
// in the Codebox component do not comply with accessibility standards
7-
// @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
6+
// in the Codebox component do not comply with accessibility standards.
7+
// See: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
88
colorReplacements: { '#616e88': '#707e99' },
99
...shikiNordTheme,
1010
};
1111

12+
export const getLanguageByName = (language, langs) =>
13+
langs.find(
14+
({ name, aliases }) =>
15+
name.toLowerCase() === language.toLowerCase() ||
16+
(aliases !== undefined && aliases.includes(language.toLowerCase()))
17+
);
18+
1219
/**
13-
* Creates a syntax highlighter with utility functions
14-
* @param {import('@shikijs/core').HighlighterCoreOptions} options - Configuration options for the highlighter
20+
* Factory function to create a syntax highlighter instance with utility methods.
21+
*
22+
* @param {Object} params - Parameters for highlighter creation.
23+
* @param {import('@shikijs/core').HighlighterCoreOptions} [params.coreOptions] - Core options for the highlighter.
24+
* @param {import('@shikijs/core').CodeToHastOptions} [params.highlighterOptions] - Additional options for highlighting.
1525
*/
16-
export const createHighlighter = ({ transformers, ...options }) => {
17-
const shiki = createHighlighterCoreSync({
26+
const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => {
27+
const options = {
1828
themes: [DEFAULT_THEME],
19-
...options,
20-
});
29+
...coreOptions,
30+
};
2131

22-
const theme = options.themes?.[0] ?? DEFAULT_THEME;
23-
const langs = options.langs ?? [];
32+
const shiki = createHighlighterCoreSync(options);
2433

25-
const getLanguageDisplayName = language => {
26-
const languageByIdOrAlias = langs.find(
27-
({ name, aliases }) =>
28-
name.toLowerCase() === language.toLowerCase() ||
29-
(aliases !== undefined && aliases.includes(language.toLowerCase()))
30-
);
31-
32-
return languageByIdOrAlias?.displayName ?? language;
33-
};
34+
const theme = options.themes[0];
3435

3536
/**
3637
* Highlights code and returns the inner HTML inside the <code> tag
@@ -42,7 +43,7 @@ export const createHighlighter = ({ transformers, ...options }) => {
4243
*/
4344
const highlightToHtml = (code, lang, meta = {}) =>
4445
shiki
45-
.codeToHtml(code, { lang, theme, meta, transformers })
46+
.codeToHtml(code, { lang, theme, meta, ...highlighterOptions })
4647
// Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag
4748
// since our own CodeBox component handles the <code> tag, we just want to extract
4849
// the inner highlighted code to the CodeBox
@@ -56,12 +57,13 @@ export const createHighlighter = ({ transformers, ...options }) => {
5657
* @param {Record<string, any>} meta - Metadata
5758
*/
5859
const highlightToHast = (code, lang, meta = {}) =>
59-
shiki.codeToHast(code, { lang, theme, meta, transformers });
60+
shiki.codeToHast(code, { lang, theme, meta, ...highlighterOptions });
6061

6162
return {
6263
shiki,
63-
getLanguageDisplayName,
6464
highlightToHtml,
6565
highlightToHast,
6666
};
6767
};
68+
69+
export default createHighlighter;
Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
2-
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma';
3-
import { transformerTwoslash } from '@shikijs/twoslash';
1+
// Keep all imports at the top
42
import cLanguage from 'shiki/langs/c.mjs';
53
import coffeeScriptLanguage from 'shiki/langs/coffeescript.mjs';
64
import cPlusPlusLanguage from 'shiki/langs/cpp.mjs';
@@ -16,57 +14,93 @@ import shellSessionLanguage from 'shiki/langs/shellsession.mjs';
1614
import typeScriptLanguage from 'shiki/langs/typescript.mjs';
1715
import yamlLanguage from 'shiki/langs/yaml.mjs';
1816

19-
import { createHighlighter } from './highlighter.mjs';
17+
import createHighlighter, { getLanguageByName } from './highlighter.mjs';
2018

21-
const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } =
22-
createHighlighter({
23-
transformers: [
19+
/**
20+
* @typedef {Object} HighlighterOptions
21+
* @property {boolean|Object} [wasm=false] - WebAssembly options for the regex engine
22+
* @property {boolean|Object} [twoslash=false] - Twoslash configuration options
23+
* @param {import('@shikijs/core').HighlighterCoreOptions} [coreOptions] - Core options for the highlighter.
24+
* @param {import('@shikijs/core').CodeToHastOptions} [highlighterOptions] - Additional options for highlighting.
25+
*/
26+
27+
/**
28+
* Creates the appropriate regex engine based on configuration
29+
* @param {HighlighterOptions} options - Configuration options
30+
*/
31+
async function getEngine({ wasm = false }) {
32+
if (wasm) {
33+
const { createJavaScriptRegexEngine } = await import(
34+
'@shikijs/engine-javascript'
35+
);
36+
return createJavaScriptRegexEngine();
37+
}
38+
39+
const { createOnigurumaEngine } = await import('@shikijs/engine-oniguruma');
40+
return createOnigurumaEngine(
41+
typeof wasm === 'boolean' ? await import('shiki/wasm') : wasm
42+
);
43+
}
44+
45+
/**
46+
* Configures and returns transformers based on options
47+
* @param {HighlighterOptions} options - Configuration options
48+
*/
49+
async function getTransformers({ twoslash = false }) {
50+
const transformers = [];
51+
52+
if (twoslash) {
53+
const { transformerTwoslash } = await import('@shikijs/twoslash');
54+
55+
transformers.push(
2456
transformerTwoslash({
2557
langs: ['ts', 'js', 'cjs', 'mjs'],
26-
// Don't show JSDoc
27-
rendererRich: {
28-
jsdoc: false,
29-
},
30-
// Don't throw on errors on untype-able code
58+
rendererRich: { jsdoc: false },
3159
throws: false,
32-
}),
33-
],
34-
// We use the faster WASM engine on the server instead of the web-optimized version.
35-
//
36-
// Currently we fall back to the JavaScript RegEx engine
37-
// on Cloudflare workers because `shiki/wasm` requires loading via
38-
// `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support
39-
// for security reasons.
40-
//
41-
// TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment
42-
// variable for detection instead of current method, which will enable better
43-
// tree-shaking.
44-
// Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615
45-
engine:
46-
'Cloudflare' in globalThis
47-
? createJavaScriptRegexEngine()
48-
: await createOnigurumaEngine(import('shiki/wasm')),
49-
langs: [
50-
...cLanguage,
51-
...coffeeScriptLanguage,
52-
...cPlusPlusLanguage,
53-
...diffLanguage,
54-
...dockerLanguage,
55-
...httpLanguage,
56-
...iniLanguage,
57-
{
58-
...javaScriptLanguage[0],
59-
// We patch the JavaScript language to include the CommonJS and ES Module aliases
60-
// that are commonly used (non-standard aliases) within our API docs and Blog posts
61-
aliases: javaScriptLanguage[0].aliases.concat('cjs', 'mjs'),
62-
},
63-
...jsonLanguage,
64-
...powershellLanguage,
65-
...shellScriptLanguage,
66-
...shellSessionLanguage,
67-
...typeScriptLanguage,
68-
...yamlLanguage,
69-
],
70-
});
60+
...(typeof twoslash === 'object' ? twoslash : {}),
61+
})
62+
);
63+
}
64+
65+
return transformers;
66+
}
67+
68+
export const LANGS = [
69+
...cLanguage,
70+
...coffeeScriptLanguage,
71+
...cPlusPlusLanguage,
72+
...diffLanguage,
73+
...dockerLanguage,
74+
...httpLanguage,
75+
...iniLanguage,
76+
{
77+
...javaScriptLanguage[0],
78+
aliases: javaScriptLanguage[0].aliases.concat('cjs', 'mjs'),
79+
},
80+
...jsonLanguage,
81+
...powershellLanguage,
82+
...shellScriptLanguage,
83+
...shellSessionLanguage,
84+
...typeScriptLanguage,
85+
...yamlLanguage,
86+
];
7187

72-
export { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml };
88+
export const getLanguageDisplayName = language =>
89+
getLanguageByName(language, LANGS)?.displayName ?? language;
90+
91+
/**
92+
* Creates and configures a syntax highlighter
93+
* @param {HighlighterOptions} options - Configuration options
94+
*/
95+
export default async (options = {}) =>
96+
createHighlighter({
97+
coreOptions: {
98+
...options.coreOptions,
99+
langs: LANGS,
100+
engine: await getEngine(options),
101+
},
102+
highlighterOptions: {
103+
...options.highlighterOptions,
104+
transformers: await getTransformers(options),
105+
},
106+
});

packages/rehype-shiki/src/minimal.mjs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
22
import powershellLanguage from 'shiki/langs/powershell.mjs';
33
import shellScriptLanguage from 'shiki/langs/shellscript.mjs';
44

5-
import { createHighlighter } from './highlighter.mjs';
5+
import createHighlighter, { getLanguageByName } from './highlighter.mjs';
66

7-
const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } =
8-
createHighlighter({
7+
export const LANGS = [...powershellLanguage, ...shellScriptLanguage];
8+
9+
export const getLanguageDisplayName = language =>
10+
getLanguageByName(language, LANGS)?.displayName ?? language;
11+
12+
export const { shiki, highlightToHast, highlightToHtml } = createHighlighter({
13+
coreOptions: {
914
// For the minimal (web) Shiki, we want to use the simpler,
1015
// JavaScript based engine.
1116
engine: createJavaScriptRegexEngine(),
1217
langs: [...powershellLanguage, ...shellScriptLanguage],
13-
});
14-
15-
export { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml };
18+
},
19+
});

packages/rehype-shiki/src/plugin.mjs

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

7-
import { highlightToHast } from './index.mjs';
7+
import createHighlighter from './index.mjs';
88

99
// This is what Remark will use as prefix within a <pre> className
1010
// to attribute the current language of the <pre> element
@@ -53,8 +53,15 @@ function isCodeBlock(node) {
5353
);
5454
}
5555

56-
export default function rehypeShikiji() {
57-
return function (tree) {
56+
/**
57+
* @param {import('./index.mjs').HighlighterOptions} options
58+
*/
59+
export default function rehypeShikiji(options) {
60+
let highlighter;
61+
62+
return async function (tree) {
63+
highlighter ??= await createHighlighter(options);
64+
5865
visit(tree, 'element', (_, index, parent) => {
5966
const languages = [];
6067
const displayNames = [];
@@ -163,7 +170,7 @@ export default function rehypeShikiji() {
163170
const languageId = codeLanguage.slice(languagePrefix.length);
164171

165172
// Parses the <pre> contents and returns a HAST tree with the highlighted code
166-
const { children } = highlightToHast(
173+
const { children } = highlighter.highlightToHast(
167174
preElementContents,
168175
languageId,
169176
meta

0 commit comments

Comments
 (0)