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

babel-plugin-dispose-sugar

v0.5.0

Published

Proof-of-concept implementation of the Resource Disposal Syntax Sugar TC39 proposal (Stage 0)

Readme

babel-plugin-dispose-sugar

A proof-of-concept implementation of Resource Disposal Syntax Sugar, a Stage 0 TC39 proposal that adds dedicated syntax for declaring and invoking Symbol.dispose / Symbol.asyncDispose:

class TempFile {
  dispose { this.handle.close(); }        // instead of [Symbol.dispose]() {...}
}

dispose file;                              // instead of file[Symbol.dispose]()
await dispose connection;                  // instead of await connection[Symbol.asyncDispose]()
dispose? cache;                            // instead of cache?.[Symbol.dispose]?.()
await dispose? cache;                      // instead of await cache?.[Symbol.asyncDispose]?.()

Please read "How this actually works" below before using this beyond a demo. It explains why this package needs a pre-parse transformation instead of relying on a normal Babel visitor.


How this actually works

Normal Babel plugins are visitors that run after @babel/parser has already turned source text into an AST. dispose file; is not valid ECMAScript today, so @babel/parser throws a SyntaxError on it before any plugin gets a chance to run. Unlike JSX, TypeScript, or the pipeline operator, there is no public API for a third-party package to teach @babel/parser new grammar — those are wired into the parser's internals directly by the Babel team. Adding real grammar support means forking @babel/parser.

This package works around this by using Babel 7's parserOverride hook. Before @babel/parser sees the source, a lexical pre-pass rewrites the four dispose forms into their already-valid JavaScript equivalents.

Once the source has been transformed into valid JavaScript, a real Babel plugin (built with @babel/traverse and @babel/types) handles the parts that benefit from AST awareness — generating collision-free temporary variables and detecting duplicate Symbol.dispose definitions.

your source (with dispose syntax)
        │
        ▼
┌────────────────────────┐   hand-rolled tokenizer, NOT regex substitution
│  src/preprocess.js     │   (correctly ignores strings/comments/templates)
└────────────────────────┘
        │  produces valid ES2022+ source, with __disposeOptionalSync(...)/
        │  __disposeOptionalAsync(...) markers standing in for the two
        │  "optional disposal" forms
        ▼
┌────────────────────────┐
│   @babel/parser.parse  │
└────────────────────────┘
        │  real AST
        ▼
┌────────────────────────┐   real @babel/traverse visitor:
│  src/index.js           │   - expands the two marker calls into
│  (disposeSugarPlugin)   │     `const _tempN = x; _tempN?.[Symbol.dispose]?.();`
│                          │     using scope.generateUidIdentifier (guaranteed
│                          │     collision-free, per the proposal's requirement)
│                          │   - walks every ClassBody and throws a SyntaxError
│                          │     on a duplicate [Symbol.dispose] / [Symbol.asyncDispose]
└────────────────────────┘
        │
        ▼
┌────────────────────────┐
│  @babel/generator      │
└────────────────────────┘
        │
        ▼
   final JavaScript

The two non-optional forms (dispose x / await dispose x) desugar to a single expression with no new bindings, so the lexical pre-pass handles them completely on its own — there's nothing left for the AST plugin to do. The two optional forms (dispose? x / await dispose? x) need a temp variable, which is exactly the kind of thing you want real scope analysis for rather than string concatenation, so those go through the AST plugin.


Installation

npm install babel-plugin-dispose-sugar

This plugin requires:

  • Node.js >= 16
  • Babel 7 (@babel/core >= 7.0.0)

Babel 6 (babel-core) is not supported because this plugin uses Babel 7's parserOverride API.

The package internally uses: @babel/parser, @babel/traverse, @babel/types, and @babel/generator. These are installed automatically with the package.

@babel/core is a peer dependency because Babel provides the runtime environment that loads the plugin.


Usage

Babel configuration

Add the plugin to your Babel config:

{
  "plugins": [
    "babel-plugin-dispose-sugar"
  ]
}

The convenience API (recommended)

const { transform } = require('babel-plugin-dispose-sugar/transform');

const { code } = transform(`
  class TempFile {
    #handle;
    constructor(name) { this.#handle = open(name); }
    dispose { this.#handle.close(); }
  }

  using file = new TempFile('data.txt');
  if (someCondition) {
    dispose file;
  }
`, {
  // Forwarded to @babel/parser. Useful for e.g. `using` declarations
  // (Explicit Resource Management) or class private fields.
  parserPlugins: ['explicitResourceManagement', 'classPrivateProperties'],
});

console.log(code);
class TempFile {
  #handle;
  constructor(name) {
    this.#handle = open(name);
  }
  [Symbol.dispose]() {
    this.#handle.close();
  }
}
using file = new TempFile('data.txt');
if (someCondition) {
  file[Symbol.dispose]();
}

transform() returns { code, intermediateCode, ast }. intermediateCode is the valid-JS output of the lexical pre-pass, before the AST plugin runs — handy for debugging or for demonstrating the two-stage pipeline to others.

Running just the lexical pre-pass

If you want to feed the result into your own @babel/core pipeline (e.g. to combine it with other, unrelated Babel plugins), you can call the pre-pass directly and hand its output to @babel/core yourself:

const babel = require('@babel/core');
const { transformDisposeSyntax } = require('babel-plugin-dispose-sugar/transform');

const validSource = transform.transformDisposeSyntax(rawSourceWithDisposeSyntax);
const { code } = babel.transformSync(validSource, {
  plugins: [
    require('babel-plugin-dispose-sugar/src/plugin'), // the bare plugin function
    // ...your other plugins
  ],
});

(src/plugin.js is the Babel entry point. It exports the plugin function and uses Babel 7's parserOverride hook to run the lexical pre-pass before parsing. The src/index.js module contains the AST plugin and the programmatic transform() API.)

Running the tests

npm test

This runs test.js, a small dependency-free test file (plain node:assert, no test framework) covering:

  • Class method declarations, sync and async, including alongside ordinary methods/fields and with static
  • Manual disposal, sync and async, including member chains and call expressions as operands
  • Optional disposal, sync and async, including a hygiene check (multiple optional disposals in the same scope get distinct temp names) and a real runtime execution check that the operand is evaluated exactly once
  • Edge cases: nested if/else branches, multiple dispose statements in a row, dispose followed by line/block comments, the word "dispose" appearing inside comments/strings/template literals (must be ignored), and duplicate-Symbol.dispose detection (including that it's scoped per-class, not global)
  • Negative tests proving dispose is left alone as an ordinary identifier in every other context: variable declarations, function/class declarations, member access, reassignment, object property names, typeof, and bare expression statements

All 36 tests currently pass.


Project structure

babel-plugin-dispose-sugar/
├── src/
│   ├── tokenizer.js    # hand-rolled JS tokenizer (strings/comments/templates/regex-aware)
│   ├── preprocess.js   # token-stream pattern matching + source rewriting
│   ├── index.js        # AST plugin + transform() convenience API
│   └── plugin.js       # Babel plugin wrapper (parserOverride + re-export)
├── test.js
├── example.js
├── package.json
├── .babelrc          # development config
└── README.md