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

@vedivad/codemirror-typst

v0.15.4

Published

CodeMirror 6 extension for Typst diagnostics with incremental compilation

Readme

@vedivad/codemirror-typst

CodeMirror 6 extensions for Typst — syntax highlighting, diagnostics, autocompletion, hover tooltips, formatting, and live preview.

Re-exports everything from @vedivad/typst-web-service, so you only need this one dependency.

Install

npm install @vedivad/codemirror-typst

Prerequisites

  • A bundler with WASM support (e.g. Vite + vite-plugin-wasm)
  • The formatter requires the bundler to handle static WASM imports from @typstyle/typstyle-wasm-bundler
  • The analyzer requires a URL to the tinymist WASM binary (see LSP analysis)

Minimal editor

Syntax highlighting, diagnostics, and compilation — no URLs or config.

import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import {
  createTypstHighlighting,
  createTypstSetup,
  TypstCompiler,
  TypstProject,
} from "@vedivad/codemirror-typst";

const compiler = await TypstCompiler.create();
const project = new TypstProject({ compiler });

const highlighting = await createTypstHighlighting({ theme: "dark" });
const setup = createTypstSetup({
  project,
  sync: "editor-driven",
  highlighting,
});

new EditorView({
  parent: document.querySelector("#app")!,
  state: EditorState.create({
    doc: "= Hello, Typst!",
    extensions: [basicSetup, ...setup],
  }),
});

Full-featured editor

Adds live SVG preview, autocompletion/hover, and format on save.

import {
  createTypstHighlighting,
  createTypstSetup,
  TypstAnalyzer,
  TypstCompiler,
  TypstFormatter,
  TypstProject,
  TypstRenderer,
} from "@vedivad/codemirror-typst";
import tinymistWasmUrl from "tinymist-web/pkg/tinymist_bg.wasm?url";

const [compiler, renderer, formatter, analyzer] = await Promise.all([
  TypstCompiler.create(),
  TypstRenderer.create(),
  TypstFormatter.create({ tab_spaces: 2, max_width: 80 }),
  TypstAnalyzer.create({ wasmUrl: tinymistWasmUrl }),
]);

const project = new TypstProject({
  compiler,
  analyzer,
  autoCompile: { debounceMs: 300, maxWaitMs: 2000 },
});

project.onCompile(async (result) => {
  if (result.vector) {
    const svg = await renderer.renderSvg(result.vector);
    document.querySelector("#preview")!.innerHTML = svg;
  }
});

const highlighting = await createTypstHighlighting({ theme: "dark" });
const setup = createTypstSetup({
  project,
  sync: "editor-driven",
  highlighting,
  formatter: { instance: formatter, formatOnSave: true },
});

Multi-file editor

Attach the typstFilePath facet per-editor so each EditorState carries its own path. Switching tabs with view.setState(states[path]) propagates the new path automatically — no external closure or activeFile variable required.

import { createTypstSetup, typstFilePath } from "@vedivad/codemirror-typst";

const project = new TypstProject({ compiler, analyzer });
await project.setMany({
  "/main.typ": "...",
  "/template.typ": "...",
});

const setup = createTypstSetup({ project, sync: "editor-driven" });
const shared = [basicSetup, ...setup];

const states = Object.fromEntries(
  project.files.map((path) => [
    path,
    EditorState.create({
      doc: project.getText(path) ?? "",
      extensions: [...shared, typstFilePath.of(path)],
    }),
  ]),
);

External sync / Y.js

For collaborative editors, let your shared document model own the text and mirror it into TypstProject. Pass sync: "external" to createTypstSetup so it does not install the editor-to-project sync plugin. Diagnostics, highlighting, analyzer-backed completion/hover, and formatting still work against the project state you provide.

import { EditorState } from "@codemirror/state";
import { EditorView, basicSetup } from "codemirror";
import { syncYTextToTypstProject } from "@vedivad/typst-web-yjs";
import * as Y from "yjs";
import { yCollab } from "y-codemirror.next";
import {
  createTypstSetup,
  typstFilePath,
  TypstProject,
} from "@vedivad/codemirror-typst";

const ydoc = new Y.Doc();
const ytext = ydoc.getText("main.typ");
const project = new TypstProject({
  compiler,
  analyzer,
  autoCompile: { debounceMs: 500, maxWaitMs: 2000 },
});

const sync = syncYTextToTypstProject({
  project,
  ytext,
  path: "/main.typ",
});
await sync.ready;

const setup = createTypstSetup({ project, sync: "external" });

new EditorView({
  parent: document.querySelector("#app")!,
  state: EditorState.create({
    doc: ytext.toString(),
    extensions: [
      basicSetup,
      yCollab(ytext, provider.awareness, { undoManager }),
      ...setup,
      typstFilePath.of("/main.typ"),
    ],
  }),
});

