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
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/error_code.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export enum ErrorCode {
DUPLICATE_DECORATED_PROPERTIES = 1012,
DUPLICATE_VARIABLE_DECLARATION = 8006,
FORBIDDEN_REQUIRED_INITIALIZER_INVOCATION = 8118,
FOREIGN_COMPONENT_UNSUPPORTED_BINDING = 8025,
FORM_FIELD_UNSUPPORTED_BINDING = 8022,
HOST_BINDING_PARSE_ERROR = 5001,
HOST_DIRECTIVE_COMPONENT = 2015,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ import {
R3TemplateDependency,
R3TemplateDependencyKind,
R3TemplateDependencyMetadata,
R3ForeignComponentMetadata,
SchemaMetadata,
SelectorlessMatcher,
SelectorMatcher,
TmplAstDeferredBlock,
TmplAstNode,
TmplAstElement,
TypeCheckId,
ViewEncapsulation,
} from '@angular/compiler';
Expand Down Expand Up @@ -85,6 +88,7 @@ import {
PipeMeta,
Resource,
ResourceRegistry,
createForeignComponentMatcher,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
Expand Down Expand Up @@ -180,6 +184,7 @@ import {
ComponentAnalysisData,
ComponentResolutionData,
DeferredComponentDependency,
ForeignComponentMeta,
} from './metadata';
import {
_extractTemplateStyleUrls,
Expand Down Expand Up @@ -607,7 +612,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
}

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

let rawImports: ts.Expression | null = component.get('imports') ?? null;
Expand Down Expand Up @@ -1206,7 +1211,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
return;
}

const binder = new R3TargetBinder<TypeCheckableDirectiveMeta>(scope.matcher);
const binder = new R3TargetBinder<TypeCheckableDirectiveMeta>(
scope.matcher,
scope.foreignMatcher,
);
const templateContext: TemplateContext = {
nodes: meta.template.diagNodes,
pipes: scope.pipes,
Expand Down Expand Up @@ -1490,10 +1498,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
? this.resolveAllDeferredDependencies(resolution)
: null;
const defer = this.compileDeferBlocks(resolution);
const foreignImports = this.resolveForeignComponentImports(node, analysis);
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
...resolution,
defer,
foreignImports,
};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));

Expand Down Expand Up @@ -1620,10 +1630,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null;

const defer = this.compileDeferBlocks(resolution);
const foreignImports = this.resolveForeignComponentImports(node, analysis);
const meta = {
...analysis.meta,
...resolution,
defer,
foreignImports,
} as R3ComponentMetadata<R3TemplateDependency>;

if (deferrableTypes !== null) {
Expand Down Expand Up @@ -1683,10 +1695,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
// Create a brand-new constant pool since there shouldn't be any constant sharing.
const pool = new ConstantPool();
const defer = this.compileDeferBlocks(resolution);
const foreignImports = this.resolveForeignComponentImports(node, analysis);
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
...resolution,
defer,
foreignImports,
};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileComponentFromMetadata(meta, pool, this.getNewBindingParser());
Expand Down Expand Up @@ -1810,7 +1824,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
}

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

Expand Down Expand Up @@ -2317,6 +2334,33 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
this.cycleAnalyzer.recordSyntheticImport(origin, imported);
}

/**
* Resolves imported foreign components for code generation.
*/
private resolveForeignComponentImports(
node: ClassDeclaration,
analysis: Readonly<ComponentAnalysisData>,
): R3ForeignComponentMetadata[] | null {
if (analysis.foreignImports === null || analysis.foreignImports.length === 0) {
return null;
}
const context = getSourceFile(node);

return analysis.foreignImports.map((foreignMeta) => {
const {ref, rawExpression} = foreignMeta;

const emittedRef = this.refEmitter.emit(ref, context);
assertSuccessfulReferenceEmit(emittedRef, node.name, 'foreign component');

ts.setEmitFlags(rawExpression, ts.EmitFlags.NoComments | ts.EmitFlags.NoNestedComments);

return {
name: foreignMeta.name,
component: new o.WrappedNodeExpr(rawExpression),
} satisfies R3ForeignComponentMetadata;
});
}

