Skip to content

feat!: replace the ember-cli addon with a Babel plugin - #83

Merged
RobbieTheWagner merged 8 commits into
adopted-ember-addons:mainfrom
aklkv:feat/babel-plugin
Sep 11, 2026
Merged

RobbieTheWagner merged 8 commits into
adopted-ember-addons:mainfrom
aklkv:feat/babel-plugin

Conversation

@aklkv

@aklkv aklkv commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Includes the dependency bumps from #82, which is now closed in favour of this PR so review stays in one place.

The problem

The addon's entire mechanism is a classic ember-cli build hook:

preprocessTree(type, tree) {
  if (type === 'test') return new TestTransformFilter(tree, this.addonConfig);
  return tree;
}

That hook does not exist in the Embroider v2 addon format, and never fires in a Vite-built app. On a modern Ember build this addon is already silently inert — installed, resolved, rewritten by @embroider/compat on every build, and transforming nothing. No error, no warning; assertions just quietly stop getting messages.

I confirmed this in a large production monorepo before touching anything: in the Vite-built app, the built bundle contained zero injected messages across 146 eligible message-less assertions. The addon was doing nothing there, and had been for as long as that app has used Vite.

It still works in classically-built packages, which is why this hasn't been widely noticed.

There is no way to fix this while remaining an addon: v2 deliberately removed preprocessTree / treeFor* because host-tree build hooks are broccoli-specific and defeat static analysis. An addon whose only purpose is transforming the consuming app's source has nowhere to stand in v2.

The change

Reimplemented as a Babel plugin, which runs in both pipelines. Note this direction was attempted once before, on the migrate-to-babel-plugin branch back in 2017 — v2 and Vite have since turned it from a nice-to-have into the only way this keeps working.

