# JavaScript / TypeScript (/usage/javascript)

Use the same `@lumis-sh/lumis` package in JavaScript and TypeScript projects.
Its exports include TypeScript declarations; both use the same runtime on each host.

Each language you highlight is its own package, and the examples below assume
it is installed:

```bash
npm install @lumis-sh/lumis @lumis-sh/wasm-javascript
```

To highlight TypeScript source, install `@lumis-sh/wasm-typescript` or
`@lumis-sh/wasm-tsx`. The language you highlight is independent of whether your
app uses JavaScript or TypeScript. [Where parsers come from](#where-parsers-come-from)
has the details.


## For one-off highlighting

Use [`highlight()`](https://www.npmjs.com/package/@lumis-sh/lumis) when you just need one async highlight call and do not need to keep a highlighter instance around.

```ts
import { highlight } from "@lumis-sh/lumis";
import { htmlInline } from "@lumis-sh/lumis/formatters";
import javascript from "@lumis-sh/wasm-javascript";
import frappe from "@lumis-sh/themes/catppuccin_frappe";

const html: string = await highlight(
  "const x = 1",
  htmlInline({ language: javascript, theme: frappe }),
);
```

`highlight()` uses a shared default runtime, so loaded languages and global WASM resolver changes are process-wide.

For a theme outside the bundled collection, follow
[Generate a theme from a Neovim colorscheme](/recipes/generate-theme-from-neovim-colorscheme)
and import the resulting JSON as the formatter's `theme` object.

## For reusable highlighters

Use [`createHighlighter()`](https://www.npmjs.com/package/@lumis-sh/lumis) when you want instance isolation, explicit setup, lazy loading, or repeated synchronous `hl.highlight()` calls after initialization.

```ts
import { createHighlighter } from "@lumis-sh/lumis";
import { htmlInline } from "@lumis-sh/lumis/formatters";
import javascript from "@lumis-sh/wasm-javascript";
import frappe from "@lumis-sh/themes/catppuccin_frappe";

const hl = await createHighlighter({
  languages: [javascript],
});

const html = hl.highlight(
  "const x = 1",
  htmlInline({ language: javascript, theme: frappe }),
);
```

This creates an isolated highlighter unless you intentionally rely on the global resolver.

## Bundles

A bundle package depends on a set of language packages and registers all of
them lazily.

| Bundle                            | Purpose                                                                                     |
| --------------------------------- | ------------------------------------------------------------------------------------------- |
| `@lumis-sh/wasm-bundle-web`       | HTML, CSS, JS, TS, JSON, Markdown, SQL, Svelte, Vue, Astro, and other web-focused languages |
| `@lumis-sh/wasm-bundle-web-extra` | framework and template languages that complement the web bundle                             |
| `@lumis-sh/wasm-bundle-system`    | C, C++, Rust, Go, Zig, LLVM, Make, CMake, and related system languages                      |
| `@lumis-sh/wasm-bundle-backend`   | popular backend languages plus common API, config, and infra formats                        |
| `@lumis-sh/wasm-bundle-full`      | every language but the largest less common parsers, capped at 64 MB of Wasm memory          |

```ts
import { createHighlighter } from "@lumis-sh/lumis";
import web from "@lumis-sh/wasm-bundle-web";

const hl = await createHighlighter({
  languages: [web],
});

// registered, including lazy entries
hl.registeredLanguages;

// loaded right now, usually just plaintext at first
hl.languages;
```

## Lazy loading

You can register a language now and load it only when needed.

```ts
import { createHighlighter } from "@lumis-sh/lumis";
import { htmlInline } from "@lumis-sh/lumis/formatters";
import frappe from "@lumis-sh/themes/catppuccin_frappe";
import web from "@lumis-sh/wasm-bundle-web";

const hl = await createHighlighter({
  languages: [web],
});

await hl.loadLanguage(web.javascript);

const html = hl.highlight(
  "const x = 1",
  htmlInline({
    language: web.javascript,
    theme: frappe,
  }),
);
```

`createHighlighter({ languages })` accepts:

- eager `Language` objects
- a bundle object
- dynamic import promises
- lazy import functions

## Repeated highlighting

Load languages once with `createHighlighter()`, then call `hl.highlight()` synchronously for repeated work.

```ts
import { createHighlighter } from "@lumis-sh/lumis";
import { htmlInline } from "@lumis-sh/lumis/formatters";
import javascript from "@lumis-sh/wasm-javascript";
import frappe from "@lumis-sh/themes/catppuccin_frappe";

const hl = await createHighlighter({
  languages: [javascript],
});

const html = hl.highlight(
  "const x = 1",
  htmlInline({ language: javascript, theme: frappe }),
);
```

## Language references

These are equivalent at highlight time:

```ts
htmlInline({ language: json, theme });
htmlInline({ language: web.json, theme });
htmlInline({ language: "json", theme });
```

## Auto-detection and plaintext fallback

If you omit `language`, Lumis tries to detect it and falls back to plaintext when it can't.

Detection methods:

- file extensions (`.rs`, `.js`, `.ex`)
- filenames (`Makefile`, `Dockerfile`)
- shebangs (`#!/usr/bin/env python`)
- Emacs mode lines

```ts
const html = await highlight(
  source,
  htmlInline({ theme: frappe }),
);
```

Useful when rendering user-provided content or pasted snippets.

## Metadata APIs

Language helpers live in `@lumis-sh/lumis/languages`, and theme helpers live in
`@lumis-sh/lumis/themes`.

Useful for dropdowns, config validation, and docs tooling:

- [`languages.available()`](https://github.com/leandrocp/lumis/blob/main/packages/javascript/lumis/src/languages.ts)
- [`languages.get()`](https://github.com/leandrocp/lumis/blob/main/packages/javascript/lumis/src/languages.ts) — metadata by name, alias, extension, or path; empty input returns plaintext, unknown input returns `undefined`
- [`languages.loaded()`](https://github.com/leandrocp/lumis/blob/main/packages/javascript/lumis/src/languages.ts) — ids ready to highlight without a download
- `themes.available()`, `themes.get(name)`, and `themes.closest(colors)` from `@lumis-sh/lumis/themes`
- `sanitizeThemeName()` from `@lumis-sh/lumis/formatters/html`

## Runtime notes

- every runtime loads the same per-language parser WASM; what differs is the engine that runs it
- on Node, `@lumis-sh/lumis` runs those parsers and the highlighting walk under Wasmtime; built-in formatters normally render there too, while custom formatters assemble events in JavaScript and an async call that may need a JavaScript resolver stays on the main thread
- browsers, and any platform with no prebuilt addon, run them under the host's own WebAssembly through `web-tree-sitter`, and walk and format in JavaScript
- the addon is prebuilt for macOS arm64 and x64, Linux arm64 and x64 against both glibc and musl, and Windows arm64 and x64; npm installs the one matching the host, so Alpine and other musl images get the same runtime as a glibc host
- `runtimeKind()` returns `'native'` or `'wasm'` if you need to confirm which one an install resolved
- the addon carries no parsers and loads each from the `@lumis-sh/wasm-*` package the project installed, so it costs about 4.7 MB of download on top of the Wasm runtime rather than one binary per language
- the package uses conditional exports so Node and web-standard runtimes get the right entry point automatically
- every language package contains matching queries plus an exact parser version and SHA-256 digest; parser bytes a resolver downloads are checked against that digest
- Node keeps compiled parser modules, and anything a custom resolver fetched, in the platform user data directory; `LUMIS_DATA_DIR` overrides it
- browsers load only the parser packages you import, and fetch nothing unless you configure a resolver; parser bytes a resolver fetched are kept in CacheStorage, with an IndexedDB fallback
- Node loads installed parser packages such as `@lumis-sh/wasm-elixir` or bundle packages such as `@lumis-sh/wasm-bundle-web`, and nothing else unless you configure a resolver
- ordinary Node highlighters share one Wasmtime runtime; a highlighter that supplies parser bytes, queries, or resolver callbacks gets an isolated Tree-sitter Wasm store so its definitions are released with that highlighter, while the Wasmtime engine and persistent caches remain shared
- Bun and Deno are also supported
- edge and worker hosts such as Supabase Edge Functions, Cloudflare Workers and Vercel are supported too; see [Edge / Workers](/usage/edge)

## Where parsers come from

Every language is a package, named after its parser: `@lumis-sh/wasm-rust`
holds the Rust parser, the queries written for it, and the parser's SHA-256
digest. Install it and import the language it exports. The same code
runs in Node and in a browser:

```bash
npm install @lumis-sh/lumis @lumis-sh/wasm-rust
```

```ts
import { createHighlighter } from "@lumis-sh/lumis";
import rust from "@lumis-sh/wasm-rust";

const hl = await createHighlighter({ languages: [rust] });
```

In Node, Lumis reads the parser out of `node_modules`, next to its `lumis.json`.
In a browser, your bundler ships it with your code, and Lumis checks the bytes
against the package's digest when it fetches them. Either way nothing comes
from a CDN, and a parser runs with the queries it was released with.

A bundle package exports a whole set, and loads each language the first time
it is used:

```ts
import web from "@lumis-sh/wasm-bundle-web";

const hl = await createHighlighter({ languages: [web] });
```

One parser can serve more than one language, so a few packages export more
than one, by name:

```ts
import markdown, { mdx } from "@lumis-sh/wasm-markdown";
import { ejs, erb } from "@lumis-sh/wasm-embedded-template";
```

A grammar split in two comes along on its own: importing `markdown` also loads
`markdown_inline`.

A language whose package the project didn't install or import is refused, and
so is a package too old for this Lumis or outside the supported version range;
update it. [Declaring parsers](/advanced/lock) explains why. To fetch parsers
at run time instead, [configure a resolver](#custom-resolvers).

In Node, `@lumis-sh/lumis/langs/rust`, `@lumis-sh/lumis/bundles/web`, and a
plain `"rust"` work too: they name the package, and Node reads the installed
one. A browser has no `node_modules` to read, so it needs the import.

Node searches from the language and bundle packages passed to
`createHighlighter`, including their parser dependencies, with the working
directory as a fallback. An imported bundle therefore works even when a CLI or
editor runs from another directory. These packages remain available to later
highlighters in the same process. Update older `@lumis-sh/wasm-bundle-*`
packages if their injected languages still depend on the working directory.

A string ID or a `@lumis-sh/lumis/langs/*` handle alone has no installation
location. If the parsers are installed elsewhere, pass the imported language
or bundle instead.

Without a bundler, import the package from jsDelivr, which serves the parser
file next to the module:

```ts
import rust from "https://cdn.jsdelivr.net/npm/@lumis-sh/wasm-rust/+esm";
```

Vite 8 needs no configuration. The dev server of Vite 7 and older, VitePress 1
included, pre-bundles a package away from its parser file, so leave the parser
packages you import out of pre-bundling. The languages a bundle loads and the
grammars a language requires come along without being listed:

```ts title="vite.config.ts"
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    exclude: ["@lumis-sh/wasm-rust", "@lumis-sh/wasm-bundle-web"],
  },
});
```

## Custom resolvers

A resolver tells Lumis where to find package metadata and parser files, such as
a CDN or a directory you serve. It is the only way Lumis fetches a parser at
run time, and configuring one is a declaration of its own: it answers for every
language you didn't import, ahead of installed packages on Node.

```ts title="Every highlighter"
import * as languages from "@lumis-sh/lumis/languages";

languages.configurePackageResolver(
  (packageName, versionRange) =>
    `https://unpkg.com/${packageName}@${versionRange}/lumis.json`,
);

languages.configureWasmResolver(
  (_language, wasm) =>
    `https://unpkg.com/${wasm.packageName}@${wasm.version}/${wasm.name}.wasm`,
);
```

```ts title="One highlighter"
import { createHighlighter } from "@lumis-sh/lumis";
import rust from "@lumis-sh/lumis/langs/rust";

const hl = await createHighlighter({
  languages: [rust],
  languagePackageResolver: (packageName) =>
    `/wasm/${encodeURIComponent(packageName)}/lumis.json`,
  wasmResolver: (_language, wasm) => `/wasm/${wasm.name}.wasm`,
});
```

Whatever a resolver returns is still checked against the package's digest. On
Node, resolvers also answer for languages found inside a document; the next
section covers what that walk can read.

## Injected languages

On Node, highlighting loads a language injected inside a document during the
same pass that finds it, so a Markdown file with a fenced Rust block highlights
that block without Rust being named anywhere in your code. It loads from the
installed package, so install the languages a document can inject too. One that
isn't installed leaves its own block plain rather than failing the document, and
Lumis warns once naming it.

Configured [`languages.configurePackageResolver()` and
`languages.configureWasmResolver()`](#custom-resolvers) callbacks
participate in that same Node native path. They apply to a language first
discovered mid-document as well as to the root language, without a preload or
second pass.

The native walk has to read a newly discovered resolver result synchronously.
Local paths plus `file:`, `data:`, `http:`, and `https:` URLs work there. A URL
owned by JavaScript itself, such as a `blob:` URL, cannot be opened from Rust;
include that language in `createHighlighter({languages: [...]})` or call
`loadLanguage()` first so JavaScript resolves it before the synchronous walk.

In a browser, a parser cannot be downloaded in the middle of highlighting, so
import and load every participating language yourself:

```ts
import css from "@lumis-sh/wasm-css";
import html from "@lumis-sh/wasm-html";
import javascript from "@lumis-sh/wasm-javascript";

const hl = await createHighlighter({
  languages: [html, css, javascript],
});

const htmlOutput = hl.highlight(
  source,
  htmlInline({ language: html, theme: frappe }),
);
```

Languages from a bundle package such as `@lumis-sh/wasm-bundle-web` are
registered but not loaded. Call `hl.loadLanguage()` for each one the document
uses before highlighting it.

A language only has to be loaded once per page. A highlighter without
`wasmResolver` or `languagePackageResolver` of its own also highlights an
injected block in any language another highlighter on the page loaded from its
package, so a page that loads `rust` for one highlighter gets Rust fences in a
Markdown-only highlighter as well. One that sets a resolver uses only what it
loaded itself, and so does every highlighter once a global resolver is set with
`languages.configureWasmResolver()` or `languages.configureLanguagePackageResolver()`.

See [On demand](/languages#on-demand).

## Preload languages during application startup

Highlighting loads on demand, so preloading only decides *when* a language is
paid for. See [Languages](/languages).

Start serving, then load in the background:

```ts title="bootstrap.ts"
import * as languages from "@lumis-sh/lumis/languages";

await startServer();

// Not awaited, so a slow compile delays no request. The
// `.catch()` is required: an unhandled rejection
// terminates the process.
languages.load(["javascript", "html", "css"]).catch(
  (error) => {
    logger.warn(
      { error },
      "Lumis preload failed; languages load on demand",
    );
  },
);
```

`languages.load()` warms the same default runtime `highlight()` uses, so nothing
has to be threaded through the application. `createHighlighter({languages})` is
the same load into an instance you keep yourself.

Awaiting a preload before `startServer()` puts every compile on the boot path,
so the process accepts no request until the slowest one finishes.

The API accepts `bundle-*` names such as `bundle-web`, covering the same
languages as the matching `@lumis-sh/wasm-bundle-*` package. Lumis loads a
parser as your package manager installed it. A parser downloaded through a
resolver is checked against its package's SHA-256 first.

When the native Node addon is available, the API also validates each parser and
its queries and persists the compiled Wasmtime module in the data directory.
The portable fallback can't: `web-tree-sitter` does not persist its compilation
across processes.

To compile in an image build or a prestart step instead, run the same
`languages.load()` there and await it, with `LUMIS_DATA_DIR` pointing at the
directory the server will use. The server then reuses those compiled modules.
Being a separate process is the point: failing there fails the build rather than
a request. [Languages](/languages#preloaded) has the command, and why
`lumis languages download` is not the build step for a Node project.