/**
* Resolves information about defer blocks dependencies to make it
* available for the final `compile` step.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ import {
SchemaMetadata,
TmplAstDeferredBlock,
ClassPropertyMapping,
SelectorlessMatcher,
} from '@angular/compiler';
import ts from 'typescript';

import {Reference} from '../../../imports';
import {
DirectiveResources,
DirectiveTypeCheckMeta,
ForeignComponentMeta,
HostDirectiveMeta,
InputMapping,
} from '../../../metadata';
export {ForeignComponentMeta} from '../../../metadata';
import {ClassDeclaration, Import} from '../../../reflection';
import {SubsetOfKeys} from '../../../util/src/typescript';

Expand Down Expand Up @@ -92,7 +95,7 @@ export interface ComponentAnalysisData {

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

Expand Down
16 changes: 12 additions & 4 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isNamedFunctionDeclaration,
} from '../../../reflection';
import {createValueHasWrongTypeError, getOriginNodeForDiagnostics} from '../../common';
import {ForeignComponentMeta} from './metadata';

/**
* Collect the animation names from the static evaluation result.
Expand Down Expand Up @@ -167,10 +168,10 @@ export function validateAndFlattenForeignImports(
imports: ResolvedValue,
expr: ts.Expression,
): {
foreignImports: Reference<ClassDeclaration>[];
foreignImports: ForeignComponentMeta[];
diagnostics: ts.Diagnostic[];
} {
const flattened: Reference<ClassDeclaration>[] = [];
const flattened: ForeignComponentMeta[] = [];
const errorMessage = `'foreignImports' must be an array of ForeignComponents.`;

if (!Array.isArray(imports)) {
Expand Down Expand Up @@ -199,8 +200,15 @@ export function validateAndFlattenForeignImports(
validateAndFlattenForeignImports(ref, refExpr);
flattened.push(...childForeignImports);
diagnostics.push(...childDiagnostics);
} else if (ref instanceof Reference && isNamedFunctionDeclaration(ref.node)) {
flattened.push(ref as Reference<ClassDeclaration>);
} else if (
ref instanceof Reference &&
(isNamedFunctionDeclaration(ref.node) || isNamedClassDeclaration(ref.node))
) {
flattened.push({
name: ref.node.name.getText(),
ref: ref as Reference<ClassDeclaration>,
rawExpression: refExpr,
});
} else {
const {node: diagnosticNode, value: diagnosticValue} = getDiagnosticOrigin(
ref,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,7 @@ runInEachFileSystem(() => {
import {foreignImport} from '@angular/core/src/render3/foreign_import';

function FancyButton() {}
function FancyMenu() {}

function frameworkImport(component: unknown) {
return foreignImport(() => {/* render component */});
Expand All @@ -1072,7 +1073,10 @@ runInEachFileSystem(() => {
@Component({
selector: 'main',
template: '',
foreignImports: [frameworkImport(FancyButton)],
foreignImports: [
frameworkImport(FancyButton),
frameworkImport(FancyMenu),
],
}) class TestCmp {}
`,
},
Expand All @@ -1089,8 +1093,9 @@ runInEachFileSystem(() => {
const {analysis, diagnostics} = handler.analyze(TestCmp, detected.metadata);

expect(diagnostics).toBeUndefined();
expect(analysis?.foreignImports?.length).toBe(1);
expect(analysis?.foreignImports![0].node.name.text).toBe('FancyButton');
expect(analysis?.foreignImports).toHaveSize(2);
expect(analysis!.foreignImports![0].name).toBe('FancyButton');
expect(analysis!.foreignImports![1].name).toBe('FancyMenu');
});

it('should produce diagnostic for imports in non-standalone component', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ export enum ErrorCode {
*/
CONFLICTING_HOST_DIRECTIVE_BINDING = -8024,

/**
* Raised when a foreign component node has an unsupported Angular binding.
*/
FOREIGN_COMPONENT_UNSUPPORTED_BINDING = 8025,

/**
* A two way binding in a template has an incorrect syntax,
* parentheses outside brackets. For example:
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
hasInjectableFields,
CompoundMetadataReader,
isHostDirectiveMetaForGlobalMode,
createForeignComponentMatcher,
} from './src/util';
export {ExportedProviderStatusResolver} from './src/providers';
export {HostDirectivesResolver} from './src/host_directives_resolver';
9 changes: 8 additions & 1 deletion packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ import {
InputOrOutput,
ClassPropertyMapping,
TemplateGuardMeta,
ForeignComponentMeta as T2ForeignComponentMeta,
} from '@angular/compiler';
import ts from 'typescript';

import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';

/** Metadata for a resolved foreign component import. */
export interface ForeignComponentMeta extends T2ForeignComponentMeta {
ref: Reference<ClassDeclaration>;
rawExpression: ts.Expression;
}

/**
* Metadata collected for an `NgModule`.
*/
Expand Down Expand Up @@ -256,7 +263,7 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
* Note that while a foreign import is not likely to be a class, this type is used
* because it includes the expected identifier we'll need, making further code simpler.
*/
foreignImports: Reference<ClassDeclaration>[] | null;
foreignImports: ForeignComponentMeta[] | null;

/**
* Node declaring the `imports` of a standalone component. Used to produce diagnostics.
Expand Down
22 changes: 21 additions & 1 deletion packages/compiler-cli/src/ngtsc/metadata/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ import {
MetadataReader,
NgModuleMeta,
PipeMeta,
ForeignComponentMeta,
} from './api';
import {TypeEntityToDeclarationError} from '../../reflection/src/typescript';
import {ClassPropertyMapping, ClassPropertyName, TemplateGuardMeta} from '@angular/compiler';
import {
ClassPropertyMapping,
ClassPropertyName,
TemplateGuardMeta,
SelectorlessMatcher,
} from '@angular/compiler';

export function extractReferencesFromType(
checker: ts.TypeChecker,
Expand Down Expand Up @@ -355,3 +361,17 @@ export function isHostDirectiveMetaForGlobalMode(
): hostDirectiveMeta is HostDirectiveMetaForGlobalMode {
return hostDirectiveMeta.directive instanceof Reference;
}

/** Extracts foreign component names from foreignImports and creates a SelectorlessMatcher. */
export function createForeignComponentMatcher(
foreignImports: ForeignComponentMeta[] | null,
): SelectorlessMatcher<ForeignComponentMeta> | null {
if (foreignImports === null || foreignImports.length === 0) {
return null;
}
const registry = new Map<string, ForeignComponentMeta[]>();
for (const meta of foreignImports) {
registry.set(meta.name, [meta]);
}
return new SelectorlessMatcher(registry);
}
12 changes: 12 additions & 0 deletions packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {Reference} from '../../imports';
import {
DirectiveMeta,
flattenInheritedDirectiveMetadata,
ForeignComponentMeta,
HostDirectivesResolver,
MetadataReader,
MetaKind,
NgModuleMeta,
PipeMeta,
createForeignComponentMatcher,
} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {ComponentScopeKind, ComponentScopeReader, SelectorlessScope} from './api';
Expand All @@ -37,6 +39,11 @@ export interface TypeCheckScope {
*/
matcher: DirectiveMatcher<DirectiveMeta> | null;

/**
* A `SelectorlessMatcher` instance that contains matched foreign components.
*/
foreignMatcher: SelectorlessMatcher<ForeignComponentMeta> | null;

/**
* All of the directives available in the compilation scope of the declaring NgModule.
*/
Expand Down Expand Up @@ -100,6 +107,7 @@ export class TypeCheckScopeRegistry {
if (scope === null) {
return {
matcher: null,
foreignMatcher: null,
directives,
pipes,
schemas: [],
Expand Down Expand Up @@ -152,8 +160,12 @@ export class TypeCheckScopeRegistry {
}
}

const foreignMatcher =
hostMeta !== null ? createForeignComponentMatcher(hostMeta.foreignImports) : null;

const typeCheckScope: TypeCheckScope = {
matcher,
foreignMatcher,
directives,
pipes,
schemas: scope.schemas,
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {
AST,
ForeignComponentMeta,
LiteralPrimitive,
ParseSourceSpan,
PropertyRead,
Expand Down Expand Up @@ -333,6 +334,14 @@ export interface TemplateTypeChecker {
node: TmplAstElement | TmplAstTemplate,
): TypeCheckableDirectiveMeta[] | null;

/**
* Gets the foreign component that matched the given template element.
*/
getForeignComponent(
component: ts.ClassDeclaration,
element: TmplAstElement,
): ForeignComponentMeta | null;

/**
* Gets the directives that have been used in a component's template.
*/
Expand Down
Loading
Loading