diff --git a/apps/site/mdx/components.mjs b/apps/site/mdx/components.mjs index 74fc4074e2e6f..0159538c123b3 100644 --- a/apps/site/mdx/components.mjs +++ b/apps/site/mdx/components.mjs @@ -3,6 +3,11 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import Blockquote from '@node-core/ui-components/Common/Blockquote'; import MDXCodeTabs from '@node-core/ui-components/MDX/CodeTabs'; +import { + MDXTooltip, + MDXTooltipContent, + MDXTooltipTrigger, +} from '@node-core/ui-components/MDX/Tooltip'; import Button from '#site/components/Common/Button'; import LinkWithArrow from '#site/components/Common/LinkWithArrow'; @@ -48,6 +53,10 @@ export default { img: MDXImage, // Renders MDX CodeTabs CodeTabs: MDXCodeTabs, + // Renders Tooltips + MDXTooltip, + MDXTooltipContent, + MDXTooltipTrigger, // Renders a Download Button DownloadButton, // Renders a stateless Release Select Component diff --git a/apps/site/mdx/plugins.mjs b/apps/site/mdx/plugins.mjs index b985d40d1655a..f6b33cd5416ae 100644 --- a/apps/site/mdx/plugins.mjs +++ b/apps/site/mdx/plugins.mjs @@ -9,6 +9,12 @@ import readingTime from 'remark-reading-time'; import remarkTableTitles from '../util/table'; +// TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment +// variable for detection instead of current method, which will enable better +// tree-shaking. +// Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615 +const OPEN_NEXT_CLOUDFLARE = 'Cloudflare' in global; + /** * Provides all our Rehype Plugins that are used within MDX */ @@ -19,7 +25,21 @@ export const rehypePlugins = [ [rehypeAutolinkHeadings, { behavior: 'wrap' }], // Transforms sequential code elements into code tabs and // adds our syntax highlighter (Shikiji) to Codeboxes - rehypeShikiji, + [ + rehypeShikiji, + { + // We use the faster WASM engine on the server instead of the web-optimized version. + // + // Currently we fall back to the JavaScript RegEx engine + // on Cloudflare workers because `shiki/wasm` requires loading via + // `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support + // for security reasons. + wasm: !OPEN_NEXT_CLOUDFLARE, + + // TODO(@avivkeller): Find a way to enable Twoslash w/ a VFS on Cloudflare + twoslash: !OPEN_NEXT_CLOUDFLARE, + }, + ], ]; /** diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 7157b5711d69e..7277ef9c39301 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -51,6 +51,16 @@ const nextConfig = { }, ], }, + serverExternalPackages: ['twoslash'], + outputFileTracingIncludes: { + // Twoslash needs TypeScript declarations to function, and, by default, + // Next.js strips them for brevity. Therefore, they must be explicitly + // included. + '/*': [ + '../../node_modules/.pnpm/typescript@*/node_modules/typescript/lib/*.d.ts', + './node_modules/@types/node/**/*', + ], + }, // On static export builds we want the output directory to be "build" distDir: ENABLE_STATIC_EXPORT ? 'build' : undefined, // On static export builds we want to enable the export feature diff --git a/apps/site/package.json b/apps/site/package.json index 1c33850e433c4..54e6e9ba29e6a 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -74,6 +74,7 @@ "semver": "~7.7.2", "sval": "^0.6.3", "tailwindcss": "catalog:", + "twoslash": "^0.3.4", "unist-util-visit": "^5.0.0", "vfile": "~6.0.3", "vfile-matter": "~5.0.1" diff --git a/apps/site/pages/en/learn/typescript/introduction.md b/apps/site/pages/en/learn/typescript/introduction.md index 4c9739e8efaa9..d11d29ebed829 100644 --- a/apps/site/pages/en/learn/typescript/introduction.md +++ b/apps/site/pages/en/learn/typescript/introduction.md @@ -70,16 +70,15 @@ npm add --save-dev @types/node These type definitions allow TypeScript to understand Node.js APIs and provide proper type checking and autocompletion when you use functions like `fs.readFile` or `http.createServer`. For example: -```js -import * as fs from 'fs'; - -fs.readFile('example.txt', 'foo', (err, data) => { - // ^^^ Argument of type '"foo"' is not assignable to parameter of type … - if (err) { - throw err; - } - console.log(data); -}); + +```ts +// @noErrors +/* eslint-disable */ +// ---cut--- +import fs from 'fs'; + +fs.read +// ^| ``` Many popular JavaScript libraries have their type definitions available under the `@types` namespace, maintained by the DefinitelyTyped community. This enables seamless integration of existing JavaScript libraries with TypeScript projects. diff --git a/apps/site/pages/en/learn/typescript/publishing-a-ts-package.md b/apps/site/pages/en/learn/typescript/publishing-a-ts-package.md index e19372c4085c4..435c0428716b7 100644 --- a/apps/site/pages/en/learn/typescript/publishing-a-ts-package.md +++ b/apps/site/pages/en/learn/typescript/publishing-a-ts-package.md @@ -126,9 +126,9 @@ A note about directory organisation: There are a few common practices for placin The purpose of types is to warn an implementation will not work: ```ts +// @errors: 2322 const foo = 'a'; const bar: number = 1 + foo; -// ^^^ Type 'string' is not assignable to type 'number'. ``` TypeScript has warned that the above code will not behave as intended, just like a unit test warns that code does not behave as intended. They are complementary and verify different things—you should have both. diff --git a/apps/site/pages/en/learn/typescript/transpile.md b/apps/site/pages/en/learn/typescript/transpile.md index 0d97d01e0cad2..655df98569708 100644 --- a/apps/site/pages/en/learn/typescript/transpile.md +++ b/apps/site/pages/en/learn/typescript/transpile.md @@ -71,6 +71,7 @@ If you have type errors in your TypeScript code, the TypeScript compiler will ca We will modify our code like this, to voluntarily introduce a type error: ```ts +// @errors: 2322 2554 type User = { name: string; age: number; @@ -88,31 +89,4 @@ const justine: User = { const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!"); ``` -And this is what TypeScript has to say about this: - -```console -example.ts:12:5 - error TS2322: Type 'string' is not assignable to type 'number'. - -12 age: 'Secret!', - ~~~ - - example.ts:3:5 - 3 age: number; - ~~~ - The expected type comes from property 'age' which is declared here on type 'User' - -example.ts:15:7 - error TS2322: Type 'boolean' is not assignable to type 'string'. - -15 const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!"); - ~~~~~~~~~~~~~~~~ - -example.ts:15:51 - error TS2554: Expected 1 arguments, but got 2. - -15 const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!"); - ~~~~~~~~~~~~~~~~~~~~~~ - - -Found 3 errors in the same file, starting at: example.ts:12 -``` - As you can see, TypeScript is very helpful in catching bugs before they even happen. This is one of the reasons why TypeScript is so popular among developers. diff --git a/apps/site/styles/index.css b/apps/site/styles/index.css index 841bd69a2830c..3152e8f4b9fb6 100644 --- a/apps/site/styles/index.css +++ b/apps/site/styles/index.css @@ -7,4 +7,5 @@ */ @import '@node-core/ui-components/styles/index.css'; +@import '@node-core/rehype-shiki/index.css'; @import './locales.css'; diff --git a/packages/rehype-shiki/package.json b/packages/rehype-shiki/package.json index 2c06ad740d81a..32da0bab6761a 100644 --- a/packages/rehype-shiki/package.json +++ b/packages/rehype-shiki/package.json @@ -1,9 +1,10 @@ { "name": "@node-core/rehype-shiki", - "version": "1.1.0", + "version": "1.2.0", "type": "module", "exports": { ".": "./src/index.mjs", + "./index.css": "./src/index.css", "./*": "./src/*.mjs" }, "repository": { @@ -23,6 +24,7 @@ "@shikijs/core": "^3.12.0", "@shikijs/engine-javascript": "^3.12.0", "@shikijs/engine-oniguruma": "^3.12.0", + "@shikijs/twoslash": "^3.12.2", "classnames": "catalog:", "hast-util-to-string": "^3.0.1", "shiki": "~3.12.0", diff --git a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs index 7ab0fcaa6e1b0..b128b7c68a498 100644 --- a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs +++ b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs @@ -20,33 +20,7 @@ mock.module('shiki/themes/nord.mjs', { }); describe('createHighlighter', async () => { - const { createHighlighter } = await import('../highlighter.mjs'); - - describe('getLanguageDisplayName', () => { - it('returns display name for known languages', () => { - const langs = [ - { name: 'javascript', displayName: 'JavaScript', aliases: ['js'] }, - ]; - const highlighter = createHighlighter({ langs }); - - assert.strictEqual( - highlighter.getLanguageDisplayName('javascript'), - 'JavaScript' - ); - assert.strictEqual( - highlighter.getLanguageDisplayName('js'), - 'JavaScript' - ); - }); - - it('returns original name for unknown languages', () => { - const highlighter = createHighlighter({ langs: [] }); - assert.strictEqual( - highlighter.getLanguageDisplayName('unknown'), - 'unknown' - ); - }); - }); + const { default: createHighlighter } = await import('../highlighter.mjs'); describe('highlightToHtml', () => { it('extracts inner HTML from code tag', () => { diff --git a/packages/rehype-shiki/src/__tests__/plugin.test.mjs b/packages/rehype-shiki/src/__tests__/plugin.test.mjs index 11f545ad28ba3..17fe2def9e6a3 100644 --- a/packages/rehype-shiki/src/__tests__/plugin.test.mjs +++ b/packages/rehype-shiki/src/__tests__/plugin.test.mjs @@ -3,7 +3,7 @@ import { describe, it, mock } from 'node:test'; // Simplified mocks - only mock what's actually needed mock.module('../index.mjs', { - namedExports: { highlightToHast: mock.fn(() => ({ children: [] })) }, + defaultExport: () => ({ highlightToHast: mock.fn(() => ({ children: [] })) }), }); mock.module('classnames', { @@ -23,13 +23,13 @@ describe('rehypeShikiji', async () => { const { default: rehypeShikiji } = await import('../plugin.mjs'); const mockTree = { type: 'root', children: [] }; - it('calls visit twice', () => { + it('calls visit twice', async () => { mockVisit.mock.resetCalls(); - rehypeShikiji()(mockTree); + await rehypeShikiji()(mockTree); assert.strictEqual(mockVisit.mock.calls.length, 2); }); - it('creates CodeTabs for multiple code blocks', () => { + it('creates CodeTabs for multiple code blocks', async () => { const parent = { children: [ { @@ -61,7 +61,7 @@ describe('rehypeShikiji', async () => { } }); - rehypeShikiji()(mockTree); + await rehypeShikiji()(mockTree); assert.ok(parent.children.some(child => child.tagName === 'CodeTabs')); }); }); diff --git a/packages/rehype-shiki/src/highlighter.mjs b/packages/rehype-shiki/src/highlighter.mjs index 41263a35911b3..709fa0578b131 100644 --- a/packages/rehype-shiki/src/highlighter.mjs +++ b/packages/rehype-shiki/src/highlighter.mjs @@ -3,45 +3,47 @@ import shikiNordTheme from 'shiki/themes/nord.mjs'; const DEFAULT_THEME = { // We are updating 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 + // in the Codebox component do not comply with accessibility standards. + // See: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html colorReplacements: { '#616e88': '#707e99' }, ...shikiNordTheme, }; +export const getLanguageByName = (language, langs) => + langs.find( + ({ name, aliases }) => + name.toLowerCase() === language.toLowerCase() || + (aliases !== undefined && aliases.includes(language.toLowerCase())) + ); + /** - * Creates a syntax highlighter with utility functions - * @param {import('@shikijs/core').HighlighterCoreOptions} options - Configuration options for the highlighter + * Factory function to create a syntax highlighter instance with utility methods. + * + * @param {Object} params - Parameters for highlighter creation. + * @param {import('@shikijs/core').HighlighterCoreOptions} [params.coreOptions] - Core options for the highlighter. + * @param {import('@shikijs/core').CodeToHastOptions} [params.highlighterOptions] - Additional options for highlighting. */ -export const createHighlighter = options => { - const shiki = createHighlighterCoreSync({ +const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => { + const options = { themes: [DEFAULT_THEME], - ...options, - }); + ...coreOptions, + }; - const theme = options.themes?.[0] ?? DEFAULT_THEME; - const langs = options.langs ?? []; + const shiki = createHighlighterCoreSync(options); - const getLanguageDisplayName = language => { - const languageByIdOrAlias = langs.find( - ({ name, aliases }) => - name.toLowerCase() === language.toLowerCase() || - (aliases !== undefined && aliases.includes(language.toLowerCase())) - ); - - return languageByIdOrAlias?.displayName ?? language; - }; + const theme = options.themes[0]; /** * Highlights code and returns the inner HTML inside the tag * * @param {string} code - The code to highlight * @param {string} lang - The programming language to use for highlighting + * @param {Record} meta - Metadata * @returns {string} The inner HTML of the highlighted code */ - const highlightToHtml = (code, lang) => + const highlightToHtml = (code, lang, meta = {}) => shiki - .codeToHtml(code, { lang, theme }) + .codeToHtml(code, { lang, theme, meta, ...highlighterOptions }) // 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
@@ -52,14 +54,16 @@ export const createHighlighter = options => {
    *
    * @param {string} code - The code to highlight
    * @param {string} lang - The programming language to use for highlighting
+   * @param {Record} meta - Metadata
    */
-  const highlightToHast = (code, lang) =>
-    shiki.codeToHast(code, { lang, theme });
+  const highlightToHast = (code, lang, meta = {}) =>
+    shiki.codeToHast(code, { lang, theme, meta, ...highlighterOptions });
 
   return {
     shiki,
-    getLanguageDisplayName,
     highlightToHtml,
     highlightToHast,
   };
 };
+
+export default createHighlighter;
diff --git a/packages/rehype-shiki/src/index.css b/packages/rehype-shiki/src/index.css
new file mode 100644
index 0000000000000..61a75fb5ed25f
--- /dev/null
+++ b/packages/rehype-shiki/src/index.css
@@ -0,0 +1 @@
+@import './transformers/twoslash/index.css';
diff --git a/packages/rehype-shiki/src/index.mjs b/packages/rehype-shiki/src/index.mjs
index cffd71ea4ce88..70dcab09069fa 100644
--- a/packages/rehype-shiki/src/index.mjs
+++ b/packages/rehype-shiki/src/index.mjs
@@ -1,5 +1,4 @@
-import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
-import { createOnigurumaEngine } from '@shikijs/engine-oniguruma';
+// Keep all imports at the top
 import cLanguage from 'shiki/langs/c.mjs';
 import coffeeScriptLanguage from 'shiki/langs/coffeescript.mjs';
 import cPlusPlusLanguage from 'shiki/langs/cpp.mjs';
@@ -15,46 +14,85 @@ import shellSessionLanguage from 'shiki/langs/shellsession.mjs';
 import typeScriptLanguage from 'shiki/langs/typescript.mjs';
 import yamlLanguage from 'shiki/langs/yaml.mjs';
 
-import { createHighlighter } from './highlighter.mjs';
+import createHighlighter, { getLanguageByName } from './highlighter.mjs';
 
-const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } =
+/**
+ * @typedef {Object} HighlighterOptions
+ * @property {boolean|Object} [wasm=false] - WebAssembly options for the regex engine
+ * @property {boolean|import('@shikijs/twoslash').TransformerTwoslashIndexOptions} [twoslash=false] - Twoslash configuration options
+ * @param {import('@shikijs/core').HighlighterCoreOptions} [coreOptions] - Core options for the highlighter.
+ * @param {import('@shikijs/core').CodeToHastOptions} [highlighterOptions] - Additional options for highlighting.
+ */
+
+/**
+ * Creates the appropriate regex engine based on configuration
+ * @param {HighlighterOptions} options - Configuration options
+ */
+async function getEngine({ wasm = false }) {
+  if (wasm) {
+    const { createOnigurumaEngine } = await import('@shikijs/engine-oniguruma');
+    return createOnigurumaEngine(
+      typeof wasm === 'boolean' ? await import('shiki/wasm') : wasm
+    );
+  }
+
+  const { createJavaScriptRegexEngine } = await import(
+    '@shikijs/engine-javascript'
+  );
+  return createJavaScriptRegexEngine();
+}
+
+/**
+ * Configures and returns transformers based on options
+ * @param {HighlighterOptions} options - Configuration options
+ */
+async function getTransformers({ twoslash: options = false }) {
+  const transformers = [];
+
+  if (options) {
+    const { twoslash } = await import('./transformers/twoslash/index.mjs');
+    transformers.push(twoslash(options));
+  }
+
+  return transformers;
+}
+
+export const LANGS = [
+  ...cLanguage,
+  ...coffeeScriptLanguage,
+  ...cPlusPlusLanguage,
+  ...diffLanguage,
+  ...dockerLanguage,
+  ...httpLanguage,
+  ...iniLanguage,
+  {
+    ...javaScriptLanguage[0],
+    aliases: javaScriptLanguage[0].aliases.concat('cjs', 'mjs'),
+  },
+  ...jsonLanguage,
+  ...powershellLanguage,
+  ...shellScriptLanguage,
+  ...shellSessionLanguage,
+  ...typeScriptLanguage,
+  ...yamlLanguage,
+];
+
+export const getLanguageDisplayName = language =>
+  getLanguageByName(language, LANGS)?.displayName ?? language;
+
+/**
+ * Creates and configures a syntax highlighter
+ * @param {HighlighterOptions} options - Configuration options
+ */
+export default async (options = {}) =>
   createHighlighter({
-    // We use the faster WASM engine on the server instead of the web-optimized version.
-    //
-    // Currently we fall back to the JavaScript RegEx engine
-    // on Cloudflare workers because `shiki/wasm` requires loading via
-    // `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support
-    // for security reasons.
-    //
-    // TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment
-    // variable for detection instead of current method, which will enable better
-    // tree-shaking.
-    // Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615
-    engine:
-      'Cloudflare' in globalThis
-        ? createJavaScriptRegexEngine()
-        : await createOnigurumaEngine(import('shiki/wasm')),
-    langs: [
-      ...cLanguage,
-      ...coffeeScriptLanguage,
-      ...cPlusPlusLanguage,
-      ...diffLanguage,
-      ...dockerLanguage,
-      ...httpLanguage,
-      ...iniLanguage,
-      {
-        ...javaScriptLanguage[0],
-        // 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'),
-      },
-      ...jsonLanguage,
-      ...powershellLanguage,
-      ...shellScriptLanguage,
-      ...shellSessionLanguage,
-      ...typeScriptLanguage,
-      ...yamlLanguage,
-    ],
+    coreOptions: {
+      ...options.coreOptions,
+      langs: LANGS,
+      engine: await getEngine(options),
+    },
+    highlighterOptions: {
+      ...options.highlighterOptions,
+      transformers: await getTransformers(options),
+    },
   });
