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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tagged-jsx/transform

v1.0.2

Published

Transform between tagged template literals and JSX syntax

Readme

@tagged-jsx/transform

Bidirectional conversion between tagged template literals and JSX syntax, with character-level source mapping for diagnostic translation.

Designed for SolidJS-ecosystem libraries that use the html tagged template tag (e.g., solid-styled-components, @solid-primitives/styled). Convert tagged template syntax to standard JSX and back, while preserving SolidJS's reactive expression semantics.

Convert this:

const el = html`<div class=${active ? "on" : "off"}>
  <span>${content}</span>
</div>`;

To this:

const el = <div class={active ? "on" : "off"}>
  <span>{content}</span>
</div>;

And back again. Used by the VS Code extension, TypeScript plugin, and formatter to provide seamless editing across both syntaxes.

Installation

npm install @tagged-jsx/transform

Usage

Basic conversion

import { createJsxTransformer, createTaggedTransformer } from "@tagged-jsx/transform";
import ts from "typescript";

// Tagged template -> JSX
const toJSX = createJsxTransformer(["html", "jsx"], ts);
const jsxResult = toJSX.toJsx(`
  const el = html\`<div class=\${active}>hello</div>\`;
`);
// Result: const el = <div class={active}>hello</div>

// JSX -> Tagged template
const toTagged = createTaggedTransformer("html", ts);
const taggedResult = toTagged.toTagged(`
  const el = <div class={active}>hello</div>;
`);
// Result: const el = html`<div class=${active}>hello</div>`

// SolidJS components are wrapped in expressions:
const taggedResult2 = toTagged.toTagged(`
  const el = <MyComponent prop={value}>{children}</MyComponent>;
`);
// Result: const el = html`<${MyComponent} prop=${value}>${children}</${MyComponent}>`

With expression callbacks (SolidJS reactive expressions)

SolidJS evaluates interpolations eagerly in JSX but some template libraries use lazy evaluation. Callbacks bridge this gap by wrapping expressions with () => when converting to tagged templates, and unwrapping on the reverse path.

import {
  createJsxTransformer,
  createTaggedTransformer,
  createExpressionTransformCallbacks,
} from "@tagged-jsx/transform";
import ts from "typescript";

const callbacks = createExpressionTransformCallbacks(ts);

const toJSX = createJsxTransformer(["html", "jsx"], ts, callbacks);
const toTagged = createTaggedTransformer("html", ts, callbacks);

// JSX (eager):
//   <div class={signal()}>
//   <Show when={condition}>

// Tagged template (lazy, with callbacks):
//   html`<div class=${() => signal()}>
//   html`<${Show} when=${() => condition}>`

// The callbacks ensure non-primitive expressions get () => wrapped
// for lazy evaluation, while primitives, arrow functions, and
// event handlers (on*) pass through unchanged.

With source mappings (for diagnostic translation)

const { toJsxWithMappings } = createJsxTransformer(["jsx", "html"], ts);
const { code, mappings } = toJsxWithMappings(sourceCode);

// Map a position in the tagged source to the equivalent position in JSX
const jsxPos = getJsxPosition(taggedPos, mappings.mappings, code.length);

// Map a JSX diagnostic position back to the original tagged source
const taggedPos = getTaggedPosition(jsxPos, mappings.reverseMappings, sourceCode.length);

API

createJsxTransformer(tags, ts, callbacks?)

Creates a transformer that converts tagged template literals to JSX syntax.

Parameters:

  • tags — Array of tag names to recognize (e.g., ["jsx", "html"])
  • ts — A TypeScript module instance
  • callbacks? — Optional TransformerCallbacks for custom expression handling

Returns: { toJsx, toJsxWithMappings }

toJsx(code) — Converts all tagged templates in code matching the configured tags into JSX syntax. Processes templates iteratively (up to 100 per file), handling nested cases.

toJsxWithMappings(code) — Same as toJsx but also returns character-level source mappings.

createTaggedTransformer(tag, ts, callbacks?)

Creates a transformer that converts JSX syntax to tagged template literals.

Parameters:

  • tag — The tag name to use in output templates (e.g., "html")
  • ts — A TypeScript module instance
  • callbacks? — Optional TransformerCallbacks for custom expression handling

Returns: { toTagged, toTaggedWithMappings }

toTagged(code, callbacks?) — Converts all JSX elements in code to tagged template literals wrapping them in the configured tag. Accepts an optional per-invocation callbacks override. Component names (starting with uppercase) are wrapped in expressions: <MyComponent />html`<${MyComponent} />`. This follows SolidJS conventions where the html tag expects component names as interpolations.

toTaggedWithMappings(code, callbacks?) — Same as toTagged but also returns character-level source mappings.

getJsxPosition(taggedPosition, mappings, jsxCodeLength)

Given a character position in the original tagged template source, find the corresponding position in the JSX output using the diff-based mapping.

getTaggedPosition(jsxPosition, reverseMappings, taggedCodeLength)

The inverse of getJsxPosition. Given a JSX position, find the corresponding tagged template position. Used by the TypeScript plugin to map diagnostic locations back to the original source.

computeMappings(oldCode, newCode)

Computes forward and reverse character-level mappings between two strings using diff.