For multi-file collaboration, keep a Y.js map of paths to text documents as the source of truth and sync it with syncYMapToTypstProject({ project, files }) from @vedivad/typst-web-yjs. The adapter serializes async project writes so bursts of local and remote edits settle on the latest Y.js state. Use autoCompile.debounceMs / maxWaitMs to coalesce compiles without letting the preview feel stuck.

Compile timing

TypstProject auto-compiles after every VFS mutation (setText, setMany, remove, clear, entry change). The editor plugin only mirrors CM edits into setText; the project owns the compile schedule. Configure it once per project:

const project = new TypstProject({
  compiler,
  autoCompile: {
    debounceMs: 300, // wait 300ms after the last mutation
    maxWaitMs: 2000, // force a compile at least every 2s during sustained typing
  },
});

| Option | Default | Behavior | | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------- | | autoCompile.debounceMs | 0 | Debounce — resets on every mutation, fires once mutations pause. 0 means compile on the next macrotask. | | autoCompile.maxWaitMs | 0 | Max-wait cap — forces a compile during sustained mutation bursts. Only effective when debounceMs > 0. |

Call await project.compile() directly when you need a specific result right now — it flushes any pending scheduled compile and returns the fresh result.

Initial compile: VFS mutations schedule a debounced compile, so the first render is delayed by debounceMs. To show initial output immediately (e.g. after setMany), call compile() explicitly:

await project.setMany({ "/main.typ": "= Hello!" });
await project.compile(); // bypass debounce for first render

LSP analysis

TypstAnalyzer runs a tinymist language server in a Web Worker. The wasmUrl option must point to the tinymist_bg.wasm binary from tinymist-web (installed automatically as a transitive dependency).

  • Vite: import wasmUrl from "tinymist-web/pkg/tinymist_bg.wasm?url"
  • Static server: copy node_modules/tinymist-web/pkg/tinymist_bg.wasm to your public directory

Diagnostics always come from TypstCompiler after each compile. TypstAnalyzer powers autocompletion and hover only.

Format on save

formatter: { instance: formatter, formatOnSave: true }

// With a save callback
formatter: {
  instance: formatter,
  formatOnSave: (content) => {
    fetch("/api/save", { method: "POST", body: content });
  },
}

Theme switching

createTypstHighlighting returns a controller you keep at the call site. Call setTheme(view, alias) to swap the active theme on a mounted EditorView:

const highlighting = await createTypstHighlighting({
  themes: { light: "github-light", dark: "github-dark-dimmed" },
  theme: "light",
});
const setup = createTypstSetup({
  project,
  sync: "editor-driven",
  highlighting,
});

highlighting.setTheme(view, "dark");

The same controller may be shared across multiple views, but CodeMirror compartments are reconfigured per view — call setTheme once per mounted view. Use separate highlighting controllers for views that should have different active themes.

Granular plugins

createTypstSetup composes the default extension bundle. Use the granular pieces directly when you want custom CodeMirror lint/autocomplete UI, external sync, or only part of the Typst feature set:

  • createTypstCompileSync({ project }) — mirrors the editor's content into the project's VFS on mount and on every change. The project auto-schedules the compile. Use on its own if you render diagnostics yourself.
  • createTypstDiagnostics({ project }) — subscribes to project.onCompile and dispatches diagnostics for the active file. Use on its own if you drive VFS updates outside the editor (e.g. a Yjs observer).
  • typstCompletionSource({ project }) — plugs Typst completions into your own autocompletion(...) setup.
  • createTypstHover({ project }) — adds Typst hover tooltips, optionally using a custom code highlighter.
  • createTypstFormatter({ instance }) — adds Typst formatting keybindings and optional format-on-save.
import {
  createTypstCompileSync,
  createTypstDiagnostics,
  createTypstHover,
  createTypstFormatter,
  typstCompletionSource,
  typstFilePath,
} from "@vedivad/codemirror-typst";
import { autocompletion } from "@codemirror/autocomplete";

const extensions = [
  createTypstCompileSync({ project }),
  createTypstDiagnostics({ project }),
  autocompletion({ override: [typstCompletionSource({ project })] }),
  createTypstHover({ project }),
  createTypstFormatter({ instance: formatter }),
  typstFilePath.of("/main.typ"),
];

Styling hover tooltips

Hover content uses stable CSS class names, so you can theme it from your app stylesheet. The plugin only sets scroll behavior inline (max-height + overflow) and leaves visual theming to CSS.

Useful selectors:

  • .cm-typst-hover
  • .cm-typst-hover-content
  • .cm-typst-hover-header
  • .cm-typst-hover-header-main
  • .cm-typst-hover-header-actions
  • .cm-typst-hover-signature
  • .cm-typst-hover-summary
  • .cm-typst-hover-open-docs
  • .cm-typst-hover-section
  • .cm-typst-hover-code
  • .cm-typst-hover-pre

Header element order is controllable via CSS order on .cm-typst-hover-summary, .cm-typst-hover-signature, and .cm-typst-hover-header-actions.

License

MIT