Skip to content

chore(monorepo): rehype-shiki #7760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint-and-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ jobs:
if: ${{ !cancelled() && github.event_name != 'merge_group' }}
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
with:
files: ./apps/site/lcov.info,./packages/ui-components/lcov.info
files: ./apps/site/lcov.info,./packages/*/lcov.info

- name: Upload test results to Codecov
if: ${{ !cancelled() && github.event_name != 'merge_group' }}
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
with:
files: ./apps/site/junit.xml,./packages/ui-components/junit.xml
files: ./apps/site/junit.xml,./packages/*/junit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { highlightToHtml } from '@node-core/rehype-shiki';
import AlertBox from '@node-core/ui-components/Common/AlertBox';
import Skeleton from '@node-core/ui-components/Common/Skeleton';
import { useTranslations } from 'next-intl';
Expand All @@ -16,7 +17,6 @@ import {
} from '#site/providers/releaseProvider';
import type { ReleaseContextType } from '#site/types/release';
import { INSTALL_METHODS } from '#site/util/downloadUtils';
import { highlightToHtml } from '#site/util/getHighlighter';

// Creates a minimal JavaScript interpreter for parsing the JavaScript code from the snippets
// Note: that the code runs inside a sandboxed environment and cannot interact with any code outside of the sandbox
Expand Down
2 changes: 1 addition & 1 deletion apps/site/components/MDX/CodeBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getLanguageDisplayName } from '@node-core/rehype-shiki';
import type { FC, PropsWithChildren } from 'react';

import CodeBox from '#site/components/Common/CodeBox';
import { getLanguageDisplayName } from '#site/util/getLanguageDisplayName';

type CodeBoxProps = { className?: string; showCopyButton?: string };

Expand Down
3 changes: 1 addition & 2 deletions apps/site/next.mdx.plugins.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
'use strict';

import rehypeShikiji from '@node-core/rehype-shiki';
import remarkHeadings from '@vcarl/remark-headings';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import readingTime from 'remark-reading-time';

import rehypeShikiji from './next.mdx.shiki.mjs';

/**
* Provides all our Rehype Plugins that are used within MDX
*/
Expand Down
6 changes: 1 addition & 5 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@heroicons/react": "~2.2.0",
"@mdx-js/mdx": "^3.1.0",
"@node-core/rehype-shiki": "workspace:*",
"@node-core/ui-components": "workspace:*",
"@node-core/website-i18n": "workspace:*",
"@nodevu/core": "0.3.0",
Expand All @@ -41,8 +42,6 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.4",
"@shikijs/core": "^3.2.2",
"@shikijs/engine-javascript": "^3.2.2",
"@tailwindcss/postcss": "~4.1.5",
"@types/node": "22.15.3",
"@types/react": "^19.1.0",
Expand All @@ -56,7 +55,6 @@
"github-slugger": "~2.0.0",
"glob": "~11.0.1",
"gray-matter": "~4.0.3",
"hast-util-to-string": "~3.0.1",
"next": "15.3.1",
"next-intl": "~4.1.0",
"next-themes": "~0.4.6",
Expand All @@ -71,10 +69,8 @@
"remark-gfm": "~4.0.1",
"remark-reading-time": "~2.0.1",
"semver": "~7.7.1",
"shiki": "~3.3.0",
"sval": "^0.6.3",
"tailwindcss": "~4.0.17",
"unist-util-visit": "~5.0.0",
"vfile": "~6.0.3",
"vfile-matter": "~5.0.1"
},
Expand Down
23 changes: 0 additions & 23 deletions apps/site/util/getHighlighter.ts

This file was deleted.

11 changes: 0 additions & 11 deletions apps/site/util/getLanguageDisplayName.ts

This file was deleted.

17 changes: 17 additions & 0 deletions packages/rehype-shiki/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import baseConfig from '../../eslint.config.js';

