emberjs / emberjs/babel-plugin-ember-template-compilation
Bundler constant inlining breaks published scope objects: "Scope objects for `precompileTemplate` may only contain direct references to in-scope values … Found StringLiteral"
- Dominant language
- TypeScript
- Stars
- 8
- Forks
- 13
- PR merge metrics
- No merged PRs in 30d
Description
> [!IMPORTANT]
> made with claude
I'm not an expert in this codebase, and could be way off base, and I apologize if this is a waste of time / completely wrong
-------------------------
### What happens
A v2 addon publishes `targetFormat: 'hbs'` output — the `precompileTemplate` form, deferred for the consuming app to compile — containing:
```js
import { EMPTY_VALUE_PLACEHOLDER } from './renderers/const.js';
setComponentTemplate(precompileTemplate("{{EMPTY_VALUE_PLACEHOLDER}}", {
strictMode: true,
scope: () => ({ EMPTY_VALUE_PLACEHOLDER }),
}), templateOnly());
```
When the library is bundled with rolldown (directly or via tsdown), rolldown's **default** `optimization.inlineConst` (`mode: "smart"`) rewrites the published module to:
```js
scope: () => ({ EMPTY_VALUE_PLACEHOLDER: "—" })
```
and the consuming app's compile then throws:
```
Scope objects for `precompileTemplate` may only contain direct references to in-scope values,
e.g. { EMPTY_VALUE_PLACEHOLDER } or { EMPTY_VALUE_PLACEHOLDER: EMPTY_VALUE_PLACEHOLDER }. Found StringLiteral
```
(from `parseScope` in `src/expression-parser.ts`, which accepts only `Identifier` / `ThisExpression` values).
The failure mode is unpleasant: the library builds, type-checks and lints clean — the first signal is the consuming app failing to compile, one package away from the code that caused it.
### Why it inlines (bisected)
rolldown's smart mode isn't condition-positions-only (as its docs describe): it substitutes an imported constant **wherever the literal is shorter than the identifier**. Ember constants are exactly that shape — short display literals (`'—'`, `''`), numbers, booleans behind descriptive names. `EMPTY_VALUE_PLACEHOLDER = '—'` inlines; a synthetic `GREETING = "hello"` does not, which made this fun to reduce.
Minimal repro (rolldown 1.2.0, no plugins, no config):
```js
// constants.js
export const EMPTY = "—";
// entry.js — babel-plugin-ember-template-compilation's own output shape
import { setComponentTemplate } from "@ember/component";
import templateOnly from "@ember/component/template-only";
import { precompileTemplate } from "@ember/template-compilation";
import { EMPTY } from "./constants.js";
export const X = setComponentTemplate(precompileTemplate("{{EMPTY}}", {
strictMode: true,
scope: () => ({ EMPTY }),
}), templateOnly());
```
`rolldown({ input: './entry.js', external: /^@ember\// })` → `scope: () => ({ EMPTY: "—" })`.
### Why I think this deserves handling here (too)
The "direct references" invariant is private between this plugin's output and its input on the next compile — standard JS tooling has no way to know it must preserve it, and the rewrite is semantically legal JS. #14 was this same story with a different optimizer: rollup rewrote `{ FieldComponent }` to `{ FieldComponent: HeadlessFormFieldComponent }`, and #17/#19 loosened the parser to accept renamed identifier references. Constant inlining is the literal-value analog, and it now happens **by default** in a mainstream bundler.
A scope property whose value is a literal still carries everything the compiler needs — the template-visible name and the value it resolves to. Could `parseScope` accept literal values (synthesizing the binding, or emitting the literal into the compiled scope), the way renamed identifiers were accepted in 2.0.2? Failing that, extending the error message to name the likely cause ("a bundler optimization such as constant inlining may have rewritten this module") would save the next person the bisect.
### Mitigation we shipped meanwhile
`@nullvoxpopuli/ember-rolldown` now defaults `optimization.inlineConst` off ([NullVoxPopuli/ember.nvp#114](https://github.com/NullVoxPopuli/ember.nvp/pull/114)), so libraries built through its `ember()` plugin are safe. But any library bundling `precompileTemplate` modules with rolldown/tsdown directly — or through any future optimizer with the same idea — republishes this trap.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with parseScope in src/expression-parser.ts and reproduce the failure using the minimal rolldown 1.2.0 example from the issue. Trace how scope values are validated and decide whether the intended fix is literal-value handling or a more diagnostic error; done means the published scope no longer fails in the consuming app, with coverage for the reduced case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100