diff --git a/__tests__/all.test.ts b/__tests__/all.test.ts
index 4e507fb..1483fa9 100644
--- a/__tests__/all.test.ts
+++ b/__tests__/all.test.ts
@@ -1766,6 +1766,358 @@ describe('htmlbars-inline-precompile', function () {
`);
});
+ describe('native rfc931', function () {
+ const color: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'simple-transform',
+ visitor: {
+ PathExpression(node) {
+ if (node.original === 'red') {
+ return env.syntax.builders.string('#ff0000');
+ }
+ return undefined;
+ },
+ },
+ };
+ };
+
+ it('emits template() for 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 }) });`
+ );
+
+ expect(transformed).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ export default template('', { scope: () => ({ HelloWorld }) });
+ `);
+ });
+
+ it('emits template() for 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('emits template() for class-backed component outside 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 eval form to 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 eval form to scope form for 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 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('emits template() for template-only 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 template('', { scope: () => ({ HelloWorld }) });`
+ );
+
+ expect(normalizeWireFormat(transformed)).equalCode(`
+ import { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ import { createTemplateFactory } from "@ember/template-factory";
+ export default template(createTemplateFactory(
+ /*
+
+ */
+ {
+ id: "",
+ block: "",
+ moduleName: "",
+ scope: () => [HelloWorld],
+ isStrictMode: true,
+ }
+ ));
+ `);
+ });
+
+ it('emits template() for 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 { template } from '@ember/template-compiler';
+ import HelloWorld from "somewhere";
+ import { createTemplateFactory } from "@ember/template-factory";
+ export default class {
+ static {
+ template(
+ createTemplateFactory(
+ /*
+
+ */
+ {
+ id: "",
+ block: "",
+ moduleName: "",
+ scope: () => [HelloWorld],
+ isStrictMode: true,
+ }
+ ),
+ { component: this }
+ );
+ }
+ }
+ `);
+ });
+
+ it('handles multiple templates with native rfc931 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('handles template-only component with no scope 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
");
+ `);
+ });
+ });
+
describe('scope', function () {
it('correctly handles scope function (non-block arrow function)', async function () {
let source = '';
diff --git a/src/plugin.ts b/src/plugin.ts
index 809c9f1..b419dd2 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,17 @@ export interface Options {
// Optional list of custom transforms to apply to the handlebars AST before
// compilation.
transforms?: ExtendedPluginBuilder[];
+
+ // Controls how RFC 931's template() API is handled.
+ //
+ // "polyfilled": The default. template() calls are converted to older APIs
+ // (precompileTemplate, setComponentTemplate, templateOnly) for runtimes
+ // that don't natively support template().
+ //
+ // "native": template() calls are kept as-is (with scope and eval
+ // normalization applied) for runtimes that natively support the
+ // template() API from @ember/template-compiler.
+ rfc931?: 'polyfilled' | 'native';
}
interface WireOpts {
@@ -100,6 +111,7 @@ interface WireOpts {
outputModuleOverrides: Record>;
enableLegacyModules: LegacyModuleName[];
transforms: ExtendedPluginBuilder[];
+ rfc931: 'polyfilled' | 'native';
}
interface HbsOpts {
@@ -107,6 +119,7 @@ interface HbsOpts {
outputModuleOverrides: Record>;
enableLegacyModules: LegacyModuleName[];
transforms: ExtendedPluginBuilder[];
+ rfc931: 'polyfilled' | 'native';
}
type NormalizedOpts = WireOpts | HbsOpts;
@@ -124,6 +137,7 @@ function normalizeOpts(options: Options): NormalizedOpts {
...options,
targetFormat: 'wire',
compiler,
+ rfc931: options.rfc931 ?? 'polyfilled',
};
} else {
return {
@@ -132,6 +146,7 @@ function normalizeOpts(options: Options): NormalizedOpts {
transforms: [],
...options,
targetFormat: 'hbs',
+ rfc931: options.rfc931 ?? 'polyfilled',
};
}
}
@@ -168,7 +183,10 @@ export function makePlugin(
},
exit(_path: NodePath, state: State) {
if (normalizedOpts.targetFormat === 'wire') {
- for (let { moduleName, export: exportName } of configuredModules(normalizedOpts)) {
+ for (let { moduleName, export: exportName, rfc931Support } of configuredModules(normalizedOpts)) {
+ if (rfc931Support === 'native') {
+ continue;
+ }
state.util.removeImport(moduleName, exportName);
}
}
@@ -347,7 +365,12 @@ function* configuredModules(normalizedOpts: NormalizedOpts) {
) {
continue;
}
- yield moduleConfig;
+ // Override rfc931Support mode based on user option
+ if (moduleConfig.rfc931Support) {
+ yield { ...moduleConfig, rfc931Support: normalizedOpts.rfc931 };
+ } else {
+ yield moduleConfig;
+ }
}
}
@@ -503,7 +526,20 @@ function insertCompiledTemplate(
let expression = t.callExpression(templateFactoryIdentifier, [templateExpression]);
- if (config.rfc931Support) {
+ if (config.rfc931Support === 'native') {
+ let templateCallArgs: t.Expression[] = [expression];
+ if (backingClass) {
+ templateCallArgs.push(
+ t.objectExpression([
+ t.objectProperty(t.identifier('component'), backingClass.node as t.Expression),
+ ])
+ );
+ }
+ expression = t.callExpression(
+ i.import('@ember/template-compiler', 'template'),
+ templateCallArgs
+ );
+ } else if (config.rfc931Support === 'polyfilled') {
expression = t.callExpression(i.import('@ember/component', 'setComponentTemplate'), [
expression,
backingClass?.node ??
@@ -516,6 +552,12 @@ function insertCompiledTemplate(
return expression;
});
+ // In native mode, the output contains a template() call which the visitor
+ // would try to re-process. Mark it as already handled.
+ if (config.rfc931Support === 'native') {
+ state.recursionGuard.add(target.node);
+ }
+
remapAndBindIdentifiers(target, babel, scopeLocals);
}
@@ -632,6 +674,19 @@ function updateCallForm(
//
target = target.get('arguments.0') as NodePath;
}
+
+ if (formatOptions.rfc931Support === 'native') {
+ // In native mode, keep the template() call form but normalize:
+ // - Remove eval property (eval form has been converted to scope via buildScopeLocals)
+ // - Keep component property as-is
+ // - Keep strict property as-is (don't rename to strictMode)
+ // - Don't replace callee with precompileTemplate
+ // - Don't wrap in setComponentTemplate
+ 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
// final AST.
@@ -725,6 +780,22 @@ function removeEvalAndScope(target: NodePath) {
}
}
+// Removes only the eval property from template() options, keeping component
+// and other properties. Used in native rfc931 mode where the template() call
+// is preserved.
+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.