diff --git a/packages/compat/src/resolver-transform.ts b/packages/compat/src/resolver-transform.ts index 229cf2aa2..075675afe 100644 --- a/packages/compat/src/resolver-transform.ts +++ b/packages/compat/src/resolver-transform.ts @@ -78,6 +78,10 @@ export function makeResolverTransform(resolver: Resolver) { handleDynamicHelper(node.params[0], resolver, filename); return; } + if (node.path.original === 'modifier' && node.params.length > 0) { + handleDynamicModifier(node.params[0], resolver, filename); + return; + } resolver.resolveSubExpression(node.path.original, filename, node.path.loc); }, MustacheStatement(node: ASTv1.MustacheStatement) { @@ -341,3 +345,9 @@ function handleDynamicHelper(param: ASTv1.Node, resolver: Resolver, moduleName: resolver.resolveDynamicHelper({ type: 'other' }, moduleName, param.loc); } } + +function handleDynamicModifier(param: ASTv1.Expression, resolver: Resolver, moduleName: string): void { + if (param.type === 'StringLiteral') { + resolver.resolveDynamicModifier({ type: 'literal', path: param.value }, moduleName, param.loc); + } +} diff --git a/packages/compat/src/resolver.ts b/packages/compat/src/resolver.ts index 1633151d8..24c12f639 100644 --- a/packages/compat/src/resolver.ts +++ b/packages/compat/src/resolver.ts @@ -859,6 +859,43 @@ export default class CompatResolver implements Resolver { ); } } + + resolveDynamicModifier(modifier: ComponentLocator, from: string, loc: Loc): Resolution | null { + if (!this.staticModifiersEnabled) { + return null; + } + + if (modifier.type === 'literal') { + let modifierName = modifier.path; + if (builtInModifiers.includes(modifierName)) { + return null; + } + + let found = this.tryModifier(modifierName, from); + if (found) { + return this.add(found, from); + } + return this.add( + { + type: 'error', + message: `Missing modifier`, + detail: modifierName, + loc, + }, + from + ); + } else { + return this.add( + { + type: 'error', + message: 'Unsafe dynamic modifier', + detail: `cannot statically analyze this expression`, + loc, + }, + from + ); + } + } } function humanReadableFile(root: string, file: string) { diff --git a/packages/compat/tests/resolver.test.ts b/packages/compat/tests/resolver.test.ts index 8e435a4c9..e00672abb 100644 --- a/packages/compat/tests/resolver.test.ts +++ b/packages/compat/tests/resolver.test.ts @@ -531,6 +531,44 @@ describe('compat-resolver', function () { }, ]); }); + test('string literal passed to "modifier" keyword in content position', function () { + let findDependencies = configure({ + staticModifiers: true, + }); + givenFile('modifiers/add-listener.js'); + expect( + findDependencies( + 'templates/application.hbs', + `` + ) + ).toEqual([ + { + path: '../modifiers/add-listener.js', + runtimeName: 'the-app/modifiers/add-listener', + }, + ]); + }); + test('modifier currying using the "modifier" keyword', function () { + let findDependencies = configure({ staticModifiers: true }); + givenFile('modifiers/add-listener.js'); + expect( + findDependencies( + 'templates/application.hbs', + ` + {{#let (modifier "add-listener") as |addListener|}} + {{#let (modifier addListener "click") as |addClickListener|}} + + {{/let}} + {{/let}} + ` + ) + ).toEqual([ + { + path: '../modifiers/add-listener.js', + runtimeName: 'the-app/modifiers/add-listener', + }, + ]); + }); test('built-in components are ignored when used with the component helper', function () { let findDependencies = configure({ staticComponents: true, @@ -561,6 +599,20 @@ describe('compat-resolver', function () { ) ).toEqual([]); }); + test('built-in modifiers are ignored when used with the "modifier" keyword', function () { + let findDependencies = configure({ + staticModifiers: true, + }); + expect( + findDependencies( + 'templates/application.hbs', + ` + + + ` + ) + ).toEqual([]); + }); test('component helper with direct addon package reference', function () { let findDependencies = configure({ staticComponents: true, @@ -681,6 +733,25 @@ describe('compat-resolver', function () { }, ]); }); + test('string literal passed to "modifier" keyword in helper position', function () { + let findDependencies = configure({ staticModifiers: true }); + givenFile('modifiers/add-listener.js'); + expect( + findDependencies( + 'templates/application.hbs', + ` + {{#let (modifier "add-listener" "click") as |addClickListener|}} + + {{/let}} + ` + ) + ).toEqual([ + { + path: '../modifiers/add-listener.js', + runtimeName: 'the-app/modifiers/add-listener', + }, + ]); + }); test('string literal passed to component helper fails to resolve', function () { let findDependencies = configure({ staticComponents: true }); givenFile('components/my-thing.js'); @@ -694,6 +765,15 @@ describe('compat-resolver', function () { findDependencies('templates/application.hbs', `{{helper "hello-world"}}`); }).toThrow(new RegExp(`Missing helper: hello-world in templates/application.hbs`)); }); + test('string literal passed to "modifier" keyword fails to resolve', function () { + let findDependencies = configure({ staticModifiers: true }); + expect(() => { + findDependencies( + 'templates/application.hbs', + `` + ); + }).toThrow(new RegExp(`Missing modifier: add-listener in templates/application.hbs`)); + }); test('string literal passed to component helper fails to resolve when staticComponents is off', function () { let findDependencies = configure({ staticComponents: false }); givenFile('components/my-thing.js'); @@ -703,6 +783,16 @@ describe('compat-resolver', function () { let findDependencies = configure({ staticHelpers: false }); expect(findDependencies('templates/application.hbs', `{{helper "hello-world"}}`)).toEqual([]); }); + test('string literal passed to "modifier" keyword fails to resolve when staticModifiers is off', function () { + let findDependencies = configure({ staticModifiers: false }); + givenFile('modifiers/add-listener.js'); + expect( + findDependencies( + 'templates/application.hbs', + `` + ) + ).toEqual([]); + }); test('dynamic component helper error in content position', function () { let findDependencies = configure({ staticComponents: true }); givenFile('components/hello-world.js'); @@ -925,7 +1015,7 @@ describe('compat-resolver', function () { ]); }); test('ignores builtins', function () { - let findDependencies = configure({ staticHelpers: true, staticComponents: true }); + let findDependencies = configure({ staticHelpers: true, staticComponents: true, staticModifiers: true }); expect( findDependencies( 'templates/application.hbs', @@ -1778,6 +1868,11 @@ describe('compat-resolver', function () { ); }); + test('ignores any non-string-literal in "modifier" keyword', function () { + let findDependencies = configure({ staticModifiers: true }); + expect(findDependencies('templates/application.hbs', `
`)).toEqual([]); + }); + test('trusts inline ensure-safe-component helper', function () { let findDependencies = configure({ staticComponents: true }); expect(findDependencies('templates/application.hbs', `{{component (ensure-safe-component this.which) }}`)).toEqual(