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

@nullvoxpopuli/ember-rolldown

v2.7.0

Published

Meta plugin for building ember v2 libraries/addons with rolldown & tsdown.

Readme

@nullvoxpopuli/ember-rolldown

A batteries-included meta-plugin for building Ember v2 libraries (addons) with rolldown — or tsdown, which is built on top of rolldown and also emits .d.ts files.

It compiles .gts/.gjs and template-tag (<template>) source into publishable output, so a single ember() call replaces the usual stack of @embroider/* externals handling, content-tag preprocessing, and babel wiring.

Install

npm add --save-dev @nullvoxpopuli/ember-rolldown

Requires node 24+, and — since these packages ship TypeScript source — a modern TypeScript when type-checking: 6+ with lib covering es2025 (e.g. esnext).

Usage

Import defineConfig from the bundler you're using — tsdown or rolldown — so it carries that tool's own config types; @nullvoxpopuli/ember-rolldown provides ember() (the defaults travel with the plugin, not defineConfig).

In your tsdown.config.js (recommended — emits declarations):

import { defineConfig } from "tsdown";
import { ember } from "@nullvoxpopuli/ember-rolldown";

export default defineConfig({
  entry: ["./src/index.ts"],
  plugins: [ember()],
});

This builds your entry to dist/*.js and dist/*.d.ts with sourcemaps, cleaning dist/ between builds and leaving the ember virtual packages to the consuming app. You choose the entry and plugins; any tsdown option you pass wins.

Or in a plain rolldown.config.js:

import { defineConfig } from "rolldown";
import { ember } from "@nullvoxpopuli/ember-rolldown";

export default defineConfig({
  input: ["src/index.ts"],
  plugins: [ember()],
});

A babel config is optional. Without one, ember() compiles templates (to precompileTemplate), decorators (via decorator-transforms), and TypeScript; with one, your config runs instead. A babel.publish.config.* is preferred over a babel.config.* — see Publish vs. development babel config.

Entrypoints

Entries may be any extension — .ts, .js, .gts, .gjs:

export default defineConfig({
  entry: ["./src/index.ts", "./src/components/menu.gts"],
  plugins: [ember()],
});

Whatever the source extension, the emitted .js and .d.ts paths mirror the entry paths (that's how tsdown's dts support works): the example emits dist/index.js + dist/index.d.ts and dist/components/menu.js + dist/components/menu.d.ts. Type imports resolve against those emitted paths, so your entries — together with your exports map — are your public API: a module that isn't an entry has no stable dist/ path of its own and is only reachable through the entrypoints that re-export it.

Declarations

Declarations are emitted with isolated declarations — the only declaration pipeline that can see <template> (.gts/.gjs) modules, which exist only inside the bundler's module graph. The tsconfig your build uses must enable it (ember() errors otherwise):

{
  "compilerOptions": {
    "isolatedDeclarations": true,
  },
}

Isolated declarations means every exported value carries an explicit type annotation. For template-only components:

import type { TOC } from "@ember/component/template-only";

export const Badge: TOC<BadgeSignature> = <template>...</template>;

Keeping the constraint off dev-only code

isolatedDeclarations is a constraint on how published code is written, so it should cover only the code you emit declarations for. If your package also holds dev-only code — a demo app, in-package tests — putting the flag on the single tsconfig.json that covers everything would force explicit annotations on demo components and test helpers that never get a .d.ts emitted for them.

The tsconfig ember() checks is the one tsdown's tsconfig option points at, so point the build at a publish-only config and leave tsconfig.json alone for editors and tsc --noEmit:

// tsdown.config.js
export default defineConfig({
  entry: ["./src/index.ts"],
  // include: ["src/**/*"], isolatedDeclarations: true
  tsconfig: "./tsconfig.publish.json",
  plugins: [ember()],
});

Any path works, so the publish config can live wherever you keep build configuration — tsconfig: "./config/tsconfig.publish.json". (A directory works too: tsconfig: "./config" picks up config/tsconfig.json.)

tsconfig: false is rejected while declarations are on: with no tsconfig there is no isolatedDeclarations, and the fallback pipeline can't see compiled .gts, so it would fail later with "Source file not found". Set dts: false alongside it if the library ships no types.

A tsconfig's relative paths resolve against the file itself, so a config kept in config/ wants "include": ["../src/**/*"] and "rootDir": "../src". The build won't tell you if you get that wrong — tsdown drives declaration emit from entry, not from the tsconfig's include — but tsc/ember-tsc and your editor will, if you ever point them at that config.

The tradeoff: isolated-declaration errors in src then surface when you build rather than in your editor, since the editor uses tsconfig.json. Run the build (or tsc --noEmit -p tsconfig.publish.json) in CI so nothing lands unchecked.

CSS

