Skip to content

Preserve barrel side effects when rewriting deep imports#21352

Merged
NullVoxPopuli merged 1 commit intoemberjs:nvp/remove-barrel-importsfrom
NullVoxPopuli-ai-agent:nvp/fix-barrel-imports-lint
May 1, 2026
Merged

Preserve barrel side effects when rewriting deep imports#21352
NullVoxPopuli merged 1 commit intoemberjs:nvp/remove-barrel-importsfrom
NullVoxPopuli-ai-agent:nvp/fix-barrel-imports-lint

Conversation

@NullVoxPopuli-ai-agent
Copy link
Copy Markdown
Contributor

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Apr 30, 2026

Summary

Fixes the failing smoke tests on #21350 with a 5-line change in @glimmer/runtime. No lint-rule changes.

The failure (Cannot read properties of null (reading 'syscall') in AppendOpcodes.evaluate) was a real correctness issue — not a flake. After the lint autofix replaced barrel imports with deep imports, nothing imported @glimmer/runtime/index.ts anymore, and that index's import './lib/bootstrap'; was the only thing loading the opcode handler files. Each lib/compiled/opcodes/*.ts registers handlers via top-level APPEND_OPCODES.add(...), so when the bootstrap chain stopped, 28 of 90 handlers stayed unregistered — opcodes those handlers cover ({{on}}, dynamic helpers, etc.) hit null.syscall at evaluation.

Approach

Own the bootstrap from the file that actually consumes registered handlers — lib/vm/low-level.ts, where evaluateSyscall calls APPEND_OPCODES.evaluate(...). Any deep-import path that uses the VM (directly or transitively) now triggers bootstrap; consumers that don't need the VM don't pay for opcode handlers in their bundle. Removed the equivalent side-effect import from the package barrel since it's now redundant.

I considered solving this in the lint rule (detect side-effect imports in barrels and re-emit import 'BARREL'; to preserve them) — pushed up an earlier version of this PR doing exactly that. But adding bare side-effect imports of whole barrels back undoes the whole tree-shaking point of #21350. Better to put the trigger where the demand actually is.

Verified

I built dist/dev from each version and counted top-level APPEND_OPCODES.add(...) calls in chunks that are actually loaded:

Branch reachable adds trapped adds
origin/main 90 0
nvp/remove-barrel-imports 62 28 stuck in unused @glimmer/runtime/index.js
this PR 90 0

The 90 handlers in this PR's dist/dev are reachable from @ember/-internals/glimmer/index.jssetup-registry-...jsrender-...js (28 adds, plus a side-effect import './untouchable-this-...js' for the other 62).

Note for reviewers

Two other barrel-only side effects in this PR's diff that might be worth a follow-up:

  • @glimmer/validator/index.ts has a globalThis-based duplicate-package guard. It becomes dormant under deep imports — silent corruption is now possible if two copies ship. Move the registration check into a shared leaf module (e.g. lib/meta.ts) to keep it firing.
  • @ember/object/index.ts runs class EmberObject extends CoreObject.extend(Observable) at module load. This one's actually fine: import EmberObject from '@ember/object' is a default import, which the rule's kept mechanism preserves on the barrel. Anyone using EmberObject still triggers the extension.

Test plan

  • pnpm lint clean
  • pnpm build:js succeeds
  • All 90 APPEND_OPCODES.add(...) calls reachable from @ember/-internals/glimmer/index.js in dist/dev
  • pnpm test:node passes
  • pnpm type-check:internals reports same 10 pre-existing errors as the base branch
  • Smoke tests in CI

🤖 Generated with Claude Code

The smoke tests on emberjs#21350 fail with "Cannot read properties of null
(reading 'syscall')" in `AppendOpcodes.evaluate`. Cause: the lint
autofix replaced barrel imports of `@glimmer/runtime` with deep paths
(`@glimmer/runtime/lib/...`), so nothing pulled in the package index.
The index's `import './lib/bootstrap';` was the only thing loading the
opcode handler files (each `lib/compiled/opcodes/*.ts` registers via
top-level `APPEND_OPCODES.add(...)`), so 28 of 90 handlers stayed
unregistered.

Fix: own the bootstrap from the file that actually consumes registered
handlers — `lib/vm/low-level.ts`, where `evaluateSyscall` calls
`APPEND_OPCODES.evaluate(...)`. Any deep-import path that uses the VM
(directly or transitively) now triggers bootstrap; consumers that
don't need the VM don't pay for opcode handlers in their bundle.

Verified by counting top-level `APPEND_OPCODES.add(...)` calls in
`dist/dev`:

  origin/main:                   90 in a chunk loaded by everyone
  nvp/remove-barrel-imports:     62 loaded + 28 trapped in the unused
                                 `@glimmer/runtime/index.js`
  this commit:                   90 reachable from
                                 `@ember/-internals/glimmer/index.js`

Note for reviewers: the validator duplicate-package guard in
`@glimmer/validator/index.ts` and the `class EmberObject extends
CoreObject.extend(Observable)` extension in `@ember/object/index.ts`
are also barrel-only side effects. EmberObject's case is handled
naturally — `import EmberObject from '@ember/object'` is a default
import, which the lint rule's `kept` mechanism preserves on the
barrel. The validator guard becomes dormant under deep imports; if
that's a concern, move the registration check into a shared leaf
module (e.g. `lib/meta.ts`) in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent force-pushed the nvp/fix-barrel-imports-lint branch from e36a604 to fb0e583 Compare May 1, 2026 00:09
@NullVoxPopuli NullVoxPopuli merged commit cfe606e into emberjs:nvp/remove-barrel-imports May 1, 2026
40 checks passed
@NullVoxPopuli NullVoxPopuli deleted the nvp/fix-barrel-imports-lint branch May 1, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants