npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

shiki-twoslash-fix

v1.0.0

Published

A workaround for Shiki's Twoslash transformer that fixes misaligned code annotations and positioning issues

Readme

shiki-twoslash-fix

A workaround for Shiki's Twoslash transformer that fixes misaligned code annotations and positioning issues

npm version npm downloads TypeScript License: MIT CI Publish

The Problem

When using Shiki with Twoslash for syntax highlighting and TypeScript code annotations, you may encounter a bug where code annotations (like @log: tags) appear in the wrong positions. This creates confusing and unprofessional-looking documentation.

Before (Buggy Behavior)

"code (line 1)";
// @log: a tag (line 2)
"code (line 3)";
// @log: a tag (line 4)
"code (line 5)";
// @log: a tag (line 6)
"code (line 7)";
// @log: a tag (line 8)

Renders as:

"code (line 1)";
"code (line 3)";
a tag (line 2)    ← Wrong position!
"code (line 5)";
a tag (line 4)    ← Wrong position!
"code (line 7)";
a tag (line 6)    ← Wrong position!
a tag (line 8)    ← Wrong position!

After (Fixed Behavior)

Renders as:

"code (line 1)";
a tag (line 2)    ← Correct position!
"code (line 3)";
a tag (line 4)    ← Correct position!
"code (line 5)";
a tag (line 6)    ← Correct position!
"code (line 7)";
a tag (line 8)    ← Correct position!

The Solution

This package provides a simple wrapper function that fixes the positioning issues by:

  1. Correcting line number calculations between Twoslash and Shiki
  2. Removing trailing newlines that cause layout problems
  3. Adjusting tag node positions to prevent overflow beyond actual code lines

The fix is non-invasive and works with your existing Shiki/Twoslash setup without breaking changes.

Installation

# npm
npm install shiki-twoslash-fix

# yarn
yarn add shiki-twoslash-fix

# pnpm
pnpm add shiki-twoslash-fix

# bun
bun add shiki-twoslash-fix

Quick Start

import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { codeToHast } from "shiki";

// Create the transformer
const transformer = transformerTwoslash();

// Apply the fix
const fixedTransformer = twoslashBugWorkaround(transformer);

// Use it with Shiki
const hast = await codeToHast(code, {
  lang: "ts",
  theme: "min-dark",
  transformers: [fixedTransformer],
});

That's it! Your code annotations will now appear in the correct positions.

Usage Examples

Basic Usage

import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { codeToHast } from "shiki";

const code = `
const message = "Hello, World!"
// @log: message
console.log(message)
// @log: "Logging to console"
`;

async function highlightCode() {
  const transformer = transformerTwoslash();
  const fixedTransformer = twoslashBugWorkaround(transformer);

  const hast = await codeToHast(code, {
    lang: "typescript",
    theme: "github-dark",
    transformers: [fixedTransformer],
  });

  return hast;
}

With Multiple Transformers

import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { transformerNotationDiff } from "@shikijs/transformers";

const transformers = [
  twoslashBugWorkaround(transformerTwoslash()),
  transformerNotationDiff(),
];

const hast = await codeToHast(code, {
  lang: "ts",
  theme: "vitesse-dark",
  transformers,
});

Custom Transformer Configuration

import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";

const transformer = transformerTwoslash({
  explicitTrigger: true,
  renderer: "rich",
});

const fixedTransformer = twoslashBugWorkaround(transformer);

Technical Details

Root Cause Analysis

The bug occurs due to two main issues in the interaction between Twoslash and Shiki:

1. Line Number Discrepancy

  • Twoslash uses the line where a tag is written as the line number
  • Shiki assumes the line number points to the next line
  • This creates an off-by-one error in positioning

2. Trailing Newline Issue

When code ends with @log: tags, Twoslash creates trailing newlines that:

  • Cause layout problems in the rendered output
  • Lead to incorrect line calculations
  • Result in tags appearing after all code instead of inline

Fix Implementation

The twoslashBugWorkaround() function works by:

  1. Wrapping the preprocess method of the transformer
  2. Removing trailing newlines from the processed code
  3. Adjusting tag node positions using two corrections:
    • Subtract 1 from line numbers (fixes Twoslash/Shiki discrepancy)
    • Cap line numbers at the maximum actual line (prevents overflow)
function adjustTagNodes(nodes, maxLine) {
  for (const node of nodes) {
    if (node.type === "tag") {
      node.line = Math.min(
        node.line - 1, // Fix off-by-one error
        maxLine, // Prevent overflow
      );
    }
  }
}

Edge Cases Handled

  • Empty code blocks: Gracefully handles empty or whitespace-only code
  • No Twoslash metadata: Safely passes through when Twoslash isn't used
  • Multiple trailing newlines: Removes all trailing newlines consistently
  • Mixed content: Works with code that has both regular lines and annotations

API Reference

twoslashBugWorkaround<T>(transformer: T): T

Applies the bug workaround to a Shiki transformer that has Twoslash preprocessing.

Parameters:

  • transformer: T - A Shiki transformer object with a preprocess method

Returns:

  • T - The same transformer object with the bug fix applied

Type Constraints:

  • T extends ShikiTransformer where ShikiTransformer has a preprocess method

Example:

const transformer = transformerTwoslash();
const fixed = twoslashBugWorkaround(transformer);

TypeScript Support

This package is written in TypeScript and provides full type safety:

import type { TwoslashShikiReturn } from "@shikijs/twoslash";

// All types are properly exported and inferred
const fixedTransformer = twoslashBugWorkaround(transformer);
// fixedTransformer maintains the same type as the input transformer

Framework Integration

Astro

// astro.config.mjs
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";

export default defineConfig({
  markdown: {
    shikiConfig: {
      transformers: [twoslashBugWorkaround(transformerTwoslash())],
    },
  },
});

Next.js with MDX

// next.config.js
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";

const nextConfig = {
  pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
  experimental: {
    mdxRs: true,
  },
};

export default withMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [
      [
        rehypeShiki,
        {
          transformers: [twoslashBugWorkaround(transformerTwoslash())],
        },
      ],
    ],
  },
})(nextConfig);

VitePress

// .vitepress/config.ts
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";

export default defineConfig({
  markdown: {
    codeTransformers: [twoslashBugWorkaround(transformerTwoslash())],
  },
});

Docusaurus

// docusaurus.config.js
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";

const config = {
  presets: [
    [
      "classic",
      {
        docs: {
          remarkPlugins: [
            [
              "@docusaurus/remark-plugin-npm2yarn",
              {
                converters: ["yarn", "pnpm"],
              },
            ],
          ],
          rehypePlugins: [
            [
              "rehype-shiki",
              {
                transformers: [twoslashBugWorkaround(transformerTwoslash())],
              },
            ],
          ],
        },
      },
    ],
  ],
};

Compatibility

See the peerDependencies section for required versions of @shikijs/twoslash and twoslash.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

# Clone the repository
git clone https://github.com/suin/shiki-twoslash-fix.git
cd shiki-twoslash-fix

# Install dependencies
bun install

# Run tests
bun test

# Build the package
bun run build

Running Tests

The test suite includes both unit tests and integration tests that verify the fix works correctly:

# Run all tests
bun test

# Run tests in watch mode
bun test --watch

Bug Reports

If you find a bug, please create an issue with:

  • A minimal reproduction case
  • Your environment details (Node.js version, package versions)
  • Expected vs actual behavior

License

MIT © suin