When an LTS upgrade stops halfway, Ember itself is rarely the thing blocking it. ember-source upgrades are well-behaved: read the deprecation guide, run the codemods, fix what breaks. What blocks the upgrade is the twenty-odd addons in package.json, several of which were last published in 2019, two of which patch internals that no longer exist, and one of which nobody on the team can explain.
This is the most under-scoped part of a modernization plan, and it is also the most tractable, because addons are the one dependency class you can simply decide to stop having. Here is the audit we run at the start of an assessment, and what we do with the results.
Build the inventory first
Start with facts, not opinions. For every ember-addon in your dependencies and devDependencies, record five things:
- Last release date, and last release compatible with your target Ember version. The npm registry answers both.
- Format: v1 or v2. Check the
ember-addon.versionfield in the addon's ownpackage.json.2means it ships plain ES modules and a standard bundler can read it. Absent or1means it participates in the Broccoli build. - What it actually does for you. One sentence. If nobody can write the sentence, that is a finding.
- How deeply you use it. Count real call sites, not the import in one file that a codemod left behind.
grepis enough. - Whether it reaches into internals. Anything that imports from
@ember/-internals, monkey-patches a framework class, or does interesting work intreeFor*hooks goes on a short list of its own.
That table is the whole deliverable. It usually surprises people twice: by how many addons are barely used, and by how few are genuinely load-bearing.
Sort into four buckets
Delete. The largest bucket in most mature apps. Addons with two call sites and a standard-library replacement, addons for features the product removed years ago, addons whose entire purpose was a browser bug or an ES5 gap. Removing one of these is an afternoon and it permanently reduces the surface area of every future upgrade. Do these first: they are cheap, they shrink the rest of the audit, and they build the case that the upgrade is moving.
Replace. An unmaintained addon with a maintained successor. The ecosystem consolidated over the last several years and many of the well-worn addons now have v2-format equivalents. Replacement is more work than deletion because behavior differs at the edges, so treat each one as its own small project with its own PR and its own test pass. Sequence by what is blocking the upgrade, not by what is annoying.
Vendor. For small abandoned addons you still need — three hundred lines that do something specific and correct — copy the source into your app under app/ or a local in-repo v2 addon, keep the license header, and delete the dependency. You now own the code, which sounds worse than it is: you already owned the risk, and now the code is readable in your own repo, debuggable in your own stack traces, and invisible to your dependency resolver. Vendoring is the answer for far more addons than teams expect.
Own. A handful of addons are large, abandoned, and structurally central — a component library, a data layer helper, an auth integration. For these, forking with intent is a legitimate choice: publish under your own scope or keep it in-repo, convert it to v2, and accept the maintenance line item. Make this decision explicitly, with a named owner, rather than by drifting into it.
One honest note on the fifth option: upstream. Sending a compatibility fix to an addon you depend on is good citizenship and sometimes the fastest path. It is not a plan, because it depends on a maintainer's availability. Open the PR, then proceed as if it will not land.
Converting a keeper to v2
If you own or fork an addon, converting it to the v2 format is what makes it survivable under Embroider and, later, under a Vite build. The shape of the work:
- Split the package. A v2 addon is a plain npm package that exports ES modules, plus — if it has generators, a
blueprint, or build-time behavior — a separate-test-appor companion package for the parts that legitimately need them.@embroider/addon-devprovides the rollup plugins for this. - Make imports explicit. v1 addons relied on the app's resolver merging their trees. v2 addons import what they use and declare their own dependencies. This is most of the diff, and most of the value: after it, everything in the addon is traceable by reading code.
- Publish the compiled output. Templates are precompiled or colocated in
.gjs; components are exported by name from an entry point. Consumers import them rather than relying on name resolution. - Move the test suite into the test app. Addon tests become an app's tests, which is simpler and closer to how the addon is actually used.
Budget more time than the file-moving suggests. The interesting failures are in blueprints, in addons that quietly injected initializers, and in anything that assumed it could see the host app's tree.
Where this fits in the sequence
Run the audit before committing to an upgrade estimate, because the addon table is the estimate. Do the deletions immediately — they are free progress and they reduce the work behind them. Then do replacements in whatever order unblocks the next LTS hop. Save the v2 conversions for when Embroider's static flags or a Vite migration actually require them; converting an addon that nothing is waiting on is work you can defer.
And re-run the inventory once a year. It takes an afternoon. An app whose addon list is reviewed annually stays upgradeable more or less by default; an app whose addon list is reviewed only when an upgrade stalls will stall again.
When the answer is smaller than a project
Sometimes the audit ends with a short list: delete four, replace one, leave the rest. That is a good outcome, and it does not need a consultancy to execute. If it ends with a list of a dozen abandoned addons wired into internals, that is worth talking about before you schedule the upgrade — that is the case where the addon work, not the Ember work, sets the timeline. Either way, the table is the thing to build first.