diff --git a/packages/compat/src/resolver-transform.ts b/packages/compat/src/resolver-transform.ts index 7c469c33b..7d160333c 100644 --- a/packages/compat/src/resolver-transform.ts +++ b/packages/compat/src/resolver-transform.ts @@ -43,51 +43,78 @@ export interface Options { appRoot: string; } -export const builtInKeywords = [ - '-get-dynamic-var', - '-in-element', - '-with-dynamic-vars', - 'action', - 'array', - 'component', - 'concat', - 'debugger', - 'each-in', - 'each', - 'fn', - 'get', - 'has-block-params', - 'has-block', - 'hasBlock', - 'hasBlockParams', - 'hash', - 'helper', - 'if', - 'in-element', - 'input', - 'let', - 'link-to', - 'loc', - 'log', - 'modifier', - 'mount', - 'mut', - 'on', - 'outlet', - 'partial', - 'query-params', - 'readonly', - 'textarea', - 'unbound', - 'unique-id', - 'unless', - 'with', - 'yield', -]; +type BuiltIn = { + importableHelper?: [string, string]; + importableComponent?: [string, string]; + importableModifier?: [string, string]; +}; + +const builtInKeywords: Record = { + '-get-dynamic-var': {}, + '-in-element': {}, + '-with-dynamic-vars': {}, + action: {}, + array: { + importableHelper: ['array', '@ember/helper'], + }, + component: {}, + concat: { + importableHelper: ['concat', '@ember/helper'], + }, + debugger: {}, + 'each-in': {}, + each: {}, + fn: { + importableHelper: ['fn', '@ember/helper'], + }, + get: { + importableHelper: ['get', '@ember/helper'], + }, + 'has-block-params': {}, + 'has-block': {}, + hasBlock: {}, + hasBlockParams: {}, + hash: { + importableHelper: ['hash', '@ember/helper'], + }, + helper: {}, + if: {}, + 'in-element': {}, + input: { + importableComponent: ['Input', '@ember/component'], + }, + let: {}, + 'link-to': { + importableComponent: ['LinkTo', '@ember/routing'], + }, + loc: {}, + log: {}, + modifier: {}, + mount: {}, + mut: {}, + on: { + importableModifier: ['on', '@ember/modifier'], + }, + outlet: {}, + partial: {}, + 'query-params': {}, + readonly: {}, + textarea: { + importableComponent: ['Textarea', '@ember/component'], + }, + unbound: {}, + 'unique-id': { + importableHelper: ['uniqueId', '@ember/helper'], + }, + unless: {}, + with: {}, + yield: {}, +}; interface ComponentResolution { type: 'component'; specifier: string; + importedName: string; yieldsComponents: Required['yieldsSafeComponents']; yieldsArguments: Required['yieldsArguments']; argumentsAreComponents: string[]; @@ -98,11 +125,13 @@ type HelperResolution = { type: 'helper'; nameHint: string; specifier: string; + importedName: string; }; type ModifierResolution = { type: 'modifier'; specifier: string; + importedName: string; nameHint: string; }; @@ -157,7 +186,7 @@ class TemplateResolver implements ASTPlugin { case 'component': case 'modifier': case 'helper': { - let name = this.env.meta.jsutils.bindImport(resolution.specifier, 'default', parentPath, { + let name = this.env.meta.jsutils.bindImport(resolution.specifier, resolution.importedName, parentPath, { nameHint: resolution.nameHint, }); setter(parentPath.node, this.env.syntax.builders.path(name)); @@ -370,7 +399,22 @@ class TemplateResolver implements ASTPlugin { return null; } - if (builtInKeywords.includes(name)) { + const builtIn = builtInKeywords[name]; + + if (builtIn?.importableComponent) { + let [importedName, specifier] = builtIn.importableComponent; + return { + type: 'component', + specifier, + importedName, + yieldsComponents: [], + yieldsArguments: [], + argumentsAreComponents: [], + nameHint: importedName, + }; + } + + if (builtIn) { return null; } if (this.isIgnoredComponent(name)) { @@ -381,6 +425,7 @@ class TemplateResolver implements ASTPlugin { return { type: 'component', specifier: `#embroider_compat/components/${name}`, + importedName: 'default', yieldsComponents: componentRules ? componentRules.yieldsSafeComponents : [], yieldsArguments: componentRules ? componentRules.yieldsArguments : [], argumentsAreComponents: componentRules ? componentRules.argumentsAreComponents : [], @@ -437,13 +482,26 @@ class TemplateResolver implements ASTPlugin { // globally-named helpers. It throws an error. So it's fine for us to // prioritize the builtIns here without bothering to resolve a user helper // of the same name. - if (builtInKeywords.includes(path)) { + const builtIn = builtInKeywords[path]; + + if (builtIn?.importableHelper) { + let [importedName, specifier] = builtIn.importableHelper; + return { + type: 'helper', + specifier, + importedName, + nameHint: importedName, + }; + } + + if (builtIn) { return null; } return { type: 'helper', specifier: `#embroider_compat/helpers/${path}`, + importedName: 'default', nameHint: this.nameHint(path), }; } @@ -505,11 +563,36 @@ class TemplateResolver implements ASTPlugin { */ // first, bail out on all the stuff we can obviously ignore - if ( - (!this.staticHelpersEnabled && !this.staticComponentsEnabled) || - builtInKeywords.includes(path) || - this.isIgnoredComponent(path) - ) { + if ((!this.staticHelpersEnabled && !this.staticComponentsEnabled) || this.isIgnoredComponent(path)) { + return null; + } + + let builtIn = builtInKeywords[path]; + + if (builtIn?.importableComponent) { + let [importedName, specifier] = builtIn.importableComponent; + return { + type: 'component', + specifier, + importedName, + yieldsComponents: [], + yieldsArguments: [], + argumentsAreComponents: [], + nameHint: importedName, + }; + } + + if (builtIn?.importableHelper) { + let [importedName, specifier] = builtIn.importableHelper; + return { + type: 'helper', + specifier, + importedName, + nameHint: importedName, + }; + } + + if (builtIn) { return null; } @@ -560,6 +643,7 @@ class TemplateResolver implements ASTPlugin { return { type: 'component', specifier: `#embroider_compat/ambiguous/${path}`, + importedName: 'default', yieldsComponents: componentRules ? componentRules.yieldsSafeComponents : [], yieldsArguments: componentRules ? componentRules.yieldsArguments : [], argumentsAreComponents: componentRules ? componentRules.argumentsAreComponents : [], @@ -571,13 +655,26 @@ class TemplateResolver implements ASTPlugin { if (!this.staticModifiersEnabled) { return null; } - if (builtInKeywords.includes(path)) { + + const builtIn = builtInKeywords[path]; + if (builtIn?.importableModifier) { + let [importedName, specifier] = builtIn.importableModifier; + return { + type: 'modifier', + specifier, + importedName, + nameHint: importedName, + }; + } + + if (builtIn) { return null; } return { type: 'modifier', specifier: `#embroider_compat/modifiers/${path}`, + importedName: 'default', nameHint: this.nameHint(path), }; } diff --git a/tests/scenarios/compat-resolver-test.ts b/tests/scenarios/compat-resolver-test.ts index d3ed4df4c..92a64a1e7 100644 --- a/tests/scenarios/compat-resolver-test.ts +++ b/tests/scenarios/compat-resolver-test.ts @@ -1062,28 +1062,80 @@ Scenarios.fromProject(() => new Project()) `); }); - test('built-in components are ignored when used with the component helper', async function () { + test('built-in components are imported when used with the component helper', async function () { givenFiles({ 'templates/application.hbs': `{{component "input"}}{{component "link-to"}}{{component "textarea"}}`, }); await configure({ staticComponents: true }); expectTranspiled('templates/application.hbs').equalsCode(` import { precompileTemplate } from "@ember/template-compilation"; - export default precompileTemplate("{{component \\"input\\"}}{{component \\"link-to\\"}}{{component \\"textarea\\"}}", { - moduleName: "my-app/templates/application.hbs" + import { Input, Textarea } from "@ember/component"; + import { LinkTo } from "@ember/routing"; + export default precompileTemplate("{{component Input}}{{component LinkTo}}{{component Textarea}}", { + moduleName: "my-app/templates/application.hbs", + scope: () => ({ + Input, + LinkTo, + Textarea + }), }); `); }); - test('built-in helpers are ignored when used with the helper keyword', async function () { + test('built-in components are imported when used directly', async function () { + givenFiles({ + 'templates/application.hbs': `