Skip to content

Add rfc931: 'native' option to preserve template() in output#115

Open
NullVoxPopuli-ai-agent wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:native-rfc931-output
Open

Add rfc931: 'native' option to preserve template() in output#115
NullVoxPopuli-ai-agent wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:native-rfc931-output

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented May 29, 2026

Copy link
Copy Markdown

Summary

Adds an rfc931 option that controls how the RFC 931 template() API (imported from @ember/template-compiler) is emitted:

  • "polyfilled" (default) — existing behavior. template() is lowered to the older, widely-supported primitives (precompileTemplate / createTemplateFactory plus setComponentTemplate and templateOnly) so the output runs on Ember versions that don't natively understand template().
  • "native" — the template() call is preserved in the output, for runtimes that implement @ember/template-compiler natively (Ember 6+). AST transforms still run, and the implicit eval form is normalized into the explicit scope form.
// input
import { template } from '@ember/template-compiler';
import HelloWorld from 'somewhere';
export default template('<HelloWorld />', { eval() { return eval(arguments[0]); } });

// output with { targetFormat: 'hbs', rfc931: 'native' }
import { template } from '@ember/template-compiler';
import HelloWorld from 'somewhere';
export default template('<HelloWorld />', { scope: () => ({ HelloWorld }) });

Findings / design notes

I read through RFC 931, the discussion thread, the existing plugin, the in-progress draft #109, and ember-source's native implementation of @ember/template-compiler. Key conclusions that shaped the design:

  1. template() only ever takes a string. ember-source's native template(templateString, options) calls glimmerPrecompile(templateString, ...) at runtime — it never accepts a precompiled wire factory. So "preserving template()" is only meaningful when the body stays as an HBS string, i.e. targetFormat: 'hbs'.

  2. targetFormat: 'wire' needs no special native handling. The standard wire output — setComponentTemplate(createTemplateFactory(...), component ?? templateOnly()) — is exactly what native template() produces internally, and it already runs on native runtimes. Wrapping a wire factory back into a template() call would not execute on Ember (it expects a string). So rfc931: 'native' only changes hbs-target output; in wire it falls back to the normal compiled form.

  3. eval → scope normalization is the useful work in native mode. The implicit (eval) form is the intermediate emitted by content-tag; converting it to the explicit scope form (via the existing implicit ScopeLocals machinery + updateScope) produces the canonical, statically-analyzable, publishable form. component and the user's strict option are preserved untouched; eval is dropped.

Implementation

  • The @ember/template-compiler module config's rfc931Support now resolves to the user's rfc931 choice ('polyfilled' | 'native') in configuredModules.
  • updateCallForm (the hbs path) gains a native branch that keeps the template() callee and its import, drops eval, and lets updateScope append the explicit scope.
  • wire path and import cleanup are unchanged.

Tests

Adds a native rfc931 suite covering: template-only and class-backed components (static block + out-of-class component:), eval→scope conversion, strict preservation, multiple templates per module, no-options template-only, and the wire fallback. Full suite: 137 passing, lint clean.

🤖 Generated with Claude Code

NullVoxPopuli and others added 3 commits May 29, 2026 09:13
Adds an `rfc931` option controlling how the RFC 931 `template()` API
(from `@ember/template-compiler`) is emitted:

- "polyfilled" (default): existing behavior — lower `template()` to
  `precompileTemplate`/`createTemplateFactory` + `setComponentTemplate`
  + `templateOnly`, for runtimes without native `template()` support.
- "native": preserve the `template()` call for runtimes that implement
  `@ember/template-compiler` natively (Ember 6+). AST transforms still
  run, and the implicit `eval` form is normalized into the explicit
  `scope` form.

This only affects `targetFormat: 'hbs'`. With `targetFormat: 'wire'` the
template is fully compiled to the standard wire format, which already
runs on native runtimes, so `native` has no effect there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the real gjs/gts path: content-tag preprocesses <template> into
the implicit eval form, then rfc931: 'native' + targetFormat: 'hbs'
keeps the template() call and converts eval -> scope. Asserts both the
expression form and the class-member form.

content-tag aliases its import (`template as template_<hash>`); native
mode preserves that alias, so a normalizeContentTagImport helper maps it
back to `template` for stable assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the last coverage gap: real <template> gjs is preprocessed by
content-tag into the implicit eval form, then compiled all the way to
the wire format with targetFormat: 'wire'. Confirms eval -> wire
end-to-end (existing wire tests only exercised the explicit scope form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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