This lands as v3 — v2.0.0 already shipped in August 2025 (#79, ember 4.12+/node 20+). MIGRATION.md is written as v2 → v3.

Two breaking changes:

  1. Installing is no longer enough — the plugin has to be added to the consumer's Babel config. MIGRATION.md leads with this, because the failure mode is silent: nothing errors, the test output just gets worse.
  2. Node 22+ is required. Node 20 reached end-of-life on 2026-04-30. Since this is already a major, dropping it here avoids either carrying an unsupported runtime through another cycle or spending a v4 on it. It also collapses engines from ^20.13.0 || >= 22 — a range that existed only because describe from node:test needs 20.13+ — down to >= 22.
// babel.config.mjs (Vite / Embroider)
import qunitNiceErrors from 'ember-qunit-nice-errors';
export default { plugins: [qunitNiceErrors] };
// ember-cli-build.js (classic)
babel: { plugins: [require.resolve('ember-qunit-nice-errors')] }

Config moves from config/environment.js to plugin options, and include/exclude become regular expressions rather than minimatch globs. annotation and persist are gone — both were broccoli concerns.

What this removes

The broccoli pipeline, the dummy app, testem, and the ember-specific lint tooling all go. Dropped dependencies: broccoli-persistent-filter, recast, minimatch, ember-cli-babel, ember-cli-htmlbars, ember-template-imports.

The package now has zero runtime dependencies and a single @babel/core peer.

Dropping the ember-addon keyword also means @embroider/compat no longer rewrites this package on every consumer build.

What this fixes beyond the port

v1 resolves assert by tracking the most recently seen test() call textually and only matching FunctionExpression. This resolves the actual Babel binding:

case v1 v2
test('x', function (assert) {…}) ✅ ✅
test('x', async (assert) => {…}) ❌ silently skipped ✅
test('x', function (a) { a.ok(…) }) fragile ✅
test.only / test.skip / test.todo ❌ ✅

Arrow-function tests are common in .gjs/.gts suites, so this is not a theoretical gap.

It is correspondingly stricter: an assert that is not a test callback's first parameter — one passed to hooks.beforeEach, say — is no longer transformed. v1 would sometimes catch those by accident.

Consumers upgrading should expect assertions that previously had no message to start reporting one. That is the fix working, but it does change CI output.

Verification

  • 73 tests (node --test), covering every supported assertion at both arities, already-messaged assertions, unsupported assertions, computed access, spread args, shadowed and unrelated assert bindings, all five test-callback shapes, every test.* variant, nested and inner-test scoping, multi-line assertions and embedded comments, every option including error cases, idempotency, and output-still-parses.
  • Verified against @babel/core 8.0.5 and 7.x — the plugin spans both majors with one CJS file and no shims.
  • CI runs lint, Node 22/24/26, and a @babel/core 7 job that keeps the lower bound of the peer range honest.

It was also validated end-to-end in a real monorepo across both pipelines — 362 injected messages in the Vite app (previously 0) and 34–133 per classic engine, with 4,570 tests across nine consumers passing, zero failures.

Notes

  • Ships as CommonJS deliberately. Babel loads plugin paths with require(), and classic build files are CJS, so a CJS entry works from both an ESM babel.config.mjs and a CJS ember-cli-build.js.
  • MIGRATION.md is included in the published tarball.
  • There is no longer an Ember version requirement — it works in any QUnit suite compiled with Babel.

BREAKING CHANGE: this is no longer an ember-cli addon. Installing it is no
longer enough — it must be added to your Babel config. See MIGRATION.md.

v1 worked by hooking `preprocessTree('test', ...)` and running a
`broccoli-persistent-filter` over the host's test tree. That hook does not exist
in the Embroider v2 addon format and never fires in a Vite-built app, so on a
modern Ember build v1 was already silently inert: installed, resolved, and
transforming nothing.

A Babel plugin runs in both pipelines — Vite via `babel.config.mjs`, classic
builds via the `babel.plugins` option in `ember-cli-build.js`. Dropping the
`ember-addon` keyword also means `@embroider/compat` no longer has to rewrite
this package on every build.

Removed with the broccoli pipeline: `broccoli-persistent-filter`, `recast`,
`minimatch`, `ember-cli-babel`, `ember-cli-htmlbars`, `ember-template-imports`,
the dummy app, testem, and the ember-specific lint tooling. The package now has
no runtime dependencies and a single `@babel/core` peer.

The rewrite also resolves `assert` through Babel's scope rather than tracking
the most recently seen `test()` call textually, closing two gaps v1 had: arrow
-function tests and `test.only` / `test.skip` / `test.todo` were silently
skipped, and a renamed `assert` parameter was fragile. It is correspondingly
stricter — an `assert` that is not a test callback's first parameter, such as one
passed to `hooks.beforeEach`, is no longer transformed.

Config moves from `config/environment.js` to plugin options, and `include` /
`exclude` change from minimatch globs to regular expressions. `annotation` and
`persist` are gone, both being broccoli concerns.

73 tests, CI over Node 20/22/24 and two @babel/core versions.
Follow-up to the Babel plugin migration, clearing references to machinery that
no longer exists:

- CONTRIBUTING.md described running a dummy application at localhost:4200 and
  scripts (`test:ember`, `test:ember-compatibility`) that were removed with the
  ember-cli scaffolding. Replaced with how the suite actually runs now, plus a
  pointer at the `transform` / `inTest` helpers.
- .gitignore dropped entries for build output (`/dist/`, `/declarations/`),
  `/testem.log`, and broccoli-debug's `/DEBUG/`. None of those are produced any
  more.
- package.json#directories pointed `doc` at a directory that never existed and
  `test` at `tests/`, which is now `test/`.
- MIGRATION.md is now published in the tarball — it is the reason this major
  exists, and is worth having available from node_modules.

Also picks up the pnpm 12 workspace policy file and the .prettierrc.json ->
.prettierrc.mjs swap.
v2.0.0 shipped in August 2025 (ember 4.12+/node 20+, plus configurable
include/exclude), so the Babel plugin change is the next major after that.
MIGRATION.md and the README referred to it as v2 throughout.

The Embroider "v2 addon format" reference is left alone — that v2 means the
addon format, not this package's version.
`node --test test/` resolves the directory as a module on some Node versions
and fails with "Cannot find module .../test". It works on Node 20 and 26 but
breaks on 22 and 24, which is why CI went red on exactly those two jobs while
passing locally.

Shell-expanded explicit paths are accepted by every version. Verified against
Node 20, 22, 24 and 26: 73 passing on each.

A quoted glob would be the other option, but Node 20 has no glob support in the
test runner, and the matrix includes it.
BREAKING CHANGE: Node 20 is no longer supported.

Node 20 reached end-of-life on 2026-04-30 and no longer receives security
patches. Since this release is already a major, it is the natural point to drop
it rather than carrying an unsupported runtime into the next cycle.

This also removes an awkward constraint: engines was `^20.13.0 || >= 22`
purely because `describe` from node:test is only stable on 20.13+. With Node 20
gone that collapses to `>= 22`.

CI matrix moves from 20/22/24 to 22/24/26 — the currently supported lines.
Verified locally on all three.
The guide covered the Babel-config change but not the runtime bump, so an
upgrader hitting an engines error would have had nothing to go on. Both
breaking changes are now stated up front, with a short section on why Node 20
is gone.
With Node 20 dropped, the test runner's own glob support is available, so the
scripts no longer need shell-expanded file lists.

Two things this fixes:

- `--watch` now picks up newly added test files. Shell expansion resolved once,
  at launch, so a new file was invisible until the command was restarted.
- It no longer depends on the shell expanding `*`, which some shells do not.

The glob is quoted so it reaches Node verbatim rather than being expanded first.
It stays explicit rather than using a bare `node --test`, because Node's default
patterns include `**/test/**/*.cjs` and would sweep up `test/helpers.cjs` —
that reports 74 tests instead of 73, counting a file with no tests in it.

Verified on Node 22, 24 and 26: 73 passing on each.
@RobbieTheWagner
RobbieTheWagner merged commit 53704ab into adopted-ember-addons:main Sep 11, 2026
7 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 11, 2026
@jrjohnson

Copy link
Copy Markdown

@aklkv I'm so happy to have this back in our tests - this made me entire week! THANK YOU!!

@aklkv
aklkv deleted the feat/babel-plugin branch September 11, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants