hazeljs

中文文档
0.0.3发布于 2026/11/04MITTypeScriptESM + CJS零依赖

Hazel edit boxes that extend ordinary JavaScript programs with custom behavior. Experimental.

原始使用文档
文档目录17 节
  1. Why
  2. Install
  3. Usage
  4. Analyze and run
  5. Call Hazel functions from JavaScript
  6. API
  7. Values
  8. A program of modules
  9. Errors, holes and unfinished code
  10. Showing them
  11. Running anyway
  12. Prototype use case
  13. Size and speed
  14. Is it really Hazel?
  15. Limits
  16. About Hazel
  17. License

Hazel's hazelnut logo

hazeljs

Hazel edit boxes that extend ordinary JavaScript programs with custom behavior.

You bring the box, a text area or your own editor; hazeljs type-checks what is typed in it, says what is wrong and where, and runs it, even while it is unfinished.

Hazel, the typed functional programming language with holes, as a JavaScript library. Analyze and run Hazel programs from Node or a web page, and call the functions they define from JavaScript.

const Hazel = require("hazeljs");

const double = Hazel.run("fun (x : Int) -> x * 2").value;
double(21); // 42

Experimental. This is a prototype (0.0.x): the API may change in any release. It is built from the draft library in hazelgrove/hazel#2711 and #2719, at commit 0d66c36618 (Hazel.build names it).

Why

Hazel type-checks and runs programs that are not finished: a program with holes or type errors still runs, around them. That makes it a good language for code a page lets people write and change while it runs: rules, scripts, exercises.

We call a JavaScript program whose event handlers are Hazel functions a Hazlet: the page calls a Hazel function with each event and acts on what it returns. The first is a game, below under Prototype use case.

Install

npm install hazeljs

Or in a web page, from a CDN that serves npm packages:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/hazel-lib.js"></script>
<script>
  console.log(Hazel.run("1 + 2").value); // 3
</script>

The script defines a global Hazel (window.Hazel). The same file is at https://unpkg.com/[email protected]/hazel-lib.js.

Usage

CommonJS:

const Hazel = require("hazeljs");

ES modules, the whole object or its parts:

import Hazel from "hazeljs";
import { analyze, run, analyzeProgram, runProgram } from "hazeljs";

TypeScript types are included (index.d.ts).

Analyze and run

Hazel.analyze("1 + true");
// { errors: ["Error in term:\n  true\nNature of error: Expecting type Int but got inconsistent type Bool"] }

Hazel.run("let x = 2 in x * 21");
// { errors: [], value: 42 }

A program's errors do not stop it running: Hazel evaluates around its holes and errors, so value is there either way, and your code decides which errors it accepts.

Call Hazel functions from JavaScript

Analyze a program once, then call its functions as often as you like:

const rule = Hazel.run(`
  type Cmd = + Done + Flip(Int, Cmd) in
  type Event = + Pull(Int) in
  fun e : Event -> case e | Pull(l) => Flip(l, Done) end`).value;

rule({ tag: "Pull", value: 2 }); // { tag: "Flip", value: [2, { tag: "Done" }] }

API

CallReturns
Hazel.analyze(text){ errors }: the program's error messages, as strings.
Hazel.run(text){ errors, value }: the same, and the program's value.
Hazel.analyzeProgram(spec){ ok, errors, holes, programHoles, incomplete, programIncomplete, ms }, for a program of named modules (below).
Hazel.runProgram(spec)The same, and value.
Hazel.versionThe library's own label, such as "0.1 (draft, js-library)".
Hazel.buildThe hazelgrove/hazel commit this build is from.

Values

Values convert both ways: Hazel's results come back as these, and a JavaScript value passed to a Hazel function goes in as these.

HazelJavaScript
Int, Float and the other numbersnumber
Boolboolean
Stringstring
a listan array
()null
a labeled tuple (x = 1, y = 2)an object {x: 1, y: 2}
another tuple (1, true)an array [1, true]
a constructor A, applied B(3){tag: "A"}, {tag: "B", value: 3}
a functiona JavaScript function (one argument, or several as a tuple)

Anything else (a type, a hole) comes back as {hazel: "<its printing>"}.

A constructor from JavaScript has no type of its own, so Hazel checks it where a parameter is annotated (fun e : Event -> ...). An applied constructor is checked whole, tag and payload; one that does not fit the type stays stuck at the parameter, and the call returns {hazel: ...}.

A program of modules

For a page with an editor per module, hand Hazel the texts and let it build one program from them:

const result = Hazel.runProgram({
  preamble: "type Cmd = + Done + Say(String) in",  // definitions every module sees, ending in `in`
  modules: [
    { name: "Common", text: "let greet = fun (s : String) -> Say(s)" },
    { name: "Room", text: "let on_event = fun (n : Int) -> Common.greet(\"hi\")" },
  ],
  tail: "Room.on_event",  // what the program gives; () if left out
});

result.ok;       // true: no errors, no holes, nothing left unfinished
result.value(1); // { tag: "Say", value: "hi" }

Each module sees the preamble and the modules before it, as Common.greet, and the tail sees them all. A module's text is its body: its members, with no in.

  • errors: [{ module, message, id }], where module is the name of the module the error is in, or null for the preamble and the tail.
  • holes: each module's holes, by name ({ Common: 0, Room: 1 }); programHoles, the preamble's and the tail's.
  • incomplete: each module's brackets and keywords left open (a ( with no ), a case with no end) or closed with no opener; programIncomplete, the preamble's and the tail's.
  • ok: no errors, no holes and nothing incomplete. A function still being written can have holes and no errors, so act on value only when ok.
  • ms: meant to be how long the call took, in milliseconds. In 0.0.1 to 0.0.3 it reads 0 (a known bug); time the call yourself.

Hazel remembers the texts of the last program it read, part by part, so calling again after one module changes reads only that module again.

Errors, holes and unfinished code

Hazel reads the whole program before it runs any of it, and it reports what is wrong as data, not as an exception. An example from Messed Up Dress Up's trap room: a lever is a Side, L or R, and the room's rule passes a number instead.

const result = Hazel.analyzeProgram({
  preamble: "type Side = + L + R in",
  modules: [
    { name: "Common", text: "let flip = fun (s : Side) -> s" },
    { name: "TrapRoom", text: "let answer = Common.flip(3)" },
  ],
  tail: "TrapRoom.answer",
});
{
  ok: false,
  errors: [{
    module: "TrapRoom",
    message: "Error in term:\n  3\nNature of error: Expecting type Side but got inconsistent type Int",
    id: "3d0f0000-0000-4000-b53b-3f45d4c660e0",
  }],
  holes: { Common: 0, TrapRoom: 0 }, programHoles: 0,
  incomplete: { Common: 0, TrapRoom: 0 }, programIncomplete: 0,
  // and ms
}

A program can be not ready in three ways, and each is counted where it is:

WhatExampleWhere it shows
An errorCommon.flip(3): a number where a Side is wantederrors, each with its module
A hole: a part not written yetCommon.flip(?)holes: { TrapRoom: 1 }, and no error
Something unfinished: a bracket or keyword without its partnerCommon.flip(Lincomplete: { TrapRoom: 1 }

A program with only text, read by Hazel.analyze or Hazel.run, reports its errors as strings, without a module:

Hazel.analyze("let x : Int = true in x").errors;
// ["Error in term:\n  true\nNature of error: Expecting type Int but got inconsistent type Bool"]

Showing them

Hazel's messages are written for Hazel's developers. Messed Up Dress Up says a type error the way a person reads it, with this function (its scriptErrorText, slightly shortened). Other messages pass through as they are:

function errorText(message) {
  const m = message.match(/^Error in term:\s*\n\s*([\s\S]*?)\nNature of error: Expecting type ([\s\S]*?) but got inconsistent type ([\s\S]*)$/);
  if (!m) return message;
  const a = t => (/^[AEIOU]/.test(t.trim()) ? "an " : "a ") + t.trim();
  return `${m[1].trim()}: ${a(m[2])} is wanted here, but this is ${a(m[3])}`;
}

for (const e of result.errors) console.log(`${e.module ?? "the program"}: ${errorText(e.message)}`);
// TrapRoom: 3: a Side is wanted here, but this is an Int

Two things to know when you show them:

  • A sum type with no name is written out. Had Side been defined in a module instead of the preamble, the message would say L + R. The game maps such types back to its own names.
  • A name that is not there is a missing label. Common.flop(L) gives Nature of error: Label not found, since a module's members are reached like a labeled tuple's.

The game shows a room's errors in three places: under the room's box, by name, with ⚠️ and a count in its outline, and as a check entry in its event log, before anything runs.

The trap room's box in Messed Up Dress Up, with its type errors listed under its name.

Running anyway

runProgram and run evaluate around errors and holes, as Hazel always does, so value is there even when ok is false. But where an error or a hole stopped the evaluation, the value comes back as { hazel: "…" }, a printout of the term it stopped at, and a page cannot act on that. Messed Up Dress Up keeps answering with its own JavaScript while a room's rule is not ok, and logs Hazel's answer beside it, so the game keeps working while the rule is fixed. For more, see the game's Game Scripts Guide, section 8, on broken scripts.

Prototype use case

Messed Up Dress Up is a dress-up and adventure game in plain JavaScript, made with two young players. It is the first Hazlet: five of its rooms hand their events to a rule, a function on_event : (Globals, Event) -> Cmd, which players can write in Hazel (or in JavaScript, or in Fumola) in the game's 📜 Scripting panel, while they play. The game builds one program from the panel's modules with Hazel.runProgram, then calls the room's rule with each event and runs the commands it returns.

The trap room's rule in Hazel, beside the game:

The game's Scripting panel at full size: the game's view at the top left, and the trap room's rule in Hazel, a case on the event: a red lever pulled flips the pits wired to it, a blue lever pulled moves the boulders.

Playing and reading at once: each event the room hands to Hazel, and Hazel's answer, in the panel's event log:

The event log: calls of TrapRoom.on_event with Pull(L) and Pull(R) events, each with the event and the rule's answer as JSON, what the call changed (a lever from false to true), and how long the rule and the call took.

The game's guide to its scripts, with more of these pictures: Game Scripts Guide.

Size and speed

hazel-lib.js is about 2.8 MB, 640 KB gzipped. Measured in Node 22 on a laptop: require("hazeljs") takes about 170 ms, and running a small program about 30 ms. A module of 100 short members takes about 110 ms to analyze, and one of 400 about 2.3 s; after an edit, only the modules whose text changed are read again.

Is it really Hazel?

hazel-lib.js is 2.8 MB of compiled JavaScript, so reading it tells you little. What you can check is that it is exactly what Hazel's source compiles to, at a commit you can read.

It names its commit. Hazel.build returns the hazelgrove/hazel commit it was built from, and the hazel field of package.json names the same commit, with its branch and pull request. For this release that is 0d66c36618ee709a60f91cf01fbf0b46121f2499.

Rebuild it and compare. With a clone of hazelgrove/hazel and its toolchain set up (see its README), run

scripts/verify-build.sh path/to/hazel

It checks out that commit, builds hazel-lib.js the way Hazel's own Deploy workflow does (the release profile, with GITHUB_SHA set to the commit), and compares the two files. Every byte must match except one comment line, //# buildInfo:, where js_of_ocaml names its own version label (5.9.1 from one install, 5.9.1+5.9.1 from Hazel's lock file). Without that line, this release's file has the SHA-256 4695853925917b8dc8514a78c3adbd3140a2277d79ab2c27612ce1595e08e856.

Or let GitHub rebuild it. This repository's Verify hazel-lib.js workflow runs the same script on GitHub's machines, from a fresh clone of Hazel. Anyone can run it from the Actions tab, and its runs are public.

Hazel's own CI built it too. Hazel's Deploy workflow builds every commit pushed to hazelgrove/hazel. Its build of this commit (run 39680368678) gives the same hash without the buildInfo line. That build is served at https://build.hazel.org/js-library-embed/hazel-lib.js only until the branch gets a newer commit:

curl -sL https://build.hazel.org/js-library-embed/hazel-lib.js | grep -v '^//# buildInfo:' | sha256sum
grep -v '^//# buildInfo:' node_modules/hazeljs/hazel-lib.js | sha256sum

What this shows is where the file came from. It is that commit of Hazel, compiled by the OCaml toolchain in Hazel's lock file, and nothing else. It does not show that the code at that commit does what you want; for that, read it, or trust the people who wrote it.

Limits

  • Experimental. No promise of stability yet; pin an exact version.
  • Synchronous. Every call runs on the calling thread, so a long-running program blocks it.
  • One global. The library puts one Hazel object on the global object.
  • A console line for slow reads. A text that only Hazel's slower, recovering parser can read (an unclosed bracket, such as let x = () prints a SLOW PARSE (...) line to the console when it is read.
  • No undo or redo. The library holds no editor: it reads the text you give it and answers. Undo and redo are part of Hazel's editor, not of this library, so the page that holds the text keeps its history: keep the texts (or each module's text) it has had, and pass an older one again to go back. Going back costs what an edit does: the modules whose text changed since the last call are read again, and the others are not.
  • Errors name a term, not a place. An error's id is the id of its term in Hazel's syntax tree; the library gives no line and column for it yet.
  • A stuck value is a debug printout. Where an error or a hole stopped the evaluation, { hazel } holds an OCaml printout of the term, not Hazel syntax.
  • Deep programs need a bigger stack. Hazel's analysis recurses deeply: in Node, a module of about 150 members overflows the default stack (Stack overflow). Raise it: node --stack-size=32000 app.js, with the shell's own limit raised to match (ulimit -s 65536), or Node crashes instead.

About Hazel

Hazel is developed by the Hazel team: see hazel.org for the project, its papers and the people who make it, and the contributors on GitHub for who has worked on the code. Its source is hazelgrove/hazel, and questions and bugs go to its issue tracker.

License

MIT, as Hazel is. See LICENSE.

版本与发布记录

hazeljs 共发布 4 个版本,首次发布于 2026/11/03;过去 12 个月发布了 4 个版本。

版本发布日期距今
0.0.3latest2026/11/04今天
0.0.22026/11/04今天
0.0.12026/11/031 天前
0.0.0-stage2026/11/031 天前

关于 hazeljs 的常见问题

hazeljs 是什么?

用自定义行为扩展普通JavaScript程序的Hazel编辑框。实验。

如何安装 hazeljs?

在项目目录运行 npm install hazeljs,或使用 pnpm add / yarn add / bun add。

hazeljs 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

hazeljs 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

hazeljs 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

hazeljs 可以商用吗?

hazeljs 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

hazeljs 还在维护吗?

最新版本 0.0.3 发布于 2026/11/04(今天);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/11/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。