export default [
...baseConfig,
{
languageOptions: {
parserOptions: {
// Allow nullish syntax (i.e. "?." or "??")
ecmaVersion: 2020,
},
},
rules: {
// Shiki's export isn't named, it's a re-export
'import-x/named': 'off',
},
},
];
18 changes: 18 additions & 0 deletions packages/rehype-shiki/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@node-core/rehype-shiki",
"type": "module",
"main": "./src/index.mjs",
"module": "./src/index.mjs",
"scripts": {
"lint:js": "eslint \"**/*.mjs\"",
"test": "node --test"
},
"dependencies": {
"@shikijs/core": "^3.3.0",
"@shikijs/engine-javascript": "^3.3.0",
"classnames": "~2.5.1",
"hast-util-to-string": "^3.0.1",
"shiki": "~3.3.0",
"unist-util-visit": "^5.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import assert from 'node:assert/strict';
import { it, describe, mock } from 'node:test';
import { it, describe } from 'node:test';

describe('getLanguageDisplayName', async () => {
mock.module('#site/shiki.config.mjs', {
namedExports: {
LANGUAGES: [
{ name: 'javascript', aliases: ['js'], displayName: 'JavaScript' },
{ name: 'typescript', aliases: ['ts'], displayName: 'TypeScript' },
],
},
});
import { getLanguageDisplayName, LANGUAGES } from '../languages.mjs';

const { getLanguageDisplayName } = await import(
'#site/util/getLanguageDisplayName'
);
LANGUAGES.splice(
0,
LANGUAGES.length,
{ name: 'javascript', aliases: ['js'], displayName: 'JavaScript' },
{ name: 'typescript', aliases: ['ts'], displayName: 'TypeScript' }
);

describe('getLanguageDisplayName', async () => {
it('should return the display name for a known language', () => {
assert.equal(getLanguageDisplayName('javascript'), 'JavaScript');
assert.equal(getLanguageDisplayName('js'), 'JavaScript');
Expand Down
47 changes: 47 additions & 0 deletions packages/rehype-shiki/src/highlighter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createHighlighterCoreSync } from '@shikijs/core';
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';

import { LANGUAGES, DEFAULT_THEME } from './languages.mjs';

let _shiki;

/**
* Lazy-load and memoize the minimal Shikiji Syntax Highlighter
* @returns {import('@shikijs/core').HighlighterCore}
*/
export const getShiki = () => {
if (!_shiki) {
_shiki = createHighlighterCoreSync({
themes: [DEFAULT_THEME],
langs: LANGUAGES,
// Let's use Shiki's new Experimental JavaScript-based regex engine!
engine: createJavaScriptRegexEngine(),
});
}
return _shiki;
};

/**
* Highlights code and returns the inner HTML inside the <code> tag
*
* @param {string} code - The code to highlight
* @param {string} language - The programming language to use for highlighting
* @returns {string} The inner HTML of the highlighted code
*/
export const highlightToHtml = (code, language) =>
getShiki()
.codeToHtml(code, { lang: language, theme: DEFAULT_THEME })
// Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag
// since our own CodeBox component handles the <code> tag, we just want to extract
// the inner highlighted code to the CodeBox
.match(/<code>(.+?)<\/code>/s)[1];

/**
* Highlights code and returns a HAST tree
*
* @param {string} code - The code to highlight
* @param {string} language - The programming language to use for highlighting
* @returns {import('hast').Element} The HAST representation of the highlighted code
*/
export const highlightToHast = (code, language) =>
getShiki().codeToHast(code, { lang: language, theme: DEFAULT_THEME });
5 changes: 5 additions & 0 deletions packages/rehype-shiki/src/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { rehypeShikiji } from './plugin.mjs';
export * from './highlighter.mjs';
export * from './languages.mjs';

export default rehypeShikiji;
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,18 @@ export const DEFAULT_THEME = {
colorReplacements: { '#616e88': '#707e99' },
...shikiNordTheme,
};

/**
* Get the display name of a given language
* @param {string} language The language ID
* @returns {string} The display name of the language, or the input as a fallback
*/
export const getLanguageDisplayName = language => {
const languageByIdOrAlias = LANGUAGES.find(
({ name, aliases }) =>
name.toLowerCase() === language.toLowerCase() ||
(aliases !== undefined && aliases.includes(language.toLowerCase()))
);

return languageByIdOrAlias?.displayName ?? language;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classNames from 'classnames';
import { toString } from 'hast-util-to-string';
import { SKIP, visit } from 'unist-util-visit';

import { highlightToHast } from './util/getHighlighter';
import { highlightToHast } from './highlighter.mjs';

// This is what Remark will use as prefix within a <pre> className
// to attribute the current language of the <pre> element
Expand Down Expand Up @@ -53,7 +53,7 @@ function isCodeBlock(node) {
);
}

export default function rehypeShikiji() {
export function rehypeShikiji() {
return function (tree) {
visit(tree, 'element', (_, index, parent) => {
const languages = [];
Expand Down
12 changes: 12 additions & 0 deletions packages/rehype-shiki/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"lint:js": {
"inputs": ["src/**/*.mjs"]
},
"test": {
"inputs": ["src/**/*.mjs"]
}
}
}
Loading
Loading