Skip to content
Draft
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
6 changes: 6 additions & 0 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export interface Component extends Directive {
animations?: any[];
changeDetection?: ChangeDetectionStrategy;
encapsulation?: ViewEncapsulation;
foreignImports?: ForeignComponent<any>[];
imports?: (Type<any> | ReadonlyArray<any>)[];
preserveWhitespaces?: boolean;
schemas?: SchemaMetadata[];
Expand Down Expand Up @@ -744,6 +745,11 @@ export interface FactorySansProvider {
useFactory: Function;
}

// @public
export type ForeignComponent<T> = T & {
ɵrender: Function;
};

// @public
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
SelectorlessMatcher,
MatchSource,
TypeCheckId,
ForeignComponentMeta,
} from '@angular/compiler';
import ts from 'typescript';

Expand Down Expand Up @@ -190,6 +191,7 @@ import {
legacyAnimationTriggerResolver,
collectLegacyAnimationNames,
validateAndFlattenComponentImports,
validateAndFlattenForeignImports,
} from './util';
import {getTemplateDiagnostics} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
Expand Down Expand Up @@ -239,6 +241,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
constructor(
private reflector: ReflectionHost,
private evaluator: PartialEvaluator,
private checker: ts.TypeChecker,
private metaRegistry: MetadataRegistry,
private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader,
Expand Down Expand Up @@ -599,16 +602,22 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
}

let resolvedImports: Reference<ClassDeclaration>[] | null = null;
let resolvedForeignImports: Reference<ClassDeclaration>[] | null = null;
let resolvedDeferredImports: Reference<ClassDeclaration>[] | null = null;

let rawImports: ts.Expression | null = component.get('imports') ?? null;
let rawDeferredImports: ts.Expression | null = component.get('deferredImports') ?? null;
let rawForeignImports: ts.Expression | null = component.get('foreignImports') ?? null;