-
-export { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml };
diff --git a/packages/rehype-shiki/src/minimal.mjs b/packages/rehype-shiki/src/minimal.mjs
index 5fde25e944a30..611ef44951053 100644
--- a/packages/rehype-shiki/src/minimal.mjs
+++ b/packages/rehype-shiki/src/minimal.mjs
@@ -2,14 +2,18 @@ import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
 import powershellLanguage from 'shiki/langs/powershell.mjs';
 import shellScriptLanguage from 'shiki/langs/shellscript.mjs';
 
-import { createHighlighter } from './highlighter.mjs';
+import createHighlighter, { getLanguageByName } from './highlighter.mjs';
 
-const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } =
-  createHighlighter({
+export const LANGS = [...powershellLanguage, ...shellScriptLanguage];
+
+export const getLanguageDisplayName = language =>
+  getLanguageByName(language, LANGS)?.displayName ?? language;
+
+export const { shiki, highlightToHast, highlightToHtml } = createHighlighter({
+  coreOptions: {
     // For the minimal (web) Shiki, we want to use the simpler,
     // JavaScript based engine.
     engine: createJavaScriptRegexEngine(),
     langs: [...powershellLanguage, ...shellScriptLanguage],
-  });
-
-export { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml };
+  },
+});
diff --git a/packages/rehype-shiki/src/plugin.mjs b/packages/rehype-shiki/src/plugin.mjs
index c07416a4db326..2bc93a327153b 100644
--- a/packages/rehype-shiki/src/plugin.mjs
+++ b/packages/rehype-shiki/src/plugin.mjs
@@ -4,34 +4,34 @@ import classNames from 'classnames';
 import { toString } from 'hast-util-to-string';
 import { SKIP, visit } from 'unist-util-visit';
 
