diff --git a/README.md b/README.md
index f2b611d..4024c4d 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,19 @@ import hbs from 'ember-cli-htmlbars-inline-precompile';
import hbs from 'htmlbars-inline-precompile';
```
+It also understands the `template()` API from [RFC 931](https://rfcs.emberjs.com/id/0931-template-compiler-api/):
+
+```js
+import { template } from '@ember/template-compiler';
+```
+
+By default, `template()` is **polyfilled**: it is lowered to the older
+`precompileTemplate` / `setComponentTemplate` / `templateOnly` primitives so the
+output runs on Ember versions that don't natively understand `template()`. If
+you are targeting a runtime that implements `@ember/template-compiler` natively
+(Ember 6+), pass `rfc931: 'native'` to keep the `template()` call in the output
+instead. See the `rfc931` option below.
+
## Common Options
This package has both a Node implementation and a portable implementation that works in browsers.
@@ -86,6 +99,23 @@ interface Options {
// Optional list of custom transforms to apply to the handlebars AST before
// compilation. See `type Transform` below.
transforms?: Transform[];
+
+ // Controls how the RFC 931 `template()` API (imported from
+ // `@ember/template-compiler`) is emitted.
+ //
+ // "polyfilled": The default. `template()` calls are lowered to the older,
+ // widely-supported primitives (`precompileTemplate` / `createTemplateFactory`
+ // plus `setComponentTemplate` and `templateOnly`). Use this to target Ember
+ // versions that do not natively understand `template()`.
+ //
+ // "native": `template()` calls are 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. 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 this option has no
+ // effect there.
+ rfc931?: 'polyfilled' | 'native';
}
// The legal legacy module names. These are the only ones that are supported,
diff --git a/__tests__/all.test.ts b/__tests__/all.test.ts
index 4e507fb..7d12d00 100644
--- a/__tests__/all.test.ts
+++ b/__tests__/all.test.ts
@@ -1554,12 +1554,356 @@ describe('htmlbars-inline-precompile', function () {
export default class MyComponent {
}
setComponentTemplate(
- precompileTemplate('', { scope: () => ({ HelloWorld }), strictMode: true }),
+ precompileTemplate('', { scope: () => ({ HelloWorld }), strictMode: true }),
MyComponent
);
`);
});
+ describe('native rfc931', function () {
+ it('preserves template() for a template-only component in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ transforms: [color],
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default template('', { scope: () => ({ HelloWorld }) });`
+ );
+
+ // the template() call and its import are preserved; only the template
+ // body is transformed.
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default template('', { scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('preserves template() for a class-backed component in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ transforms: [color],
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default class MyComponent {
+ static {
+ template('', { component: this, scope: () => ({ HelloWorld }) });
+ }
+ }
+ `
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default class MyComponent {
+ static {
+ template('', { component: this, scope: () => ({ HelloWorld }) });
+ }
+ }
+ `);
+ });
+
+ it('preserves template() for a class-backed component declared outside the class in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ transforms: [color],
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default class MyComponent {
+ }
+ template('', { component: MyComponent, scope: () => ({ HelloWorld }) });
+ `
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default class MyComponent {
+ }
+ template('', { component: MyComponent, scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('converts the implicit eval form into the explicit scope form in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default template('', { eval() { return eval(arguments[0]); } });`
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default template("", { scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('converts the implicit eval form into the explicit scope form for a class-backed component in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `
+ import { template } from '@ember/template-compiler';
+ import Component from '@glimmer/component';
+ import HelloWorld from 'somewhere';
+ export default class MyComponent extends Component {
+ static {
+ template('', { component: this, eval() { return eval(arguments[0]); } });
+ }
+ }
+ `
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import Component from "@glimmer/component";
+ import HelloWorld from "somewhere";
+ export default class MyComponent extends Component {
+ static {
+ template("", { component: this, scope: () => ({ HelloWorld }) });
+ }
+ }
+ `);
+ });
+
+ it("preserves the user's strict option on template() in hbs format", async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default template('', { strict: false, scope: () => ({ HelloWorld }) });`
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default template('', { strict: false, scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('handles multiple templates in a module in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `
+ import { template } from "@ember/template-compiler";
+ import Component from '@glimmer/component';
+ export default class Test extends Component {
+ foo = 1;
+ static{
+ template("", {
+ component: this,
+ eval () {
+ return eval(arguments[0]);
+ }
+ });
+ }
+ }
+ const Icon = template("Icon", {
+ eval () {
+ return eval(arguments[0]);
+ }
+ });
+ `
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from "@ember/template-compiler";
+ import Component from "@glimmer/component";
+ export default class Test extends Component {
+ foo = 1;
+ static {
+ template("", {
+ component: this,
+ scope: () => ({
+ Icon,
+ }),
+ });
+ }
+ }
+ const Icon = template("Icon", {});
+ `);
+ });
+
+ it('preserves a template-only component with no options in hbs format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `import { template } from '@ember/template-compiler';
+ export default template('
Hello World
');`
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ export default template("Hello World
");
+ `);
+ });
+
+ it('falls back to the standard wire format for a template-only component in wire format', async function () {
+ // In wire format the template is fully compiled, which already runs on
+ // native runtimes, so rfc931: 'native' produces the same output as the
+ // default (polyfilled) wire output.
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'wire',
+ transforms: [],
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default template('', { scope: () => ({ HelloWorld }) });`
+ );
+
+ expect(normalizeWireFormat(transformed)).equalCode(`
+ import HelloWorld from "somewhere";
+ import { setComponentTemplate } from "@ember/component";
+ import { createTemplateFactory } from "@ember/template-factory";
+ import templateOnly from "@ember/component/template-only";
+ export default setComponentTemplate(createTemplateFactory(
+ /*
+
+ */
+ {
+ id: "",
+ block: "",
+ moduleName: "",
+ scope: () => [HelloWorld],
+ isStrictMode: true,
+ }
+ ), templateOnly());
+ `);
+ });
+
+ it('falls back to the standard wire format for a class-backed component in wire format', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'wire',
+ transforms: [],
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let transformed = await transform(
+ `
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from 'somewhere';
+ export default class {
+ static {
+ template('', { component: this, scope: () => ({ HelloWorld }) });
+ }
+ }
+ `
+ );
+
+ expect(normalizeWireFormat(transformed)).equalCode(`
+ import HelloWorld from "somewhere";
+ import { setComponentTemplate } from "@ember/component";
+ import { createTemplateFactory } from "@ember/template-factory";
+ export default class {
+ static {
+ setComponentTemplate(
+ createTemplateFactory(
+ /*
+
+ */
+ {
+ id: "",
+ block: "",
+ moduleName: "",
+ scope: () => [HelloWorld],
+ isStrictMode: true,
+ }
+ ),
+ this
+ );
+ }
+ }
+ `);
+ });
+ });
+
it('cleans up leftover imports when there is more than one template', async function () {
plugins = [
[
@@ -2517,9 +2861,125 @@ describe('htmlbars-inline-precompile', function () {
}
`);
});
+
+ it('preserves template() for expression form with rfc931: native and hbs target', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let p = new Preprocessor();
+
+ const { code: preTransformed } = p.process(
+ `import HelloWorld from 'somewhere';
+ const MyComponent = ;
+ `
+ );
+
+ let transformed = await transform(preTransformed);
+
+ // the gjs is preprocessed by content-tag into the implicit eval form,
+ // then this plugin keeps template() and converts eval -> scope. content-tag
+ // aliases the import to a unique name (`template as template_`) which
+ // we preserve; normalize it back to `template` for a stable assertion.
+ expect(normalizeContentTagImport(transformed)).equalCode(`
+ import { template } from "@ember/template-compiler";
+ import HelloWorld from "somewhere";
+ const MyComponent = template('', { scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('preserves template() for class member form with rfc931: native and hbs target', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'hbs',
+ rfc931: 'native',
+ },
+ ],
+ ];
+
+ let p = new Preprocessor();
+
+ const { code: preTransformed } = p.process(
+ `import HelloWorld from 'somewhere';
+ export default class {
+
+ }
+ `
+ );
+
+ let transformed = await transform(preTransformed);
+
+ expect(normalizeContentTagImport(transformed)).equalCode(`
+ import { template } from "@ember/template-compiler";
+ import HelloWorld from "somewhere";
+ export default class {
+ static {
+ template('', { component: this, scope: () => ({ HelloWorld }) });
+ }
+ }
+ `);
+ });
+
+ it('compiles to wire format with targetFormat: wire', async function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ targetFormat: 'wire',
+ },
+ ],
+ ];
+
+ let p = new Preprocessor();
+
+ const { code: preTransformed } = p.process(
+ `import HelloWorld from 'somewhere';
+ const MyComponent = ;
+ `
+ );
+
+ let transformed = await transform(preTransformed);
+
+ // content-tag emits the implicit eval form; the default (polyfilled) wire
+ // path fully compiles it and drops the @ember/template-compiler import.
+ expect(normalizeWireFormat(transformed)).equalCode(`
+ import HelloWorld from "somewhere";
+ import { setComponentTemplate } from "@ember/component";
+ import { createTemplateFactory } from "@ember/template-factory";
+ import templateOnly from "@ember/component/template-only";
+ const MyComponent = setComponentTemplate(createTemplateFactory(
+ /*
+
+ */
+ {
+ id: "",
+ block: "",
+ moduleName: "",
+ scope: () => [HelloWorld],
+ isStrictMode: true,
+ }
+ ), templateOnly());
+ `);
+ });
});
});
+// content-tag aliases its `template` import to a unique, hashed local name
+// (e.g. `template as template_abc123`). When we preserve the template() call
+// (rfc931: 'native'), that alias survives into the output. Normalize it back to
+// `template` so assertions don't depend on the specific hash.
+function normalizeContentTagImport(src: string): string {
+ return src.replace(/template_[0-9a-f]+/g, 'template');
+}
+
// This takes out parts of ember's wire format that aren't our job and shouldn't
// break our tests if they change.
function normalizeWireFormat(src: string): string {
diff --git a/src/plugin.ts b/src/plugin.ts
index 809c9f1..841d152 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -18,7 +18,7 @@ interface ModuleConfig {
export: string;
allowTemplateLiteral?: true;
enableScope?: true;
- rfc931Support?: 'polyfilled';
+ rfc931Support?: 'polyfilled' | 'native';
}
const INLINE_PRECOMPILE_MODULES: ModuleConfig[] = [
@@ -92,6 +92,23 @@ export interface Options {
// Optional list of custom transforms to apply to the handlebars AST before
// compilation.
transforms?: ExtendedPluginBuilder[];
+
+ // Controls how the RFC 931 `template()` API (imported from
+ // `@ember/template-compiler`) is emitted.
+ //
+ // "polyfilled": The default. `template()` calls are lowered to the older,
+ // widely-supported primitives (`precompileTemplate` / `createTemplateFactory`
+ // plus `setComponentTemplate` and `templateOnly`). Use this to target Ember
+ // versions that do not natively understand `template()`.
+ //
+ // "native": `template()` calls are 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. 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 this option has no
+ // effect there.
+ rfc931?: 'polyfilled' | 'native';
}
interface WireOpts {
@@ -100,6 +117,7 @@ interface WireOpts {
outputModuleOverrides: Record>;
enableLegacyModules: LegacyModuleName[];
transforms: ExtendedPluginBuilder[];
+ rfc931: 'polyfilled' | 'native';
}
interface HbsOpts {
@@ -107,6 +125,7 @@ interface HbsOpts {
outputModuleOverrides: Record>;
enableLegacyModules: LegacyModuleName[];
transforms: ExtendedPluginBuilder[];
+ rfc931: 'polyfilled' | 'native';
}
type NormalizedOpts = WireOpts | HbsOpts;
@@ -124,6 +143,7 @@ function normalizeOpts(options: Options): NormalizedOpts {
...options,
targetFormat: 'wire',
compiler,
+ rfc931: options.rfc931 ?? 'polyfilled',
};
} else {
return {
@@ -132,6 +152,7 @@ function normalizeOpts(options: Options): NormalizedOpts {
transforms: [],
...options,
targetFormat: 'hbs',
+ rfc931: options.rfc931 ?? 'polyfilled',
};
}
}
@@ -347,7 +368,14 @@ function* configuredModules(normalizedOpts: NormalizedOpts) {
) {
continue;
}
- yield moduleConfig;
+ if (moduleConfig.rfc931Support) {
+ // The `@ember/template-compiler` `template()` config defaults to
+ // 'polyfilled'; the user's `rfc931` option decides whether we lower it to
+ // the legacy primitives or keep it native.
+ yield { ...moduleConfig, rfc931Support: normalizedOpts.rfc931 };
+ } else {
+ yield moduleConfig;
+ }
}
}
@@ -631,6 +659,15 @@ function updateCallForm(
// precompileTemplate call for the final updateScope below.
//
target = target.get('arguments.0') as NodePath;
+ } else if (formatOptions.rfc931Support === 'native') {
+ // Native mode: the runtime understands `template()` directly, so we keep
+ // the call (and its callee import) as-is. We only drop the implicit `eval`
+ // option, because its role is taken over by the explicit `scope` that
+ // updateScope appends below. The user's `component` and `strict` options
+ // are preserved untouched.
+ removeEval(target);
+ target.node.arguments = target.node.arguments.slice(0, 2);
+ state.recursionGuard.add(target.node);
}
// We deliberately do updateScope at the end so that when it updates
// references, those references will point to the accurate paths in the
@@ -725,6 +762,23 @@ function removeEvalAndScope(target: NodePath) {
}
}
+// Removes only the `eval` option from a template() call, leaving `component`,
+// `strict`, and any other options in place. Used in native rfc931 mode, where
+// the template() call is preserved and the implicit eval form is converted into
+// the explicit scope form (the scope is re-added by updateScope).
+function removeEval(target: NodePath) {
+ let secondArg = target.get('arguments.1') as NodePath | undefined;
+ if (secondArg) {
+ let evalProp = secondArg.get('properties').find((p) => {
+ let key = p.get('key') as NodePath;
+ return key.isIdentifier() && key.node.name === 'eval';
+ });
+ if (evalProp) {
+ evalProp.remove();
+ }
+ }
+}
+
// Given a call to template(), convert its "strict" argument into
// precompileTemplate's "strictMode" argument. They differ in name and default
// value.