refactor(compiler): create an internal util method to detect matching directives and pipes (#57466)

This commit adds an internal util method that allows to detect:

* which selectors are matching nodes in a template
* which pipes are present in a template

Both directives and pipes are split into 2 buckets: eagerly used and the ones that might potentially be defer-loaded.

PR Close #57466
This commit is contained in:
Andrew Kushnir
2024-08-18 23:56:27 -07:00
committed by Dylan Hunn
parent aff6729398
commit e75e61a637
2 changed files with 135 additions and 2 deletions
@@ -16,7 +16,7 @@ import {
SafePropertyRead,
ThisReceiver,
} from '../../expression_parser/ast';
import {SelectorMatcher} from '../../selector';
import {CssSelector, SelectorMatcher} from '../../selector';
import {
BoundAttribute,
BoundEvent,
@@ -59,8 +59,73 @@ import {
TargetBinder,
TemplateEntity,
} from './t2_api';
import {parseTemplate} from './template';
import {createCssSelectorFromNode} from './util';
/**
* Computes a difference between full list (first argument) and
* list of items that should be excluded from the full list (second
* argument).
*/
function diff(fullList: string[], itemsToExclude: string[]): string[] {
const exclude = new Set(itemsToExclude);
return fullList.filter((item) => !exclude.has(item));
}
/**
* Given a template string and a set of available directive selectors,
* computes a list of matching selectors and splits them into 2 buckets:
* (1) eagerly used in a template and (2) directives used only in defer
* blocks. Similarly, returns 2 lists of pipes (eager and deferrable).
*
* Note: deferrable directives selectors and pipes names used in `@defer`
* blocks are **candidates** and API caller should make sure that:
*
* * A Component where a given template is defined is standalone
* * Underlying dependency classes are also standalone
* * Dependency class symbols are not eagerly used in a TS file
* where a host component (that owns the template) is located
*/
export function findMatchingDirectivesAndPipes(template: string, directiveSelectors: string[]) {
const matcher = new SelectorMatcher<unknown[]>();
for (const selector of directiveSelectors) {
// Create a fake directive instance to account for the logic inside
// of the `R3TargetBinder` class (which invokes the `hasBindingPropertyName`
// function internally).
const fakeDirective = {
selector,
inputs: {
hasBindingPropertyName() {
return false;
},
},
outputs: {
hasBindingPropertyName() {
return false;
},
},
};
matcher.addSelectables(CssSelector.parse(selector), [fakeDirective]);
}
const parsedTemplate = parseTemplate(template, '' /* templateUrl */);
const binder = new R3TargetBinder(matcher as any);
const bound = binder.bind({template: parsedTemplate.nodes});
const eagerDirectiveSelectors = bound.getEagerlyUsedDirectives().map((dir) => dir.selector!);
const allMatchedDirectiveSelectors = bound.getUsedDirectives().map((dir) => dir.selector!);
const eagerPipes = bound.getEagerlyUsedPipes();
return {
directives: {
regular: eagerDirectiveSelectors,
deferCandidates: diff(allMatchedDirectiveSelectors, eagerDirectiveSelectors),
},
pipes: {
regular: eagerPipes,
deferCandidates: diff(bound.getUsedPipes(), eagerPipes),
},
};
}
/**
* Processes `Target`s with a given set of directives and performs a binding operation, which
* returns an object similar to TypeScript's `ts.TypeChecker` that contains knowledge about the
@@ -9,7 +9,7 @@
import * as e from '../../../src/expression_parser/ast';
import * as a from '../../../src/render3/r3_ast';
import {DirectiveMeta, InputOutputPropertySet} from '../../../src/render3/view/t2_api';
import {R3TargetBinder} from '../../../src/render3/view/t2_binder';
import {findMatchingDirectivesAndPipes, R3TargetBinder} from '../../../src/render3/view/t2_binder';
import {parseTemplate} from '../../../src/render3/view/template';
import {CssSelector, SelectorMatcher} from '../../../src/selector';
@@ -140,6 +140,74 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
return matcher;
}
describe('findMatchingDirectivesAndPipes', () => {
it('should match directives and detect pipes in eager and deferrable parts of a template', () => {
const template = `
<div [title]="abc | uppercase"></div>
@defer {
<my-defer-cmp [label]="abc | lowercase" />
} @placeholder {}
`;
const directiveSelectors = ['[title]', 'my-defer-cmp', 'not-matching'];
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
expect(result).toEqual({
directives: {
regular: ['[title]'],
deferCandidates: ['my-defer-cmp'],
},
pipes: {
regular: ['uppercase'],
deferCandidates: ['lowercase'],
},
});
});
it('should return empty directive list if no selectors are provided', () => {
const template = `
<div [title]="abc | uppercase"></div>
@defer {
<my-defer-cmp [label]="abc | lowercase" />
} @placeholder {}
`;
const directiveSelectors: string[] = [];
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
expect(result).toEqual({
directives: {
regular: [],
deferCandidates: [],
},
// Expect pipes to be present still.
pipes: {
regular: ['uppercase'],
deferCandidates: ['lowercase'],
},
});
});
it('should return a directive and a pipe only once (either as a regular or deferrable)', () => {
const template = `
<my-defer-cmp [label]="abc | lowercase" [title]="abc | uppercase" />
@defer {
<my-defer-cmp [label]="abc | lowercase" [title]="abc | uppercase" />
} @placeholder {}
`;
const directiveSelectors = ['[title]', 'my-defer-cmp', 'not-matching'];
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
expect(result).toEqual({
directives: {
regular: ['my-defer-cmp', '[title]'],
// All directives/components are used eagerly.
deferCandidates: [],
},
pipes: {
regular: ['lowercase', 'uppercase'],
// All pipes are used eagerly.
deferCandidates: [],
},
});
});
});
describe('t2 binding', () => {
it('should bind a simple template', () => {
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});