-import { highlightToHast } from './index.mjs';
+import createHighlighter from './index.mjs';
 
 // This is what Remark will use as prefix within a 
 className
 // to attribute the current language of the 
 element
 const languagePrefix = 'language-';
 
+// The regex to match metadata
+const rMeta = /(\w+)(?:=(?:"([^"]+)"|(\S+)))?/g;
+
 /**
- * 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.
+ * Parses a fenced code block metadata string into a JavaScript object.
+ * @param {string} meta - The metadata string from a Markdown code fence.
+ * @returns {Record} An object representing the metadata.
  */
-function getMetaParameter(meta, key) {
-  if (typeof meta !== 'string') {
-    return;
+function parseMeta(meta) {
+  const obj = { __raw: meta };
+
+  if (!meta) {
+    return obj;
   }
 
-  const matches = meta.match(new RegExp(`${key}="(?[^"]*)"`));
-  const parameter = matches?.groups.parameter;
+  let match;
 
-  return parameter !== undefined && parameter.length > 0
-    ? parameter
-    : undefined;
+  while ((match = rMeta.exec(meta)) !== null) {
+    obj[match[1]] = match[2] ?? match[3] ?? true;
+  }
+
+  return obj;
 }
 
 /**
@@ -53,8 +53,15 @@ function isCodeBlock(node) {
   );
 }
 
-export default function rehypeShikiji() {
-  return function (tree) {
+/**
+ * @param {import('./index.mjs').HighlighterOptions} options
+ */
+export default function rehypeShikiji(options) {
+  let highlighter;
+
+  return async function (tree) {
+    highlighter ??= await createHighlighter(options);
+
     visit(tree, 'element', (_, index, parent) => {
       const languages = [];
       const displayNames = [];
@@ -65,11 +72,7 @@ export default function rehypeShikiji() {
 
       while (isCodeBlock(parent?.children[currentIndex])) {
         const codeElement = parent?.children[currentIndex].children[0];
-
-        const displayName = getMetaParameter(
-          codeElement.data?.meta,
-          'displayName'
-        );
+        const meta = parseMeta(codeElement.data?.meta);
 
         // We should get the language name from the class name
         if (codeElement.properties.className?.length) {
@@ -80,18 +83,13 @@ export default function rehypeShikiji() {
         }
 
         // Map the display names of each variant for the CodeTab
-        displayNames.push(displayName?.replaceAll('|', '') ?? '');
+        displayNames.push(meta.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') {
+        if (meta.active === 'true') {
           defaultTab = String(codeTabsChildren.length - 1);
         }
 
@@ -162,6 +160,9 @@ export default function rehypeShikiji() {
         return;
       }
 
+      // Get the metadata
+      const meta = parseMeta(preElement.data?.meta);
+
       // Retrieve the whole 
 contents as a parsed DOM string
       const preElementContents = toString(preElement);
 
@@ -169,7 +170,11 @@ export default function rehypeShikiji() {
       const languageId = codeLanguage.slice(languagePrefix.length);
 
       // Parses the 
 contents and returns a HAST tree with the highlighted code
-      const { children } = highlightToHast(preElementContents, languageId);
+      const { children } = highlighter.highlightToHast(
+        preElementContents,
+        languageId,
+        meta
+      );
 
       // Adds the original language back to the 
 element
       children[0].properties.class = classNames(
@@ -177,15 +182,13 @@ export default function rehypeShikiji() {
         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;
+      if (
+        meta.showCopyButton &&
+        ['true', 'false'].includes(meta.showCopyButton)
+      ) {
+        children[0].properties.showCopyButton = meta.showCopyButton;
       }
 
       // Replaces the 
 element with the updated one
diff --git a/packages/rehype-shiki/src/transformers/twoslash/index.css b/packages/rehype-shiki/src/transformers/twoslash/index.css
new file mode 100644
index 0000000000000..92b9502372b46
--- /dev/null
+++ b/packages/rehype-shiki/src/transformers/twoslash/index.css
@@ -0,0 +1,29 @@
+@import '@shikijs/twoslash/style-rich.css';
+
+.twoslash-hover {
+  cursor: text;
+}
+
+.twoslash-popup-code {
+  max-width: 600px;
+  display: block;
+  width: fit-content;
+}
+
+.twoslash-popup-code:not(:has(div)) {
+  min-width: 100%;
+  padding: 6px 12px;
+  white-space: pre-wrap;
+  background-color: var(--color-neutral-950);
+
+  span {
+    display: inline-block;
+  }
+}
+
+.twoslash-completion-list {
+  display: block;
+  width: auto;
+  max-height: 100px;
+  overflow-x: auto;
+}
diff --git a/packages/rehype-shiki/src/transformers/twoslash/index.mjs b/packages/rehype-shiki/src/transformers/twoslash/index.mjs
new file mode 100644
index 0000000000000..0f999dc4ab07f
--- /dev/null
+++ b/packages/rehype-shiki/src/transformers/twoslash/index.mjs
@@ -0,0 +1,48 @@
+import { transformerTwoslash } from '@shikijs/twoslash';
+
+const compose = ({ token, cursor, popup }) => [
+  {
+    type: 'element',
+    tagName: 'MDXTooltipTrigger',
+    children: [token || cursor],
+    properties: { className: ['twoslash-hover'] },
+  },
+  popup,
+];
+
+export const twoslash = options =>
+  transformerTwoslash({
+    langs: ['ts', 'js', 'cjs', 'mjs'],
+    rendererRich: {
+      jsdoc: false,
+      hast: {
+        hoverToken: { tagName: 'MDXTooltip' },
+        hoverPopup: { tagName: 'MDXTooltipContent' },
+        hoverCompose: compose,
+
+        queryToken: { tagName: 'MDXTooltip' },
+        queryPopup: { tagName: 'MDXTooltipContent' },
+        queryCompose: compose,
+
+        errorToken: { tagName: 'MDXTooltip' },
+        errorPopup: { tagName: 'MDXTooltipContent' },
+        errorCompose: compose,
+
+        completionToken: {
+          tagName: 'MDXTooltip',
+          properties: {
+            open: true,
+          },
+        },
+        completionPopup: {
+          tagName: 'MDXTooltipContent',
+          properties: {
+            align: 'start',
+          },
+        },
+        completionCompose: compose,
+      },
+    },
+    throws: false,
+    ...(typeof options === 'object' ? options : {}),
+  });
diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json
index 87f5b3aa90005..756b0265960ef 100644
--- a/packages/ui-components/package.json
+++ b/packages/ui-components/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@node-core/ui-components",
-  "version": "1.2.0",
+  "version": "1.3.0",
   "type": "module",
   "exports": {
     "./*": [
diff --git a/packages/ui-components/src/MDX/Tooltip.tsx b/packages/ui-components/src/MDX/Tooltip.tsx
new file mode 100644
index 0000000000000..dd2a824f1a9ae
--- /dev/null
+++ b/packages/ui-components/src/MDX/Tooltip.tsx
@@ -0,0 +1,33 @@
+import * as TooltipPrimitive from '@radix-ui/react-tooltip';
+import classNames from 'classnames';
+import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
+
+import styles from '../Common/Tooltip/index.module.css';
+
+export const MDXTooltip: FC = ({ children, ...props }) => (
+  
+    {children}
+  
+);
+
+export const MDXTooltipTrigger: FC = ({
+  children,
+  ...props
+}) => (
+  {children}
+);
+
+export const MDXTooltipContent: FC<
+  PropsWithChildren>
+> = ({ children, ...props }) => (
+  
+    
+      {children}
+    
+  
+);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c17f79f6944d3..a0e868ecdfd9f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -204,6 +204,9 @@ importers:
       tailwindcss:
         specifier: 'catalog:'
         version: 4.0.17
+      twoslash:
+        specifier: ^0.3.4
+        version: 0.3.4(typescript@5.8.3)
       unist-util-visit:
         specifier: ^5.0.0
         version: 5.0.0
@@ -268,9 +271,6 @@ importers:
       mdast-util-from-markdown:
         specifier: ^2.0.2
         version: 2.0.2
-      mdast-util-to-string:
-        specifier: ^4.0.0
-        version: 4.0.0
       nock:
         specifier: ^14.0.10
         version: 14.0.10
@@ -322,6 +322,9 @@ importers:
       '@shikijs/engine-oniguruma':
         specifier: ^3.12.0
         version: 3.12.0
+      '@shikijs/twoslash':
+        specifier: ^3.12.2
+        version: 3.13.0(typescript@5.8.3)
       classnames:
         specifier: 'catalog:'
         version: 2.5.1
@@ -2561,6 +2564,9 @@ packages:
   '@shikijs/core@3.12.0':
     resolution: {integrity: sha512-rPfCBd6gHIKBPpf2hKKWn2ISPSrmRKAFi+bYDjvZHpzs3zlksWvEwaF3Z4jnvW+xHxSRef7qDooIJkY0RpA9EA==}
 
+  '@shikijs/core@3.13.0':
+    resolution: {integrity: sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==}
+
   '@shikijs/engine-javascript@1.29.2':
     resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==}
 
@@ -2585,12 +2591,20 @@ packages:
   '@shikijs/themes@3.12.0':
     resolution: {integrity: sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==}
 
+  '@shikijs/twoslash@3.13.0':
+    resolution: {integrity: sha512-OmNKNoZ8Hevt4VKQHfJL+hrsrqLSnW/Nz7RMutuBqXBCIYZWk80HnF9pcXEwRmy9MN0MGRmZCW2rDDP8K7Bxkw==}
+    peerDependencies:
+      typescript: '>=5.5.0'
+
   '@shikijs/types@1.29.2':
     resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==}
 
   '@shikijs/types@3.12.0':
     resolution: {integrity: sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==}
 
+  '@shikijs/types@3.13.0':
+    resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==}
+
   '@shikijs/vscode-textmate@10.0.2':
     resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
 
@@ -3508,6 +3522,11 @@ packages:
     resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@typescript/vfs@1.6.1':
+    resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==}
+    peerDependencies:
+      typescript: '*'
+
   '@ungap/structured-clone@1.3.0':
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
 
@@ -7714,6 +7733,14 @@ packages:
     resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==}
     hasBin: true
 
+  twoslash-protocol@0.3.4:
+    resolution: {integrity: sha512-HHd7lzZNLUvjPzG/IE6js502gEzLC1x7HaO1up/f72d8G8ScWAs9Yfa97igelQRDl5h9tGcdFsRp+lNVre1EeQ==}
+
+  twoslash@0.3.4:
+    resolution: {integrity: sha512-RtJURJlGRxrkJmTcZMjpr7jdYly1rfgpujJr1sBM9ch7SKVht/SjFk23IOAyvwT1NLCk+SJiMrvW4rIAUM2Wug==}
+    peerDependencies:
+      typescript: ^5.5.0
+
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
@@ -10751,6 +10778,13 @@ snapshots:
       '@types/hast': 3.0.4
       hast-util-to-html: 9.0.5
 
+  '@shikijs/core@3.13.0':
+    dependencies:
+      '@shikijs/types': 3.13.0
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.5
+
   '@shikijs/engine-javascript@1.29.2':
     dependencies:
       '@shikijs/types': 1.29.2
@@ -10789,6 +10823,15 @@ snapshots:
     dependencies:
       '@shikijs/types': 3.12.0
 
+  '@shikijs/twoslash@3.13.0(typescript@5.8.3)':
+    dependencies:
+      '@shikijs/core': 3.13.0
+      '@shikijs/types': 3.13.0
+      twoslash: 0.3.4(typescript@5.8.3)
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@shikijs/types@1.29.2':
     dependencies:
       '@shikijs/vscode-textmate': 10.0.2
@@ -10799,6 +10842,11 @@ snapshots:
       '@shikijs/vscode-textmate': 10.0.2
       '@types/hast': 3.0.4
 
+  '@shikijs/types@3.13.0':
+    dependencies:
+      '@shikijs/vscode-textmate': 10.0.2
+      '@types/hast': 3.0.4
+
   '@shikijs/vscode-textmate@10.0.2': {}
 
   '@sindresorhus/is@7.0.2': {}
@@ -12031,6 +12079,13 @@ snapshots:
       '@typescript-eslint/types': 8.42.0
       eslint-visitor-keys: 4.2.1
 
+  '@typescript/vfs@1.6.1(typescript@5.8.3)':
+    dependencies:
+      debug: 4.4.1
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@ungap/structured-clone@1.3.0': {}
 
   '@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -17263,6 +17318,16 @@ snapshots:
       turbo-windows-64: 2.5.6
       turbo-windows-arm64: 2.5.6
 
+  twoslash-protocol@0.3.4: {}
+
+  twoslash@0.3.4(typescript@5.8.3):
+    dependencies:
+      '@typescript/vfs': 1.6.1(typescript@5.8.3)
+      twoslash-protocol: 0.3.4
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
   type-check@0.4.0:
     dependencies:
       prelude-ls: 1.2.1