Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow null, undefined, if and unless for strict component arguments #922

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion packages/compat/src/resolver-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function makeResolverTransform(resolver: Resolver) {
for (let name of argumentsAreComponents) {
let attr = node.attributes.find((attr: any) => attr.name === '@' + name);
if (attr) {
handleComponentHelper(attr.value, resolver, filename, scopeStack, {
handleOptionalComponentHelper(attr.value, resolver, filename, scopeStack, {
componentName: node.tag,
argumentName: name,
});
Expand Down Expand Up @@ -316,3 +316,40 @@ function handleComponentHelper(

// resolver.unresolvableComponentArgument(componentName, argumentName, moduleName, param.loc);
}

function handleOptionalComponentHelper(
param: any,
resolver: Resolver,
moduleName: string,
scopeStack: ScopeStack,
impliedBecause?: { componentName: string; argumentName: string }
): void {
switch (param.type) {
case 'NullLiteral':
case 'UndefinedLiteral':
return;

case 'MustacheStatement':
case 'SubExpression':
if (param.path.type === 'NullLiteral' || param.path.type === 'UndefinedLiteral') {
return;
}

// Allow passing `{{ensure-safe-component @argument}}`
if (param.path.type === 'PathExpression' && param.path.original === 'ensure-safe-component') {
return;
}

// We specifically allow `{{if}}` and `{{unless}}` as long as both branches are valid
if (param.path.type === 'PathExpression' && (param.path.original === 'if' || param.path.original === 'unless')) {
// Ignore first one, which is the truthy check
param.params.slice(1).forEach((param: any) => {
handleOptionalComponentHelper(param, resolver, moduleName, scopeStack, impliedBecause);
});
break;
}

default:
handleComponentHelper(param, resolver, moduleName, scopeStack, impliedBecause);
}
}
220 changes: 220 additions & 0 deletions packages/compat/tests/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,192 @@ describe('compat-resolver', function () {
]);
});

test('allow to pass `null` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{null}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('allow to pass `undefined` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{undefined}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('allow to pass `if` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{if @navbarComponent (ensure-safe-component @navbar) null}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('allow to pass nested `if` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{if @navbarComponent (ensure-safe-component @navbar) (if @defaultNull null undefined)}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('allow to pass `unless` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{unless @navbar (ensure-safe-component @navbar) null}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('allow to pass `ensure-safe-component` as argument for component', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');

expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{ensure-safe-component @navbar}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
});

test('yieldsArguments causes warning to propagate up lexically, angle', function () {
let packageRules: PackageRules[] = [
{
Expand Down Expand Up @@ -1331,6 +1517,40 @@ describe('compat-resolver', function () {
);
});

test('handle warnings inside of `if` for components', function () {
let packageRules: PackageRules[] = [
{
package: 'the-test-package',
components: {
'<FormBuilder />': {
yieldsArguments: ['navbar'],
},
},
},
];
let findDependencies = configure({ staticComponents: true, packageRules });
givenFile('templates/components/form-builder.hbs');
expect(() => {
expect(
findDependencies(
'templates/components/x.hbs',
`
<FormBuilder @navbar={{if true this.unknown}} as |bar|>
{{component bar}}
</FormBuilder>
`
)
).toEqual([
{
path: './form-builder.hbs',
runtimeName: 'the-app/templates/components/form-builder',
},
]);
}).toThrow(
/argument "navbar" to component "FormBuilder" is treated as a component, but the value you're passing is dynamic: this\.unknown/
);
});

test('yieldsArguments causes warning to propagate up lexically, curl', function () {
let packageRules: PackageRules[] = [
{
Expand Down