diff --git a/.github/workflows/lint-and-tests.yml b/.github/workflows/lint-and-tests.yml index 368edb118c53b..3ce3ffc34d5e4 100644 --- a/.github/workflows/lint-and-tests.yml +++ b/.github/workflows/lint-and-tests.yml @@ -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 diff --git a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx index 1ee5e6303d0c0..0497ca0d234a9 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -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'; @@ -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 diff --git a/apps/site/components/MDX/CodeBox/index.tsx b/apps/site/components/MDX/CodeBox/index.tsx index 8aa1367993b20..ef0b977a1a995 100644 --- a/apps/site/components/MDX/CodeBox/index.tsx +++ b/apps/site/components/MDX/CodeBox/index.tsx @@ -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 }; diff --git a/apps/site/next.mdx.plugins.mjs b/apps/site/next.mdx.plugins.mjs index 51a76ef0f7bcf..016407b360422 100644 --- a/apps/site/next.mdx.plugins.mjs +++ b/apps/site/next.mdx.plugins.mjs @@ -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 */ diff --git a/apps/site/package.json b/apps/site/package.json index a840c139bb22f..52d687ed90230 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -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", @@ -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", @@ -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", @@ -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" }, diff --git a/apps/site/util/getHighlighter.ts b/apps/site/util/getHighlighter.ts deleted file mode 100644 index 63aa6d1c0b0ba..0000000000000 --- a/apps/site/util/getHighlighter.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createHighlighterCoreSync } from '@shikijs/core'; -import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'; - -import { LANGUAGES, DEFAULT_THEME } from '#site/shiki.config.mjs'; - -// This creates a memoized minimal Shikiji Syntax Highlighter -export const shiki = createHighlighterCoreSync({ - themes: [DEFAULT_THEME], - langs: LANGUAGES, - // Let's use Shiki's new Experimental JavaScript-based regex engine! - engine: createJavaScriptRegexEngine(), -}); - -export const highlightToHtml = (code: string, language: string) => - shiki - .codeToHtml(code, { lang: language, theme: DEFAULT_THEME }) - // Shiki will always return the Highlighted code encapsulated in a
and tag
- // since our own CodeBox component handles the tag, we just want to extract
- // the inner highlighted code to the CodeBox
- .match(/(.+?)<\/code>/s)![1];
-
-export const highlightToHast = (code: string, language: string) =>
- shiki.codeToHast(code, { lang: language, theme: DEFAULT_THEME });
diff --git a/apps/site/util/getLanguageDisplayName.ts b/apps/site/util/getLanguageDisplayName.ts
deleted file mode 100644
index a62081bd93c4d..0000000000000
--- a/apps/site/util/getLanguageDisplayName.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { LANGUAGES } from '#site/shiki.config.mjs';
-
-export const getLanguageDisplayName = (language: string): string => {
- const languageByIdOrAlias = LANGUAGES.find(
- ({ name, aliases }) =>
- name.toLowerCase() === language.toLowerCase() ||
- (aliases !== undefined && aliases.includes(language.toLowerCase()))
- );
-
- return languageByIdOrAlias?.displayName ?? language;
-};
diff --git a/packages/rehype-shiki/eslint.config.js b/packages/rehype-shiki/eslint.config.js
new file mode 100644
index 0000000000000..a6d556fefa8f3
--- /dev/null
+++ b/packages/rehype-shiki/eslint.config.js
@@ -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',
+ },
+ },
+];
diff --git a/packages/rehype-shiki/package.json b/packages/rehype-shiki/package.json
new file mode 100644
index 0000000000000..ed32e84e52e9e
--- /dev/null
+++ b/packages/rehype-shiki/package.json
@@ -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"
+ }
+}
diff --git a/apps/site/util/__tests__/getLanguageDisplayName.test.mjs b/packages/rehype-shiki/src/__tests__/languages.test.mjs
similarity index 62%
rename from apps/site/util/__tests__/getLanguageDisplayName.test.mjs
rename to packages/rehype-shiki/src/__tests__/languages.test.mjs
index e70c4ecf5bc27..0900f25ff365b 100644
--- a/apps/site/util/__tests__/getLanguageDisplayName.test.mjs
+++ b/packages/rehype-shiki/src/__tests__/languages.test.mjs
@@ -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');
diff --git a/packages/rehype-shiki/src/highlighter.mjs b/packages/rehype-shiki/src/highlighter.mjs
new file mode 100644
index 0000000000000..a31fda8da7300
--- /dev/null
+++ b/packages/rehype-shiki/src/highlighter.mjs
@@ -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 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 and tag
+ // since our own CodeBox component handles the tag, we just want to extract
+ // the inner highlighted code to the CodeBox
+ .match(/(.+?)<\/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 });
diff --git a/packages/rehype-shiki/src/index.mjs b/packages/rehype-shiki/src/index.mjs
new file mode 100644
index 0000000000000..68cfc211ea64c
--- /dev/null
+++ b/packages/rehype-shiki/src/index.mjs
@@ -0,0 +1,5 @@
+import { rehypeShikiji } from './plugin.mjs';
+export * from './highlighter.mjs';
+export * from './languages.mjs';
+
+export default rehypeShikiji;
diff --git a/apps/site/shiki.config.mjs b/packages/rehype-shiki/src/languages.mjs
similarity index 77%
rename from apps/site/shiki.config.mjs
rename to packages/rehype-shiki/src/languages.mjs
index f940a9311d686..d9764697ff335 100644
--- a/apps/site/shiki.config.mjs
+++ b/packages/rehype-shiki/src/languages.mjs
@@ -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;
+};
diff --git a/apps/site/next.mdx.shiki.mjs b/packages/rehype-shiki/src/plugin.mjs
similarity index 98%
rename from apps/site/next.mdx.shiki.mjs
rename to packages/rehype-shiki/src/plugin.mjs
index 9b8c0edbb94d4..d486615296c40 100644
--- a/apps/site/next.mdx.shiki.mjs
+++ b/packages/rehype-shiki/src/plugin.mjs
@@ -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 className
// to attribute the current language of the element
@@ -53,7 +53,7 @@ function isCodeBlock(node) {
);
}
-export default function rehypeShikiji() {
+export function rehypeShikiji() {
return function (tree) {
visit(tree, 'element', (_, index, parent) => {
const languages = [];
diff --git a/packages/rehype-shiki/turbo.json b/packages/rehype-shiki/turbo.json
new file mode 100644
index 0000000000000..aa0ce04f53ac8
--- /dev/null
+++ b/packages/rehype-shiki/turbo.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build": {},
+ "lint:js": {
+ "inputs": ["src/**/*.mjs"]
+ },
+ "test": {
+ "inputs": ["src/**/*.mjs"]
+ }
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4bef89954622a..741f186fd0943 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -51,6 +51,9 @@ importers:
'@mdx-js/mdx':
specifier: ^3.1.0
version: 3.1.0(acorn@8.14.1)
+ '@node-core/rehype-shiki':
+ specifier: workspace:*
+ version: link:../../packages/rehype-shiki
'@node-core/ui-components':
specifier: workspace:*
version: link:../../packages/ui-components
@@ -80,7 +83,7 @@ importers:
version: 2.1.4
'@radix-ui/react-slot':
specifier: ^1.1.2
- version: 1.2.0(@types/react@19.1.2)(react@19.1.0)
+ version: 1.2.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-tabs':
specifier: ^1.1.3
version: 1.1.9(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -90,12 +93,6 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@shikijs/core':
- specifier: ^3.2.2
- version: 3.3.0
- '@shikijs/engine-javascript':
- specifier: ^3.2.2
- version: 3.3.0
'@tailwindcss/postcss':
specifier: ~4.1.5
version: 4.1.5
@@ -135,9 +132,6 @@ importers:
gray-matter:
specifier: ~4.0.3
version: 4.0.3
- hast-util-to-string:
- specifier: ~3.0.1
- version: 3.0.1
next:
specifier: 15.3.1
version: 15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -180,18 +174,12 @@ importers:
semver:
specifier: ~7.7.1
version: 7.7.1
- shiki:
- specifier: ~3.3.0
- version: 3.3.0
sval:
specifier: ^0.6.3
version: 0.6.7
tailwindcss:
specifier: ~4.0.17
version: 4.0.17
- unist-util-visit:
- specifier: ~5.0.0
- version: 5.0.0
vfile:
specifier: ~6.0.3
version: 6.0.3
@@ -317,6 +305,27 @@ importers:
specifier: ~8.31.1
version: 8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
+ packages/rehype-shiki:
+ dependencies:
+ '@shikijs/core':
+ specifier: ^3.3.0
+ version: 3.3.0
+ '@shikijs/engine-javascript':
+ specifier: ^3.3.0
+ version: 3.3.0
+ classnames:
+ specifier: ~2.5.1
+ version: 2.5.1
+ hast-util-to-string:
+ specifier: ^3.0.1
+ version: 3.0.1
+ shiki:
+ specifier: ~3.3.0
+ version: 3.3.0
+ unist-util-visit:
+ specifier: ^5.0.0
+ version: 5.0.0
+
packages/ui-components:
dependencies:
'@heroicons/react':
@@ -12774,7 +12783,7 @@ snapshots:
tinyglobby: 0.2.13
unrs-resolver: 1.7.2
optionalDependencies:
- eslint-plugin-import: 2.31.0(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2))
eslint-plugin-import-x: 4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies:
- supports-color
@@ -12802,27 +12811,16 @@ snapshots:
- bluebird
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.26.0(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.26.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 4.3.4(eslint-plugin-import-x@4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.4.2))
- transitivePeerDependencies:
- - supports-color
-
- eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2)):
- dependencies:
- debug: 3.2.7
- optionalDependencies:
- eslint: 9.26.0(jiti@2.4.2)
- eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 4.3.4(eslint-plugin-import-x@4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.4.2))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
- optional: true
eslint-plugin-import-x@4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3):
dependencies:
@@ -12853,7 +12851,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.26.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.26.0(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -12871,34 +12869,6 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-import@2.31.0(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2)):
- dependencies:
- '@rtsao/scc': 1.1.0
- array-includes: 3.1.8
- array.prototype.findlastindex: 1.2.6
- array.prototype.flat: 1.3.3
- array.prototype.flatmap: 1.3.3
- debug: 3.2.7
- doctrine: 2.1.0
- eslint: 9.26.0(jiti@2.4.2)
- eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.4.2))
- hasown: 2.0.2
- is-core-module: 2.16.1
- is-glob: 4.0.3
- minimatch: 3.1.2
- object.fromentries: 2.0.8
- object.groupby: 1.0.3
- object.values: 1.2.1
- semver: 6.3.1
- string.prototype.trimend: 1.0.9
- tsconfig-paths: 3.15.0
- transitivePeerDependencies:
- - eslint-import-resolver-typescript
- - eslint-import-resolver-webpack
- - supports-color
- optional: true
-
eslint-plugin-jsx-a11y@6.10.2(eslint@9.26.0(jiti@2.4.2)):
dependencies:
aria-query: 5.3.2