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