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..e6931e625d3f8 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -1,5 +1,7 @@ 'use client'; +import createSval from '@node-core/mdx/evaluator'; +import { highlightToHtml } from '@node-core/mdx/highlighter'; import AlertBox from '@node-core/ui-components/Common/AlertBox'; import Skeleton from '@node-core/ui-components/Common/Skeleton'; import { useTranslations } from 'next-intl'; @@ -9,14 +11,12 @@ import { useContext, useMemo } from 'react'; import CodeBox from '#site/components/Common/CodeBox'; import Link from '#site/components/Link'; import LinkWithArrow from '#site/components/LinkWithArrow'; -import { createSval } from '#site/next.jsx.compiler.mjs'; import { ReleaseContext, ReleasesContext, } 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..f7f71d3758057 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/mdx/utils'; 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.dynamic.mjs b/apps/site/next.dynamic.mjs index a7e3467a4f123..a5930800bc026 100644 --- a/apps/site/next.dynamic.mjs +++ b/apps/site/next.dynamic.mjs @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'; import { join, normalize, sep } from 'node:path'; +import compile from '@node-core/mdx/compiler'; import matter from 'gray-matter'; import { cache } from 'react'; import { VFile } from 'vfile'; @@ -22,7 +23,6 @@ import { import { getMarkdownFiles } from './next.helpers.mjs'; import { siteConfig } from './next.json.mjs'; import { availableLocaleCodes, defaultLocale } from './next.locales.mjs'; -import { compile } from './next.mdx.compiler.mjs'; import { MDX_COMPONENTS } from './next.mdx.components.mjs'; // This is the combination of the Application Base URL and Base PATH diff --git a/apps/site/next.jsx.compiler.mjs b/apps/site/next.jsx.compiler.mjs deleted file mode 100644 index f536b14e84502..0000000000000 --- a/apps/site/next.jsx.compiler.mjs +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -import Sval from 'sval'; - -/** - * Creates a JavaScript Evaluater - * - * @param {Record} dependencies All sort of dependencies to be passed to the JavaScript context - * @param {'module' | 'script'} mode The mode of the JavaScript execution - * - * @returns {Sval} Returns an Sandboxed instance of a JavaScript interpreter - */ -export const createSval = (dependencies = {}, mode = 'module') => { - const svalInterpreter = new Sval({ - ecmaVer: 'latest', - sandBox: true, - sourceType: mode, - }); - - svalInterpreter.import(dependencies); - - return svalInterpreter; -}; diff --git a/apps/site/next.mdx.shiki.mjs b/apps/site/next.mdx.shiki.mjs deleted file mode 100644 index 9b8c0edbb94d4..0000000000000 --- a/apps/site/next.mdx.shiki.mjs +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -import classNames from 'classnames'; -import { toString } from 'hast-util-to-string'; -import { SKIP, visit } from 'unist-util-visit'; - -import { highlightToHast } from './util/getHighlighter'; - -// This is what Remark will use as prefix within a
 className
-// to attribute the current language of the 
 element
-const languagePrefix = 'language-';
-
-/**
- * Retrieve the value for the given meta key.
- *
- * @example - Returns "CommonJS"
- * getMetaParameter('displayName="CommonJS"', 'displayName');
- *
- * @param {any} meta - The meta parameter.
- * @param {string} key - The key to retrieve the value.
- *
- * @return {string | undefined} - The value related to the given key.
- */
-function getMetaParameter(meta, key) {
-  if (typeof meta !== 'string') {
-    return;
-  }
-
-  const matches = meta.match(new RegExp(`${key}="(?[^"]*)"`));
-  const parameter = matches?.groups.parameter;
-
-  return parameter !== undefined && parameter.length > 0
-    ? parameter
-    : undefined;
-}
-
-/**
- * @typedef {import('unist').Node} Node
- * @property {string} tagName
- * @property {Array} children
- */
-
-/**
- * Checks if the given node is a valid code element.
- *
- * @param {import('unist').Node} node - The node to be verified.
- *
- * @return {boolean} - True when it is a valid code element, false otherwise.
- */
-function isCodeBlock(node) {
-  return Boolean(
-    node?.tagName === 'pre' && node?.children[0].tagName === 'code'
-  );
-}
-
-export default function rehypeShikiji() {
-  return function (tree) {
-    visit(tree, 'element', (_, index, parent) => {
-      const languages = [];
-      const displayNames = [];
-      const codeTabsChildren = [];
-
-      let defaultTab = '0';
-      let currentIndex = index;
-
-      while (isCodeBlock(parent?.children[currentIndex])) {
-        const codeElement = parent?.children[currentIndex].children[0];
-
-        const displayName = getMetaParameter(
-          codeElement.data?.meta,
-          'displayName'
-        );
-
-        // We should get the language name from the class name
-        if (codeElement.properties.className?.length) {
-          const className = codeElement.properties.className.join(' ');
-          const matches = className.match(/language-(?.*)/);
-
-          languages.push(matches?.groups.language ?? 'text');
-        }
-
-        // Map the display names of each variant for the CodeTab
-        displayNames.push(displayName?.replaceAll('|', '') ?? '');
-
-        codeTabsChildren.push(parent?.children[currentIndex]);
-
-        // If `active="true"` is provided in a CodeBox
-        // then the default selected entry of the CodeTabs will be the desired entry
-        const specificActive = getMetaParameter(
-          codeElement.data?.meta,
-          'active'
-        );
-
-        if (specificActive === 'true') {
-          defaultTab = String(codeTabsChildren.length - 1);
-        }
-
-        const nextNode = parent?.children[currentIndex + 1];
-
-        // If the CodeBoxes are on the root tree the next Element will be
-        // an empty text element so we should skip it
-        currentIndex += nextNode && nextNode?.type === 'text' ? 2 : 1;
-      }
-
-      if (codeTabsChildren.length >= 2) {
-        const codeTabElement = {
-          type: 'element',
-          tagName: 'CodeTabs',
-          children: codeTabsChildren,
-          properties: {
-            languages: languages.join('|'),
-            displayNames: displayNames.join('|'),
-            defaultTab,
-          },
-        };
-
-        // This removes all the original Code Elements and adds a new CodeTab Element
-        // at the original start of the first Code Element
-        parent.children.splice(index, currentIndex - index, codeTabElement);
-
-        // Prevent visiting the code block children and for the next N Elements
-        // since all of them belong to this CodeTabs Element
-        return [SKIP];
-      }
-    });
-
-    visit(tree, 'element', (node, index, parent) => {
-      // We only want to process 
...
elements - if (!parent || index == null || node.tagName !== 'pre') { - return; - } - - // We want the contents of the
 element, hence we attempt to get the first child
-      const preElement = node.children[0];
-
-      // If thereƄs nothing inside the 
 element... What are we doing here?
-      if (!preElement || !preElement.properties) {
-        return;
-      }
-
-      // Ensure that we're not visiting a  element but it's inner contents
-      // (keep iterating further down until we reach where we want)
-      if (preElement.type !== 'element' || preElement.tagName !== 'code') {
-        return;
-      }
-
-      // Get the 
 element class names
-      const preClassNames = preElement.properties.className;
-
-      // The current classnames should be an array and it should have a length
-      if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
-        return;
-      }
-
-      // We want to retrieve the language class name from the class names
-      const codeLanguage = preClassNames.find(
-        c => typeof c === 'string' && c.startsWith(languagePrefix)
-      );
-
-      // If we didn't find any `language-` classname then we shouldn't highlight
-      if (typeof codeLanguage !== 'string') {
-        return;
-      }
-
-      // Retrieve the whole 
 contents as a parsed DOM string
-      const preElementContents = toString(preElement);
-
-      // Grabs the relevant alias/name of the language
-      const languageId = codeLanguage.slice(languagePrefix.length);
-
-      // Parses the 
 contents and returns a HAST tree with the highlighted code
-      const { children } = highlightToHast(preElementContents, languageId);
-
-      // Adds the original language back to the 
 element
-      children[0].properties.class = classNames(
-        children[0].properties.class,
-        codeLanguage
-      );
-
-      const showCopyButton = getMetaParameter(
-        preElement.data?.meta,
-        'showCopyButton'
-      );
-
-      // Adds a Copy Button to the CodeBox if requested as an additional parameter
-      // And avoids setting the property (overriding) if undefined or invalid value
-      if (showCopyButton && ['true', 'false'].includes(showCopyButton)) {
-        children[0].properties.showCopyButton = showCopyButton;
-      }
-
-      // Replaces the 
 element with the updated one
-      parent.children.splice(index, 1, ...children);
-    });
-  };
-}
diff --git a/apps/site/package.json b/apps/site/package.json
index a840c139bb22f..aefb55c19c828 100644
--- a/apps/site/package.json
+++ b/apps/site/package.json
@@ -27,8 +27,8 @@
   },
   "dependencies": {
     "@heroicons/react": "~2.2.0",
-    "@mdx-js/mdx": "^3.1.0",
     "@node-core/ui-components": "workspace:*",
+    "@node-core/mdx": "workspace:*",
     "@node-core/website-i18n": "workspace:*",
     "@nodevu/core": "0.3.0",
     "@opentelemetry/api-logs": "~0.200.0",
@@ -41,8 +41,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",
@@ -53,10 +51,8 @@
     "classnames": "~2.5.1",
     "cross-env": "7.0.3",
     "feed": "~4.2.2",
-    "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",
@@ -66,17 +62,9 @@
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "reading-time": "~1.5.0",
-    "rehype-autolink-headings": "~7.1.0",
-    "rehype-slug": "~6.0.0",
-    "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"
+    "vfile": "~6.0.3"
   },
   "devDependencies": {
     "@eslint/compat": "~1.2.8",
@@ -91,6 +79,7 @@
     "eslint-plugin-mdx": "~3.4.0",
     "eslint-plugin-react": "~7.37.4",
     "eslint-plugin-react-hooks": "5.2.0",
+    "github-slugger": "~2.0.0",
     "global-jsdom": "^26.0.0",
     "handlebars": "4.7.8",
     "jsdom": "^26.0.0",
diff --git a/apps/site/pages/id/about/security-reporting.mdx b/apps/site/pages/id/about/security-reporting.mdx
index 396acfce25eb6..307463d9aeb0a 100644
--- a/apps/site/pages/id/about/security-reporting.mdx
+++ b/apps/site/pages/id/about/security-reporting.mdx
@@ -46,7 +46,7 @@ Pemberitahuan keamanan akan didistribusikan melalui metode berikut.
 
 ## Komentar tentang kebijakan ini
 
-Jika Anda memiliki saran tentang bagaimana proses ini dapat ditingkatkan, silakan kirimkan [permintaan penarikan](https://github.com/nodejs/nodejs.org) atau [ajukan masalah (https://github.com/nodejs/security -wg/issues/new) untuk didiskusikan.
+Jika Anda memiliki saran tentang bagaimana proses ini dapat ditingkatkan, silakan kirimkan [permintaan penarikan](https://github.com/nodejs/nodejs.org) atau [ajukan masalah](https://github.com/nodejs/security-wg/issues/new) untuk didiskusikan.
 
 ## Praktik Terbaik OpenSSF
 
diff --git a/apps/site/util/__tests__/gitHubUtils.test.mjs b/apps/site/util/__tests__/gitHubUtils.test.mjs
index 5afc3e64b8c6a..aa45090631836 100644
--- a/apps/site/util/__tests__/gitHubUtils.test.mjs
+++ b/apps/site/util/__tests__/gitHubUtils.test.mjs
@@ -1,16 +1,8 @@
 import assert from 'node:assert/strict';
-import { describe, it, mock } from 'node:test';
+import { describe, it } from 'node:test';
 
-mock.module('github-slugger', {
-  defaultExport: class {},
-});
-
-const {
-  getGitHubAvatarUrl,
-  createGitHubSlugger,
-  getGitHubBlobUrl,
-  getGitHubApiDocsUrl,
-} = await import('#site/util/gitHubUtils');
+const { getGitHubAvatarUrl, getGitHubBlobUrl, getGitHubApiDocsUrl } =
+  await import('#site/util/gitHubUtils');
 
 describe('gitHubUtils', () => {
   it('getGitHubAvatarUrl returns the correct URL', () => {
@@ -20,10 +12,6 @@ describe('gitHubUtils', () => {
     );
   });
 
-  it('createGitHubSlugger returns a slugger', () => {
-    assert.notEqual(createGitHubSlugger(), undefined);
-  });
-
   it('getGitHubBlobUrl returns the correct URL', () => {
     const result = getGitHubBlobUrl('learn/getting-started/introduction.md');
     const expected =
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/gitHubUtils.ts b/apps/site/util/gitHubUtils.ts
index 813eaada810d2..c3a7d1b4dff93 100644
--- a/apps/site/util/gitHubUtils.ts
+++ b/apps/site/util/gitHubUtils.ts
@@ -1,14 +1,6 @@
-import GitHubSlugger from 'github-slugger';
-
 export const getGitHubAvatarUrl = (username: string): string =>
   `https://avatars.githubusercontent.com/${username}`;
 
-export const createGitHubSlugger = () => {
-  const githubSlugger = new GitHubSlugger();
-
-  return (text: string) => githubSlugger.slug(text);
-};
-
 export const getGitHubBlobUrl = (filename: string) =>
   `https://github.com/nodejs/nodejs.org/blob/main/apps/site/pages/en/${filename}`;
 
diff --git a/package.json b/package.json
index 808bcad2b1814..af7f016086c16 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
     "storybook": "turbo run storybook",
     "storybook:build": "turbo run storybook:build",
     "test": "turbo test:unit",
-    "test:ci": "cross-env NODE_OPTIONS=\"--test-reporter=lcov --test-reporter-destination=lcov.info --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=@reporters/github --test-reporter-destination=stdout\" turbo test:unit",
+    "test:ci": "cross-env NODE_OPTIONS=\"--test-reporter=lcov --test-reporter-destination=lcov.info --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout\" turbo test:unit",
     "cloudflare:preview": "turbo run cloudflare:preview",
     "cloudflare:deploy": "turbo run cloudflare:deploy"
   },
diff --git a/packages/mdx/eslint.config.js b/packages/mdx/eslint.config.js
new file mode 100644
index 0000000000000..2a6304344fb89
--- /dev/null
+++ b/packages/mdx/eslint.config.js
@@ -0,0 +1,16 @@
+import importX from 'eslint-plugin-import-x';
+import tseslint from 'typescript-eslint';
+
+import baseConfig from '../../eslint.config.js';
+
+export default [
+  ...baseConfig,
+  ...tseslint.configs.recommended,
+  importX.flatConfigs.typescript,
+  {
+    rules: {
+      '@typescript-eslint/array-type': ['error', { default: 'generic' }],
+      '@typescript-eslint/consistent-type-imports': 'error',
+    },
+  },
+];
diff --git a/packages/mdx/lib/__tests__/shiki.test.mjs b/packages/mdx/lib/__tests__/shiki.test.mjs
new file mode 100644
index 0000000000000..c31fd7112fc80
--- /dev/null
+++ b/packages/mdx/lib/__tests__/shiki.test.mjs
@@ -0,0 +1,201 @@
+import assert from 'node:assert/strict';
+import { mock, it, describe } from 'node:test';
+
+import { SKIP } from 'unist-util-visit';
+
+mock.module('classnames', {
+  defaultExport: (...args) => args.filter(Boolean).join(' '),
+});
+
+mock.module('hast-util-to-string', {
+  namedExports: {
+    toString: node => node.children?.[0]?.value || '',
+  },
+});
+
+mock.module('unist-util-visit', {
+  namedExports: {
+    visit: (tree, nodeType, visitor) => {
+      if (tree.type === nodeType) visitor(tree, null, null);
+      tree.children?.forEach((child, i) => {
+        if (child.type === nodeType) visitor(child, i, tree);
+        child.children?.forEach((subChild, j) => {
+          if (subChild.type === nodeType) visitor(subChild, j, child);
+        });
+      });
+    },
+    SKIP,
+  },
+});
+
+mock.module('../highlighter', {
+  namedExports: {
+    highlightToHast: (code, language) => ({
+      children: [
+        {
+          type: 'element',
+          tagName: 'pre',
+          properties: { class: `highlighted-${language}` },
+          children: [
+            {
+              type: 'element',
+              tagName: 'code',
+              children: [{ type: 'text', value: `highlighted ${code}` }],
+            },
+          ],
+        },
+      ],
+    }),
+  },
+});
+
+const {
+  getMetaParameter,
+  isCodeBlock,
+  processCodeTabs,
+  processCodeHighlighting,
+} = await import('../shiki');
+
+describe('rehypeShikiji module', () => {
+  describe('Utility functions', () => {
+    it('getMetaParameter extracts values from meta strings', () => {
+      const tests = [
+        {
+          meta: 'displayName="JavaScript"',
+          param: 'displayName',
+          expected: 'JavaScript',
+        },
+        {
+          meta: 'active="true" displayName="TypeScript"',
+          param: 'active',
+          expected: 'true',
+        },
+        {
+          meta: 'key="value with spaces"',
+          param: 'key',
+          expected: 'value with spaces',
+        },
+        { meta: 'noQuotes=value', param: 'noQuotes', expected: undefined },
+        { meta: null, param: 'key', expected: undefined },
+        { meta: 'key="value"', param: undefined, expected: undefined },
+        { meta: 'key=""', param: 'key', expected: undefined },
+        { meta: 'otherKey="value"', param: 'key', expected: undefined },
+      ];
+
+      tests.forEach(({ meta, param, expected }) => {
+        assert.equal(getMetaParameter(meta, param), expected);
+      });
+    });
+
+    it('isCodeBlock correctly identifies code blocks', () => {
+      // Valid code block
+      const validBlock = {
+        type: 'element',
+        tagName: 'pre',
+        children: [
+          {
+            type: 'element',
+            tagName: 'code',
+            children: [{ type: 'text', value: 'code' }],
+          },
+        ],
+      };
+
+      // Invalid cases
+      const invalidCases = [
+        {
+          type: 'element',
+          tagName: 'pre',
+          children: [{ type: 'element', tagName: 'span' }],
+        },
+        {
+          type: 'element',
+          tagName: 'div',
+          children: [{ type: 'element', tagName: 'code' }],
+        },
+        undefined,
+      ];
+
+      assert.ok(isCodeBlock(validBlock));
+      invalidCases.forEach(testCase => assert.ok(!isCodeBlock(testCase)));
+    });
+  });
+
+  describe('Processing functions', () => {
+    it('processCodeTabs groups adjacent code blocks', () => {
+      const tree = {
+        type: 'root',
+        children: [
+          {
+            type: 'element',
+            tagName: 'pre',
+            children: [
+              {
+                type: 'element',
+                tagName: 'code',
+                properties: { className: ['language-javascript'] },
+                data: { meta: 'displayName="JavaScript"' },
+                children: [{ type: 'text', value: 'console.log("hello");' }],
+              },
+            ],
+          },
+          {
+            type: 'element',
+            tagName: 'pre',
+            children: [
+              {
+                type: 'element',
+                tagName: 'code',
+                properties: { className: ['language-typescript'] },
+                data: { meta: 'displayName="TypeScript" active="true"' },
+                children: [{ type: 'text', value: 'console.log("hello");' }],
+              },
+            ],
+          },
+        ],
+      };
+
+      processCodeTabs(tree);
+
+      assert.partialDeepStrictEqual(tree.children[0], {
+        tagName: 'CodeTabs',
+        properties: {
+          languages: 'javascript|typescript',
+          displayNames: 'JavaScript|TypeScript',
+          defaultTab: '1',
+        },
+      });
+    });
+
+    it('processCodeHighlighting applies syntax highlighting', () => {
+      const tree = {
+        type: 'root',
+        children: [
+          {
+            type: 'element',
+            tagName: 'pre',
+            children: [
+              {
+                type: 'element',
+                tagName: 'code',
+                properties: { className: ['language-javascript'] },
+                data: { meta: 'showCopyButton="true"' },
+                children: [{ type: 'text', value: 'console.log("hello");' }],
+              },
+            ],
+          },
+        ],
+      };
+
+      processCodeHighlighting(tree);
+
+      assert.partialDeepStrictEqual(tree.children[0], {
+        tagName: 'pre',
+        properties: {
+          class: 'highlighted-javascript language-javascript',
+          showCopyButton: 'true',
+        },
+      });
+    });
+  });
+});
diff --git a/apps/site/util/__tests__/getLanguageDisplayName.test.mjs b/packages/mdx/lib/__tests__/utils.test.mjs
similarity index 86%
rename from apps/site/util/__tests__/getLanguageDisplayName.test.mjs
rename to packages/mdx/lib/__tests__/utils.test.mjs
index e70c4ecf5bc27..32ba25b9c0348 100644
--- a/apps/site/util/__tests__/getLanguageDisplayName.test.mjs
+++ b/packages/mdx/lib/__tests__/utils.test.mjs
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
 import { it, describe, mock } from 'node:test';
 
 describe('getLanguageDisplayName', async () => {
-  mock.module('#site/shiki.config.mjs', {
+  mock.module('../../shiki.config.mjs', {
     namedExports: {
       LANGUAGES: [
         { name: 'javascript', aliases: ['js'], displayName: 'JavaScript' },
@@ -11,9 +11,7 @@ describe('getLanguageDisplayName', async () => {
     },
   });
 
-  const { getLanguageDisplayName } = await import(
-    '#site/util/getLanguageDisplayName'
-  );
+  const { getLanguageDisplayName } = await import('../utils');
 
   it('should return the display name for a known language', () => {
     assert.equal(getLanguageDisplayName('javascript'), 'JavaScript');
diff --git a/apps/site/next.mdx.compiler.mjs b/packages/mdx/lib/compiler.tsx
similarity index 56%
rename from apps/site/next.mdx.compiler.mjs
rename to packages/mdx/lib/compiler.tsx
index 009c25f067e74..e1bec0a9f7717 100644
--- a/apps/site/next.mdx.compiler.mjs
+++ b/packages/mdx/lib/compiler.tsx
@@ -1,38 +1,50 @@
 'use strict';
 
 import { compile as mdxCompile } from '@mdx-js/mdx';
+import type { Heading } from '@vcarl/remark-headings';
+import type { MDXComponents } from 'mdx/types';
+import type { ReactElement } from 'react';
 import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
+import type { ReadTimeResults } from 'reading-time';
+import type { VFile } from 'vfile';
 import { matter } from 'vfile-matter';
 
-import { createSval } from './next.jsx.compiler.mjs';
-import { REHYPE_PLUGINS, REMARK_PLUGINS } from './next.mdx.plugins.mjs';
-import { createGitHubSlugger } from './util/gitHubUtils';
+import createSval from './evaluator';
+import { REHYPE_PLUGINS, REMARK_PLUGINS } from './plugins';
+import { createGitHubSlugger } from './utils';
 
 // Defines a JSX Fragment and JSX Runtime for the MDX Compiler
 export const reactRuntime = { Fragment, jsx, jsxs };
 
+type CompiledMDX = {
+  content: ReactElement;
+  headings: Array;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  frontmatter: Record;
+  readingTime: ReadTimeResults;
+};
+
+type MDXVFileData = Omit & {
+  matter: Record;
+};
+
 /**
  * This is our custom simple MDX Compiler that is used to compile Markdown and MDX
  * this returns a serializable VFile as a string that then gets passed to our MDX Provider
  *
- * @param {import('vfile').VFile} source The source Markdown/MDX content
- * @param {'md' | 'mdx'} fileExtension If it should use the MDX or a plain Markdown parser/compiler
- * @param {import('mdx/types').MDXComponents} components The MDX Components to be used in the MDX Provider
- * @param {Record} props Extra optional React props for the MDX Provider
+ * @param source The source Markdown/MDX content
+ * @param fileExtension If it should use the MDX or a plain Markdown parser/compiler
+ * @param components The MDX Components to be used in the MDX Provider
+ * @param props Extra optional React props for the MDX Provider
  *
- * @returns {Promise<{
- *   content: import('react').ReactElement;
- *   headings: Array;
- *   frontmatter: Record;
- *   readingTime: import('reading-time').ReadTimeResults;
- * }>}
+ * @returns Promise resolving to compiled MDX content and metadata
  */
-export async function compile(
-  source,
-  fileExtension,
-  components = {},
-  props = {}
-) {
+export default async function compile(
+  source: VFile,
+  fileExtension: 'md' | 'mdx',
+  components: MDXComponents = {},
+  props: Record = {}
+): Promise {
   // Parses the Frontmatter to the VFile and removes from the original source
   // cleaning the frontmatter to the source that is going to be parsed by the MDX Compiler
   matter(source, { strip: true });
@@ -63,9 +75,13 @@ export async function compile(
 
   // Retrieve some parsed data from the VFile metadata
   // such as frontmatter and Markdown headings
-  const { headings = [], matter: frontmatter, readingTime } = source.data;
+  const {
+    headings = [],
+    matter: frontmatter,
+    readingTime,
+  } = (source.data || {}) as MDXVFileData;
 
-  headings.forEach(heading => {
+  headings.forEach((heading: Heading) => {
     // we re-sluggify the links to match the GitHub slugger
     // since some also do not come with sluggifed links
     heading.data = { ...heading.data, id: slugger(heading.value) };
diff --git a/packages/mdx/lib/evaluator.ts b/packages/mdx/lib/evaluator.ts
new file mode 100644
index 0000000000000..0d33e174474ec
--- /dev/null
+++ b/packages/mdx/lib/evaluator.ts
@@ -0,0 +1,26 @@
+'use strict';
+
+import Sval from 'sval';
+
+/**
+ * Creates a JavaScript Evaluator
+ *
+ * @param dependencies All sort of dependencies to be passed to the JavaScript context
+ * @param mode The mode of the JavaScript execution
+ *
+ * @returns Returns an Sandboxed instance of a JavaScript interpreter
+ */
+export default function createSval(
+  dependencies: Record = {},
+  mode: 'module' | 'script' = 'module'
+): Sval {
+  const svalInterpreter = new Sval({
+    ecmaVer: 'latest',
+    sandBox: true,
+    sourceType: mode,
+  });
+
+  svalInterpreter.import(dependencies);
+
+  return svalInterpreter;
+}
diff --git a/packages/mdx/lib/highlighter.ts b/packages/mdx/lib/highlighter.ts
new file mode 100644
index 0000000000000..5fbf44774834a
--- /dev/null
+++ b/packages/mdx/lib/highlighter.ts
@@ -0,0 +1,54 @@
+import {
+  type CodeToHastOptions,
+  createHighlighterCoreSync,
+} from '@shikijs/core';
+import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
+
+import { LANGUAGES, DEFAULT_THEME } from '../shiki.config';
+
+// Create a memoized minimal Shiki Syntax Highlighter
+export const shiki = createHighlighterCoreSync({
+  themes: [DEFAULT_THEME],
+  langs: LANGUAGES,
+  engine: createJavaScriptRegexEngine(),
+});
+
+/**
+ * Highlight code to HTML and extract the inner content from code tags
+ * @param code - Source code to highlight
+ * @param language - Programming language for syntax highlighting
+ * @param options - Additional Shiki options
+ * @returns HTML string with syntax highlighting
+ */
+export const highlightToHtml = (
+  code: string,
+  language: string,
+  options: Partial> = {}
+): string => {
+  const html = shiki.codeToHtml(code, {
+    lang: language,
+    theme: DEFAULT_THEME,
+    ...options,
+  });
+
+  const match = html.match(/]*>([\s\S]*?)<\/code>/);
+  return match?.[1] || html;
+};
+
+/**
+ * Convert code to HAST with syntax highlighting
+ * @param code - Source code to highlight
+ * @param language - Programming language for syntax highlighting
+ * @param options - Additional Shiki options
+ * @returns HAST (Hypertext Abstract Syntax Tree) representation
+ */
+export const highlightToHast = (
+  code: string,
+  language: string,
+  options: Partial> = {}
+) =>
+  shiki.codeToHast(code, {
+    lang: language,
+    theme: DEFAULT_THEME,
+    ...options,
+  });
diff --git a/apps/site/next.mdx.plugins.mjs b/packages/mdx/lib/plugins.ts
similarity index 83%
rename from apps/site/next.mdx.plugins.mjs
rename to packages/mdx/lib/plugins.ts
index 51a76ef0f7bcf..b5f098669bd85 100644
--- a/apps/site/next.mdx.plugins.mjs
+++ b/packages/mdx/lib/plugins.ts
@@ -5,13 +5,14 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
 import rehypeSlug from 'rehype-slug';
 import remarkGfm from 'remark-gfm';
 import readingTime from 'remark-reading-time';
+import type { PluggableList } from 'unified';
 
-import rehypeShikiji from './next.mdx.shiki.mjs';
+import rehypeShikiji from './shiki';
 
 /**
  * Provides all our Rehype Plugins that are used within MDX
  */
-export const REHYPE_PLUGINS = [
+export const REHYPE_PLUGINS: PluggableList = [
   // Generates `id` attributes for headings (H1, ...)
   rehypeSlug,
   // Automatically add anchor links to headings (H1, ...)
@@ -24,7 +25,7 @@ export const REHYPE_PLUGINS = [
 /**
  * Provides all our Remark Plugins that are used within MDX
  */
-export const REMARK_PLUGINS = [
+export const REMARK_PLUGINS: PluggableList = [
   // Support GFM syntax to be used within Markdown
   remarkGfm,
   // Generates metadata regarding headings
diff --git a/packages/mdx/lib/shiki.ts b/packages/mdx/lib/shiki.ts
new file mode 100644
index 0000000000000..c29a07015fd6d
--- /dev/null
+++ b/packages/mdx/lib/shiki.ts
@@ -0,0 +1,182 @@
+'use strict';
+
+import classNames from 'classnames';
+import type { Element } from 'hast';
+import { toString } from 'hast-util-to-string';
+import type { Node } from 'unist';
+import { SKIP, visit } from 'unist-util-visit';
+
+import { highlightToHast } from './highlighter';
+
+// Constants
+const LANGUAGE_PREFIX = 'language-';
+const CODE_TAG = 'code';
+const PRE_TAG = 'pre';
+
+type Metadata = undefined | { meta: string };
+
+/**
+ * Extracts a parameter value from a meta string
+ */
+export function getMetaParameter(
+  data: Metadata,
+  key?: string
+): string | undefined {
+  if (!data?.meta || !key) return undefined;
+
+  const match = data?.meta.match(new RegExp(`${key}="([^"]*)"`));
+  const value = match?.[1];
+
+  return value?.length ? value : undefined;
+}
+
+/**
+ * Checks if a node is a code block (
...
) + */ +export function isCodeBlock(node?: Element): boolean { + if (!node || node.tagName !== PRE_TAG) return false; + + const codeElement = node.children[0] as Element | undefined; + return Boolean(codeElement?.tagName === CODE_TAG); +} + +/** + * Process code tabs (adjacent code blocks) + */ +export function processCodeTabs(tree: Node): void { + visit( + tree, + 'element', + (node: Element, index: number | null, parent: Element) => { + if (index === null || !parent) return; + + // Skip if the current element isn't a code block + if (!isCodeBlock(node)) return; + + const languages: Array = []; + const displayNames: Array = []; + const codeTabsChildren: Array = []; + + let defaultTab = '0'; + let currentIndex = index; + + // Collect consecutive code blocks + while (isCodeBlock(parent.children[currentIndex] as Element)) { + const preElement = parent.children[currentIndex] as Element; + const codeElement = preElement.children[0] as Element; + + // Extract meta information + const displayName = + getMetaParameter( + codeElement.data as Metadata, + 'displayName' + )?.replaceAll('|', '') || ''; + displayNames.push(displayName); + + // Extract language from class name + const classNameList = codeElement.properties?.className as + | Array + | undefined; + const langClass = classNameList?.find(c => + c.startsWith(LANGUAGE_PREFIX) + ); + languages.push( + langClass ? langClass.slice(LANGUAGE_PREFIX.length) : 'text' + ); + + // Store the code block + codeTabsChildren.push(preElement); + + // Check if this tab should be the default active one + if ( + getMetaParameter(codeElement.data as Metadata, 'active') === 'true' + ) { + defaultTab = String(codeTabsChildren.length - 1); + } + + // Move to next node + const nextNode = parent.children[currentIndex + 1]; + currentIndex += nextNode?.type === 'text' ? 2 : 1; + } + + // If we found multiple code blocks, group them into a CodeTabs component + if (codeTabsChildren.length >= 2) { + const codeTabElement: Element = { + type: 'element', + tagName: 'CodeTabs', + children: codeTabsChildren, + properties: { + languages: languages.join('|'), + displayNames: displayNames.join('|'), + defaultTab, + }, + }; + + // Replace the original code blocks with our new CodeTabs element + parent.children.splice(index, currentIndex - index, codeTabElement); + + // Skip processing the children - we've already handled them + return [SKIP]; + } + } + ); +} + +/** + * Apply syntax highlighting to code blocks + */ +export function processCodeHighlighting(tree: Node): void { + visit(tree, 'element', (node: Element, index, parent: Element) => { + if (!parent || index == null || node.tagName !== PRE_TAG) return; + + const codeElement = node.children[0] as Element; + if (!codeElement?.properties || codeElement.tagName !== CODE_TAG) return; + + // Get language class + const className = codeElement.properties.className as + | Array + | undefined; + if (!Array.isArray(className) || className.length === 0) return; + + const langClass = className.find( + c => typeof c === 'string' && c.startsWith(LANGUAGE_PREFIX) + ); + if (!langClass) return; + + // Get code content and language + const code = toString(codeElement); + const language = langClass.slice(LANGUAGE_PREFIX.length); + + // Generate highlighted HTML + const { children } = highlightToHast(code, language); + const highlightedCode = children[0] as Element; + + // Preserve language class + highlightedCode.properties!.class = classNames( + highlightedCode.properties!.class, + langClass + ); + + // Add copy button if specified + const showCopyButton = getMetaParameter( + codeElement.data as Metadata, + 'showCopyButton' + ); + if (showCopyButton === 'true' || showCopyButton === 'false') { + highlightedCode.properties!.showCopyButton = showCopyButton; + } + + // Replace the original pre element with the highlighted one + parent.children.splice(index, 1, ...(children as Array)); + }); +} + +/** + * Rehype plugin for code highlighting and code tabs + */ +export default function rehypeShikiji() { + return function transformer(tree: Node): void { + processCodeTabs(tree); + processCodeHighlighting(tree); + }; +} diff --git a/apps/site/util/getLanguageDisplayName.ts b/packages/mdx/lib/utils.ts similarity index 58% rename from apps/site/util/getLanguageDisplayName.ts rename to packages/mdx/lib/utils.ts index a62081bd93c4d..15d95c0d519b2 100644 --- a/apps/site/util/getLanguageDisplayName.ts +++ b/packages/mdx/lib/utils.ts @@ -1,4 +1,12 @@ -import { LANGUAGES } from '#site/shiki.config.mjs'; +import GithubSlugger from 'github-slugger'; + +import { LANGUAGES } from '../shiki.config'; + +export const createGitHubSlugger = () => { + const githubSlugger = new GithubSlugger(); + + return (text: string) => githubSlugger.slug(text); +}; export const getLanguageDisplayName = (language: string): string => { const languageByIdOrAlias = LANGUAGES.find( diff --git a/packages/mdx/package.json b/packages/mdx/package.json new file mode 100644 index 0000000000000..ae245728e9642 --- /dev/null +++ b/packages/mdx/package.json @@ -0,0 +1,52 @@ +{ + "name": "@node-core/mdx", + "type": "module", + "exports": { + "./shiki.config.mjs": "./shiki.config.mjs", + "./*": [ + "./lib/*", + "./lib/*.ts", + "./lib/*.tsx" + ] + }, + "scripts": { + "check-types": "tsc --noEmit", + "lint:js": "eslint \"**/*.{js,mjs,ts}\"", + "lint": "pnpm lint:js", + "lint:fix": "pnpm lint --fix", + "test": "turbo test:unit", + "test:unit": "cross-env NODE_NO_WARNINGS=1 node --experimental-test-coverage --test-coverage-exclude=**/*.test.* --experimental-test-module-mocks --enable-source-maps --import=tsx --test **/*.test.*", + "test:unit:watch": "cross-env NODE_OPTIONS=\"--watch\" pnpm test:unit" + }, + "devDependencies": { + "@types/hast": "^3.0.4", + "@types/mdx": "^2.0.13", + "@types/react": "^19.1.0", + "@types/unist": "^3.0.3", + "@vcarl/remark-headings": "~0.1.0", + "eslint-plugin-import-x": "~4.11.0", + "reading-time": "~1.5.0", + "tsx": "^4.19.4", + "typescript": "~5.8.3", + "typescript-eslint": "~8.31.1", + "unified": "^11.0.5", + "vfile": "~6.0.3" + }, + "dependencies": { + "@mdx-js/mdx": "^3.1.0", + "@shikijs/core": "^3.3.0", + "@shikijs/engine-javascript": "^3.3.0", + "classnames": "~2.5.1", + "github-slugger": "~2.0.0", + "hast-util-to-string": "~3.0.1", + "react": "^19.1.0", + "rehype-autolink-headings": "~7.1.0", + "rehype-slug": "~6.0.0", + "remark-gfm": "~4.0.1", + "remark-reading-time": "~2.0.1", + "shiki": "~3.3.0", + "sval": "^0.6.7", + "unist-util-visit": "~5.0.0", + "vfile-matter": "~5.0.1" + } +} diff --git a/apps/site/shiki.config.mjs b/packages/mdx/shiki.config.ts similarity index 76% rename from apps/site/shiki.config.mjs rename to packages/mdx/shiki.config.ts index f940a9311d686..bc434e8826460 100644 --- a/apps/site/shiki.config.mjs +++ b/packages/mdx/shiki.config.ts @@ -1,5 +1,4 @@ -'use strict'; - +import type { LanguageRegistration, ThemeRegistration } from 'shiki'; import diffLanguage from 'shiki/langs/diff.mjs'; import dockerLanguage from 'shiki/langs/docker.mjs'; import iniLanguage from 'shiki/langs/ini.mjs'; @@ -14,15 +13,13 @@ import shikiNordTheme from 'shiki/themes/nord.mjs'; /** * All languages needed within the Node.js website for syntax highlighting. - * - * @type {Array} */ -export const LANGUAGES = [ +export const LANGUAGES: Array = [ { ...javaScriptLanguage[0], - // We path the JavaScript language to include the CommonJS and ES Module aliases + // We patch the JavaScript language to include the CommonJS and ES Module aliases // that are commonly used (non-standard aliases) within our API docs and Blog posts - aliases: javaScriptLanguage[0].aliases.concat('cjs', 'mjs'), + aliases: javaScriptLanguage[0].aliases!.concat('cjs', 'mjs'), }, ...iniLanguage, ...jsonLanguage, @@ -36,8 +33,8 @@ export const LANGUAGES = [ ]; // This is the default theme we use for our Shiki Syntax Highlighter -export const DEFAULT_THEME = { - // We updating this color because the background color and comment text color +export const DEFAULT_THEME: ThemeRegistration = { + // We update this color because the background color and comment text color // in the Codebox component do not comply with accessibility standards // @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html colorReplacements: { '#616e88': '#707e99' }, diff --git a/packages/mdx/tsconfig.json b/packages/mdx/tsconfig.json new file mode 100644 index 0000000000000..e6aed110022ef --- /dev/null +++ b/packages/mdx/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": "." + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/mdx/turbo.json b/packages/mdx/turbo.json new file mode 100644 index 0000000000000..e7e8ea79f3e69 --- /dev/null +++ b/packages/mdx/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "lint:js": { + "inputs": ["lib/**/*.{js,mjs,ts}"] + }, + "test:unit": { + "inputs": ["**/*.{ts,tsx,mjs}"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bef89954622a..c744fa26924e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,9 @@ importers: '@heroicons/react': specifier: ~2.2.0 version: 2.2.0(react@19.1.0) - '@mdx-js/mdx': - specifier: ^3.1.0 - version: 3.1.0(acorn@8.14.1) + '@node-core/mdx': + specifier: workspace:* + version: link:../../packages/mdx '@node-core/ui-components': specifier: workspace:* version: link:../../packages/ui-components @@ -90,12 +90,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 @@ -126,18 +120,12 @@ importers: feed: specifier: ~4.2.2 version: 4.2.2 - github-slugger: - specifier: ~2.0.0 - version: 2.0.0 glob: specifier: ~11.0.1 version: 11.0.2 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) @@ -165,39 +153,15 @@ importers: reading-time: specifier: ~1.5.0 version: 1.5.0 - rehype-autolink-headings: - specifier: ~7.1.0 - version: 7.1.0 - rehype-slug: - specifier: ~6.0.0 - version: 6.0.0 - remark-gfm: - specifier: ~4.0.1 - version: 4.0.1 - remark-reading-time: - specifier: ~2.0.1 - version: 2.0.1 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 - vfile-matter: - specifier: ~5.0.1 - version: 5.0.1 devDependencies: '@eslint/compat': specifier: ~1.2.8 @@ -235,6 +199,9 @@ importers: eslint-plugin-react-hooks: specifier: 5.2.0 version: 5.2.0(eslint@9.26.0(jiti@2.4.2)) + github-slugger: + specifier: ~2.0.0 + version: 2.0.0 global-jsdom: specifier: ^26.0.0 version: 26.0.0(jsdom@26.1.0) @@ -317,6 +284,91 @@ importers: specifier: ~8.31.1 version: 8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + packages/mdx: + dependencies: + '@mdx-js/mdx': + specifier: ^3.1.0 + version: 3.1.0(acorn@8.14.1) + '@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 + github-slugger: + specifier: ~2.0.0 + version: 2.0.0 + hast-util-to-string: + specifier: ~3.0.1 + version: 3.0.1 + react: + specifier: ^19.1.0 + version: 19.1.0 + rehype-autolink-headings: + specifier: ~7.1.0 + version: 7.1.0 + rehype-slug: + specifier: ~6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ~4.0.1 + version: 4.0.1 + remark-reading-time: + specifier: ~2.0.1 + version: 2.0.1 + shiki: + specifier: ~3.3.0 + version: 3.3.0 + sval: + specifier: ^0.6.7 + version: 0.6.7 + unist-util-visit: + specifier: ~5.0.0 + version: 5.0.0 + vfile-matter: + specifier: ~5.0.1 + version: 5.0.1 + devDependencies: + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 + '@types/mdx': + specifier: ^2.0.13 + version: 2.0.13 + '@types/react': + specifier: ^19.1.0 + version: 19.1.2 + '@types/unist': + specifier: ^3.0.3 + version: 3.0.3 + '@vcarl/remark-headings': + specifier: ~0.1.0 + version: 0.1.0 + eslint-plugin-import-x: + specifier: ~4.11.0 + version: 4.11.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + reading-time: + specifier: ~1.5.0 + version: 1.5.0 + tsx: + specifier: ^4.19.4 + version: 4.19.4 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ~8.31.1 + version: 8.31.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + unified: + specifier: ^11.0.5 + version: 11.0.5 + vfile: + specifier: ~6.0.3 + version: 6.0.3 + packages/ui-components: dependencies: '@heroicons/react': @@ -12774,7 +12826,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 +12854,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 +12894,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 +12912,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