Transformer internals

createJsxTransformer algorithm

  1. Find the first TaggedTemplateExpression in the source whose tag matches the configured tags (depth-first traversal)
  2. Extract the template strings array and expression nodes from the TypeScript AST
  3. Tokenize the strings using @tagged-jsx/parse's tokenize()
  4. Parse the tokens into an AST using parse()
  5. Attach whitespace metadata from template string segments to the AST via WeakMaps (preserves original spacing around attributes)
  6. Render the AST back to JSX text, calling callbacks.toJSX() for each expression encountered
  7. Replace the original tagged template text in the source
  8. Repeat until no more tagged templates remain

createTaggedTransformer algorithm

  1. Find the first JSX element or fragment (JsxElement, JsxSelfClosingElement, JsxFragment; depth-first traversal)
  2. Convert attributes preserving whitespace between tag name and attributes, and between attributes:
    • JSXAttribute with string initializer: class="foo"
    • JSXAttribute with expression initializer: class=${expr} (with optional callback transformation)
    • JSXAttribute without initializer (boolean): disabled
    • JSXSpreadAttribute: ...${obj}
  3. Convert children recursively, preserving whitespace between opening tags/first children, between children, and between last children/closing tags
  4. Wrap the result in the configured tag: tag`...`
  5. Replace the JSX text in the source
  6. Repeat until no more JSX elements remain

Callback system

The TransformerCallbacks interface allows custom handling of expressions during conversion. This is essential for frameworks like SolidJS that use different evaluation models for tagged templates vs. JSX.

interface TransformerCallbacks {
  toTagged?: (opts: ToTaggedCallbackOptions) => string;
  toJSX?: (opts: ToJsxCallbackOptions) => string;
}

ToTaggedCallbackOptions (JSX → Tagged direction)

| Field | Type | Description | |-------|------|-------------| | expression | ts.Expression | The TypeScript expression AST node | | propName? | string | The attribute name (only set for attributes, not children) | | propType | "attribute" | "child" | Whether this is an attribute value or a child expression | | sourceCode | string | The full source code of the file being transformed |

ToJsxCallbackOptions (Tagged → JSX direction)

| Field | Type | Description | |-------|------|-------------| | expression | ts.Expression | The TypeScript expression AST node | | propName? | string | The attribute name (only set for attributes) | | propType | "attribute" | "child" | Whether attribute value or child expression | | templateNode | ExpressionProp \| ExpressionNode | The parsed template AST node (from @tagged-jsx/parse) with token-level metadata | | sourceCode | string | The full source code of the file being transformed |

Default callbacks: createExpressionTransformCallbacks(ts)

The built-in callbacks handle the () => wrap/unwrap pattern for idempotent round-tripping between JSX and tagged templates.

toTagged callback logic:

  1. Skip props: ref and on* (event handlers) are returned verbatim
  2. Primitives: String/number literals, true, false, null, undefined are returned verbatim
  3. Arrow functions: Returned verbatim (already carry parameter context)
  4. Everything else: Wrapped in () => <expr> to convert eager evaluation to lazy thunks

toJSX callback logic:

  1. Skip props: Same ref/on* skip logic
  2. Primitives: Returned verbatim
  3. Arrow functions with parameters or block body: Returned verbatim (cannot safely unwrap)
  4. Zero-parameter arrow functions with expression body: () => prefix is stripped (reversing the wrapping)

This produces a clean round-trip suitable for SolidJS reactive expressions:

signal()  --toTagged--> () => signal()  --toJSX--> signal()

Real-world SolidJS example:

// Source (JSX):
<button class={baseClass} onClick={handler} disabled={isDisabled()}>
  <Show when={loaded()}><span>{text()}</span></Show>
</button>

// After toTagged with callbacks:
html`<button class=${() => baseClass} onClick=${handler} disabled=${() => isDisabled()}>
  <${Show} when=${() => loaded()}><span>${() => text()}</span></${Show}>
</button>`

// Going back through toJSX with callbacks:
<button class={baseClass} onClick={handler} disabled={isDisabled()}>
  <Show when={loaded()}><span>{text()}</span></Show>
</button>

Note how onClick and primitives pass through untouched — only non-trivial expressions get wrapped.


---

## Source mapping system

The mapping module provides character-level offset tracking between tagged template and JSX representations, enabling tools to translate positions for error reporting, completions, and cursor synchronization.

Tagged source: html<div>${name}</div> ^-- position 14 | v (mapped position) JSX output: {name} ^-- position 6


This is used by the TypeScript plugin to map diagnostic positions from the JSX output back to the original tagged template source, so error squiggles appear at the correct locations in your `html` template literals.

The diff-based approach uses `diffChars` to compute fine-grained character differences, building both forward (tagged→JSX) and reverse (JSX→tagged) mappings.

---

## Whitespace model

The JSX-to-tagged direction preserves whitespace by extracting source text between known positions (e.g., between tag name and first attribute, between children). The tagged-to-JSX direction attaches whitespace metadata from the template string segments to the parsed AST using WeakMaps, separate from the parse package's interfaces.

This ensures that formatting like spacing around attributes, blank lines between elements, and indentation inside fragments is preserved through conversions.

## License

MIT