Components that import co-located CSS (import './popup.css') need @tsdown/css installed in your library — tsdown auto-detects it and bundles every imported stylesheet into a single CSS file in dist/. Install the version matching your tsdown version (they're released in lockstep):

npm add --save-dev @tsdown/css

Without it, tsdown's css-guard fails the build on the first CSS import — and because the importing component module never loads, any declaration that references that component dangles, surfacing as misleading UNLOADABLE_DEPENDENCY errors on <component>.d.ts files.

ember-scoped-css

ember-scoped-css works with this pipeline: its template transform rides along via ember()'s babel.templateTransforms option, and its unplugin (ember-scoped-css/rollup) resolves the scoped CSS requests the transform injects:

import { defineConfig } from "tsdown";
import { ember } from "@nullvoxpopuli/ember-rolldown";
import { scopedCSS } from "ember-scoped-css/rollup";
import { scopedCSS as scopedCssBabel } from "ember-scoped-css/babel";

export default defineConfig({
  entry: ["./src/index.ts"],
  css: { inject: true },
  plugins: [
    ember({
      babel: {
        plugins: [scopedCssBabel()],
        templateTransforms: [scopedCssBabel.template({})],
      },
    }),
    scopedCSS(),
  ],
});

This scopes co-located .css files, inline <style scoped> blocks, and the scopedClass pseudo-helper (that last one is what the babel.plugins entry handles — leave it off if you don't use scopedClass in module code).

css.inject matters for libraries: it keeps the import "./style.css" statement in dist/index.js, so consuming apps pull the styles in through the module graph — without it the bundled CSS is emitted but nothing loads it.

What ember() does

ember() returns an array of rolldown plugins:

  • emberIsolatedDeclarations() — errors when the tsconfig the build uses (tsdown's tsconfig option, defaulting to tsconfig.json) is present without isolatedDeclarations: true.
  • emberExternals() — keeps your dependencies, peerDependencies, and the ember virtual packages (e.g. @ember/component, @glimmer/tracking, the template compiler) external, so the consuming app resolves them.
  • emberTransform() — preprocesses <template> via content-tag and maps .gts/.gjs to .ts/.js so rolldown understands them. Also rewrites .gts specifiers in emitted .d.ts files.
  • emberBabel() — runs babel with babelHelpers: "bundled", but only on the files that actually need it (template-tag, decorators, template imports); everything else stays on rolldown's fast native (oxc) transform. Uses your babel.publish.config.* (root or config/) in preference to your babel.config.*.

Configuration

ember({
  babel: {
    configFile: "./babel.config.js",
    babelHelpers: "bundled",
    plugins: [],
    templateTransforms: [],
    filter: { include: { imports: ["ember-concurrency"], code: [] } },
  },
});

Each option is documented on BabelOptions. templateTransforms feeds template AST transforms to the default template-compilation step; it can't be combined with a babel config file — a config lists babel-plugin-ember-template-compilation itself, so its transforms belong there.

Publish vs. development babel config

A library's plain babel.config.* is usually its development config: it compiles @embroider/macros away, targets the wire format, and wires up whatever the in-package demo app or test suite needs. None of that belongs in a published artifact — macros must survive for the consuming app to evaluate, and the wire format is private between one template compiler and one glimmer runtime of the same version.

Babel's own resolution can't tell those apart, so ember() looks for a config named for publishing first. With no explicit configFile, detection is:

  1. babel.publish.config.{mjs,cjs,js,mts,cts,ts,json} in the package root
  2. the same names in config/
  3. otherwise babel's own resolution (babel.config.*, honoring rootMode)
  4. otherwise no config file — templates, decorators and TypeScript are still handled by the built-in defaults

So a library that keeps both configs needs no babel option at all:

my-addon/
  babel.config.mjs          # dev: macros compiled, wire format, test-app wiring
  babel.publish.config.mjs  # what ember() uses

...and so does one that keeps build configuration out of its root:

my-addon/
  babel.config.mjs
  config/
    babel.publish.config.mjs  # what ember() uses
    tsconfig.publish.json     # see below

Set configFile explicitly to override that (configFile: false ignores config files entirely, which is what you want when your publish config would just restate the built-in defaults).

App re-exports

For libraries whose modules must appear in the consuming app's namespace (classic resolution: {{a-component}}, services and helpers looked up by name), appReexports() does the same job as @embroider/addon-dev's appReexports rollup plugin. It's a separate import because most libraries don't need it:

import { defineConfig } from "tsdown";
import { ember } from "@nullvoxpopuli/ember-rolldown";
import { appReexports } from "@nullvoxpopuli/ember-rolldown/app-reexports";

export default defineConfig({
  entry: ["./src/index.ts", "./src/services/session.ts"],
  plugins: [ember(), appReexports()],
});

With no arguments, top-level services (services/*) are re-exported — under strict mode, components and helpers are imported, but services are still injected by name. A string or array of strings is the include glob(s), optionally followed by the remaining options; an object gives full control:

appReexports(); // services/*
appReexports("components/**"); // one include glob
appReexports(["services/*", "helpers/*"]);
appReexports("components/**", { exclude: ["components/-private/**"] });
appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });

For every built file matching include (minus exclude and .d.ts files), it writes a module under dist/_app_/ re-exporting from the library's own name, and records the set in package.json under ember-addon.app-js. mapFilename renames a re-export; exports picks which bindings it forwards (default ["default"]).

Unlike the embroider plugin, nothing is written unless its content actually differs from what is on disk — _app_/ modules are compared byte-for-byte and package.json is only rewritten when the app-js map itself changed — so rebuilds don't re-trigger file watchers.

Credit

The emberExternals and emberTransform plugins are derived from embroider-build/embroider#2658.