Skip to content

How to add Hem to a Vite project

Install the plugin, point it at your tsconfig, and get .hem files compiling to siblings your build already understands.

Updated View as Markdown

Hem’s packages are not published to npm yet. The commands below name them as they will ship. Today they resolve inside the Hem repository’s own workspace.

Hem compiles each .hem file to a .ts or .tsx sibling on disk. Vite, your framework, your test runner, and your editor all keep seeing ordinary TypeScript, so adding Hem to an existing project changes what you write, not what your toolchain consumes.

Install the plugin

npm install -D @hem-lang/vite-plugin vite

Vite is a peer dependency and Hem accepts versions 6, 7, and 8. A React project also needs react, react-dom, and @types/react.

Register the plugin

vite.config.tsts
import { defineConfig } from "vite";
import { hem } from "@hem-lang/vite-plugin";

export default defineConfig({
  plugins: [hem()],
});

On build start the plugin compiles the whole tsconfig project and writes each sibling. In dev it watches every .hem file and re-emits the siblings whose bytes actually changed.

Place hem() alongside your framework plugin rather than instead of it. The plugin does not own the JSX transform.

Confirm the tsconfig covers your Hem files

Hem reads your real tsconfig.json for module resolution, libraries, paths, JSX, and project membership. A .hem file has to be covered by that config’s include or files, otherwise the compiler refuses it rather than checking it in isolation.

You do not need to change your strictness settings. Hem’s own checker overlays strict, strictNullChecks, and noUncheckedIndexedAccess internally, so your existing TypeScript build keeps its current options while Hem code still gets its guarantees.

Configure JSX if you write components

JSX stays a TypeScript and framework concern:

  • "jsx": "react-jsx" works against @types/react 19 as written.
  • "jsx": "preserve" needs "jsxImportSource": "react" beside it. Bare "preserve" fails with TypeScript’s own TS7026, which is not a Hem error and will not mention Hem.

For the runtime transform, use your framework’s React plugin. On Vite 8 you can instead set the oxc runtime directly:

vite.config.tsts
import { defineConfig } from "vite";
import { hem } from "@hem-lang/vite-plugin";

export default defineConfig({
  plugins: [hem()],
  oxc: { jsx: { runtime: "automatic", importSource: "react" } },
});

Decide what to do with the generated siblings

Every emitted file carries a // Generated from … by hem — do not edit. hem:sha256=<16 hex> banner, and each successful build refreshes .hem/generated-files.json.

Commit the manifest. To keep generated siblings out of reviews, add their exact project-relative paths to .gitignore. Obtain the list after a successful build:

hem generated exclusions .

For example, if the list contains src/greet.ts, ignore /src/greet.ts rather than *.ts. Update the entries when Hem modules are added or removed. Keep handwritten TypeScript, declarations, and configuration files visible to Git. A .hem file sitting beside a handwritten file of the same name is a build error (HEM3002), never a silent overwrite. Editing a generated sibling also causes an error (HEM3008) on build, watch, Vite refresh, or hem check. Move the change into the .hem source, then delete the sibling (or restore it with git) and rebuild.

To configure formatters, linters, and other tools to ignore generated siblings, see how to keep other tools off generated files.

Write your first module

src/greet.hemhem
export let greet = (name: string) => `Hello, ${name}`

A .ts file imports it as it would import anything else:

import { greet } from "./greet.hem";

The Vite plugin resolves the .hem specifier to the emitted sibling. Without the plugin, compile Hem first and import the emitted module using your toolchain’s normal resolution rules. Neither path requires bindings or wrappers.

If you use Effect

Set the transport once in your tsconfig so the CLI, the language server, and the plugin all read the same value:

{ "hem": { "effectImportSource": "effect" } }

Passing hem({ effectImportSource }) overrides it for the Vite build only. See how to enable the Effect extension for what activating it changes.

Next

Navigation

Type to search…

↑↓ navigate↵ selectEsc close