if ((rawImports || rawDeferredImports) && !metadata.isStandalone) {
if ((rawImports || rawDeferredImports || rawForeignImports) && !metadata.isStandalone) {
if (diagnostics === undefined) {
diagnostics = [];
}
const importsField = rawImports ? 'imports' : 'deferredImports';
const importsField = rawImports
? 'imports'
: rawDeferredImports
? 'deferredImports'
: 'foreignImports';
diagnostics.push(
makeDiagnostic(
ErrorCode.COMPONENT_NOT_STANDALONE,
Expand All @@ -627,7 +636,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
isPoisoned = true;
} else if (
this.compilationMode !== CompilationMode.LOCAL &&
(rawImports || rawDeferredImports)
(rawImports || rawDeferredImports || rawForeignImports)
) {
const importResolvers = combineResolvers([
createModuleWithProvidersResolver(this.reflector, this.isCore),
Expand Down Expand Up @@ -662,6 +671,17 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
rawDeferredImports = expr;
}

if (rawForeignImports) {
const expr = rawForeignImports;
const imported = this.evaluator.evaluate(expr, importResolvers);
const {foreignImports: foreign, diagnostics} = validateAndFlattenForeignImports(
imported,
expr,
);
importDiagnostics.push(...diagnostics);
resolvedForeignImports = foreign;
}

if (importDiagnostics.length > 0) {
isPoisoned = true;
if (diagnostics === undefined) {
Expand Down Expand Up @@ -1025,6 +1045,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
legacyAnimationTriggerNames: legacyAnimationTriggerNames,
rawImports,
resolvedImports,
resolvedForeignImports,
rawDeferredImports,
resolvedDeferredImports,
explicitlyDeferredTypes,
Expand Down Expand Up @@ -1076,6 +1097,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: analysis.resolvedImports,
foreignImports: analysis.resolvedForeignImports,
rawImports: analysis.rawImports,
deferredImports: analysis.resolvedDeferredImports,
animationTriggerNames: analysis.legacyAnimationTriggerNames,
Expand Down Expand Up @@ -1757,8 +1779,22 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
}
}

// Extract foreign component names and create a matcher.
let foreignMatcher: SelectorlessMatcher<ForeignComponentMeta> | null = null;
if (analysis.resolvedForeignImports !== null && analysis.resolvedForeignImports.length > 0) {
const registry = new Map<string, ForeignComponentMeta[]>();
for (const ref of analysis.resolvedForeignImports) {
const name = ref.node.name.getText();
registry.set(name, [{name, ref}]);
}
foreignMatcher = new SelectorlessMatcher(registry);
}

// Set up the R3TargetBinder.
const binder = new R3TargetBinder(createMatcherFromScope(scope, this.hostDirectivesResolver));
const binder = new R3TargetBinder(
createMatcherFromScope(scope, this.hostDirectivesResolver),
foreignMatcher,
);
let allDependencies = dependencies;
let deferBlockBinder = binder;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface ComponentAnalysisData {

rawImports: ts.Expression | null;
resolvedImports: Reference<ClassDeclaration>[] | null;
resolvedForeignImports: Reference<ClassDeclaration>[] | null;
rawDeferredImports: ts.Expression | null;
resolvedDeferredImports: Reference<ClassDeclaration>[] | null;

Expand Down
107 changes: 88 additions & 19 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
ResolvedValueMap,
SyntheticValue,
} from '../../../partial_evaluator';
import {ClassDeclaration, isNamedClassDeclaration} from '../../../reflection';
import {
ClassDeclaration,
isNamedClassDeclaration,
isNamedFunctionDeclaration,
} from '../../../reflection';
import {createValueHasWrongTypeError, getOriginNodeForDiagnostics} from '../../common';

/**
Expand Down Expand Up @@ -143,24 +147,12 @@ export function validateAndFlattenComponentImports(
),
);
} else {
let diagnosticNode: ts.Node;
let diagnosticValue: ResolvedValue;

// Reporting a diagnostic on the entire array can be noisy, especially if the user has a
// large array. Attempt to determine the most accurate position within the `imports` expression to report the
// diagnostic on.
if (ref instanceof DynamicValue && isWithinExpression(ref.node, expr)) {
// Use the dynamic value position itself if it occurs within the `imports` expression.
diagnosticNode = ref.node;
diagnosticValue = ref;
} else if (refExpr !== expr) {
// The reference comes from a specific element in `expr`, so use that element to report the diagnostic on.
diagnosticNode = refExpr;
diagnosticValue = ref;
} else {
diagnosticNode = expr;
diagnosticValue = imports;
}
const {node: diagnosticNode, value: diagnosticValue} = getDiagnosticOrigin(
ref,
expr,
refExpr,
imports,
);

diagnostics.push(
createValueHasWrongTypeError(diagnosticNode, diagnosticValue, errorMessage).toDiagnostic(),
Expand All @@ -171,6 +163,83 @@ export function validateAndFlattenComponentImports(
return {imports: flattened, diagnostics};
}

export function validateAndFlattenForeignImports(
imports: ResolvedValue,
expr: ts.Expression,
): {
foreignImports: Reference<ClassDeclaration>[];
diagnostics: ts.Diagnostic[];
} {
const flattened: Reference<ClassDeclaration>[] = [];
const errorMessage = `'foreignImports' must be an array of ForeignComponents.`;

if (!Array.isArray(imports)) {
const error = createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic();
return {
foreignImports: [],
diagnostics: [error],
};
}

const diagnostics: ts.Diagnostic[] = [];

for (let i = 0; i < imports.length; i++) {
const ref = imports[i];
let refExpr = expr;
if (
ts.isArrayLiteralExpression(expr) &&
expr.elements.length === imports.length &&
!expr.elements.some(ts.isSpreadAssignment)
) {
refExpr = expr.elements[i];
}

if (Array.isArray(ref)) {
const {foreignImports: childForeignImports, diagnostics: childDiagnostics} =
validateAndFlattenForeignImports(ref, refExpr);
flattened.push(...childForeignImports);
diagnostics.push(...childDiagnostics);
} else if (ref instanceof Reference && isNamedFunctionDeclaration(ref.node)) {
flattened.push(ref as Reference<ClassDeclaration>);
} else {
const {node: diagnosticNode, value: diagnosticValue} = getDiagnosticOrigin(
ref,
expr,
refExpr,
imports,
);

diagnostics.push(
createValueHasWrongTypeError(diagnosticNode, diagnosticValue, errorMessage).toDiagnostic(),
);
}
}

return {foreignImports: flattened, diagnostics};
}

/**
* Reporting a diagnostic on the entire array can be noisy, especially if the user has a
* large array. Attempt to determine the most accurate position within the array expression to report the
* diagnostic on.
*/
function getDiagnosticOrigin(
ref: ResolvedValue,
expr: ts.Expression,
refExpr: ts.Expression,
fallbackValue: ResolvedValue,
): {node: ts.Node; value: ResolvedValue} {
if (ref instanceof DynamicValue && isWithinExpression(ref.node, expr)) {
// Use the dynamic value position itself if it occurs within the expression.
return {node: ref.node, value: ref};
} else if (refExpr !== expr) {
// The reference comes from a specific element in `expr`, so use that element to report the diagnostic on.
return {node: refExpr, value: ref};
} else {
return {node: expr, value: fallbackValue};
}
}

function isWithinExpression(node: ts.Node, expr: ts.Expression): boolean {
let current: ts.Node | undefined = node;
while (current !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ function setup(
const handler = new ComponentDecoratorHandler(
reflectionHost,
evaluator,
checker,
metaRegistry,
metaReader,
scopeRegistry,
{
getCanonicalFileName: (fileName) => fileName,
getCanonicalFileName: (fileName: string) => fileName,
},
scopeRegistry,
typeCheckScopeRegistry,
Expand Down Expand Up @@ -1042,6 +1043,94 @@ runInEachFileSystem(() => {
expect(diagnostics).toBeUndefined();
});

it('should populate foreignImports with ForeignComponents', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export const Component: any;
export type ForeignComponent<T> = T & { ɵrender: () => void };
`,
},
{
name: _('/entry.ts'),
contents: `
import {Component, ForeignComponent} from '@angular/core';

function FancyButton() {}

function foreignImport<T>(type: T): ForeignComponent<T> {
return type as any;
}

@Component({
selector: 'main',
template: '',
foreignImports: [foreignImport(FancyButton)],
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler} = setup(program, options, host);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(
TestCmp,
reflectionHost.getDecoratorsOfDeclaration(TestCmp),
);
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resolvedForeignImports?.length).toBe(1);
expect((analysis?.resolvedForeignImports![0].node as any).name.text).toBe('FancyButton');
});

it('should match template elements to foreign components', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export const Component: any;
export type ForeignComponent<T> = T & { ɵrender: () => void };
`,
},
{
name: _('/entry.ts'),
contents: `
import {Component, ForeignComponent} from '@angular/core';

function FancyButton() {}

function foreignImport<T>(type: T): ForeignComponent<T> {
return type as any;
}

@Component({
selector: 'main',
template: '<FancyButton></FancyButton>',
foreignImports: [foreignImport(FancyButton)],
standalone: true,
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler} = setup(program, options, host);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(
TestCmp,
reflectionHost.getDecoratorsOfDeclaration(TestCmp),
);
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
handler.register(TestCmp, analysis!);

const symbol = handler.symbol(TestCmp, analysis!);
const result = handler.resolve(TestCmp, analysis!, symbol);
expect(result.data).toBeDefined();
});

it('should produce diagnostic for imports in non-standalone component', () => {
const {program, options, host} = makeProgram(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: null,
foreignImports: null,
rawImports: null,
deferredImports: null,
schemas: null,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,7 @@ export class NgCompiler {
new ComponentDecoratorHandler(
reflector,
evaluator,
checker,
metaRegistry,
metaReader,
scopeReader,
Expand Down
Loading
Loading