Skip to content
Closed
13 changes: 7 additions & 6 deletions goldens/public-api/compiler-cli/compiler_options.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ export interface MiscOptions {
}

// @public
export interface StrictTemplateOptions {
export interface TargetOptions {
compilationMode?: 'full' | 'partial' | 'experimental-local';
}

// @public
export interface TypeCheckingOptions {
strictAttributeTypes?: boolean;
strictContextGenerics?: boolean;
strictDomEventTypes?: boolean;
Expand All @@ -75,11 +80,7 @@ export interface StrictTemplateOptions {
strictOutputEventTypes?: boolean;
strictSafeNavigationTypes?: boolean;
strictTemplates?: boolean;
}

// @public
export interface TargetOptions {
compilationMode?: 'full' | 'partial' | 'experimental-local';
typeCheckHostBindings?: boolean;
}

// (No @packageDocumentation comment for this package)
Expand Down
1 change: 1 addition & 0 deletions packages/bazel/src/ngc-wrapped/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function runOneBuild(
'_enableLetSyntax',
'_enableHmr',
'strictStandalone',
'typeCheckHostBindings',
]);

const userOverrides = Object.entries(userOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
TypeCheckableDirectiveMeta,
TypeCheckContext,
TemplateContext,
HostBindingsContext,
} from '../../../typecheck/api';
import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api';
Expand Down Expand Up @@ -155,7 +156,11 @@ import {
validateHostDirectives,
wrapFunctionExpressionsInParens,
} from '../../common';
import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive';
import {
extractDirectiveMetadata,
extractHostBindingResources,
parseDirectiveStyles,
} from '../../directive';
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';

import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
Expand Down Expand Up @@ -184,7 +189,7 @@ import {
collectAnimationNames,
validateAndFlattenComponentImports,
} from './util';
import {getTemplateDiagnostics} from '../../../typecheck';
import {getTemplateDiagnostics, createHostElement} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
import {getProjectRelativePath} from '../../../util/src/path';
Expand Down Expand Up @@ -266,6 +271,7 @@ export class ComponentDecoratorHandler
private readonly strictStandalone: boolean,
private readonly enableHmr: boolean,
private readonly implicitStandaloneValue: boolean,
private readonly typeCheckHostBindings: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -742,11 +748,11 @@ export class ComponentDecoratorHandler
}
}
}
const templateResource = template.declaration.isInline
? {path: null, expression: component.get('template')!}
const templateResource: Resource = template.declaration.isInline
? {path: null, node: component.get('template')!}
: {
path: absoluteFrom(template.declaration.resolvedTemplateUrl),
expression: template.sourceMapping.node,
node: template.sourceMapping.node,
};
const relativeTemplatePath = getProjectRelativePath(
templateResource.path ?? ts.getOriginalNode(node).getSourceFile().fileName,
Expand All @@ -760,6 +766,7 @@ export class ComponentDecoratorHandler
let styles: string[] = [];
const externalStyles: string[] = [];

const hostBindingResources = extractHostBindingResources(directiveResult.hostBindingNodes);
const styleResources = extractInlineStyleResources(component);
const styleUrls: StyleUrlMeta[] = [
...extractComponentStyleUrls(this.evaluator, component),
Expand All @@ -781,7 +788,7 @@ export class ComponentDecoratorHandler
// Only string literal values from the decorator are considered style resources
styleResources.add({
path: absoluteFrom(resourceUrl),
expression: styleUrl.expression,
node: styleUrl.expression,
});
}
const resourceStr = this.resourceLoader.load(resourceUrl);
Expand Down Expand Up @@ -934,6 +941,7 @@ export class ComponentDecoratorHandler
resources: {
styles: styleResources,
template: templateResource,
hostBindings: hostBindingResources,
},
isPoisoned,
animationTriggerNames,
Expand All @@ -944,6 +952,7 @@ export class ComponentDecoratorHandler
explicitlyDeferredTypes,
schemas,
decorator: (decorator?.node as ts.Decorator | null) ?? null,
hostBindingNodes: directiveResult.hostBindingNodes,
},
diagnostics,
};
Expand Down Expand Up @@ -1058,11 +1067,7 @@ export class ComponentDecoratorHandler
node: ClassDeclaration,
meta: Readonly<ComponentAnalysisData>,
): void {
if (this.typeCheckScopeRegistry === null || !ts.isClassDeclaration(node)) {
return;
}

if (meta.isPoisoned && !this.usePoisonedData) {
if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
return;
}
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
Expand All @@ -1081,11 +1086,30 @@ export class ComponentDecoratorHandler
preserveWhitespaces: meta.meta.template.preserveWhitespaces ?? false,
};

const hostElement = this.typeCheckHostBindings
? createHostElement(
'component',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
)
: null;
const hostBindingsContext: HostBindingsContext | null =
hostElement === null
? null
: {
node: hostElement,
sourceMapping: {type: 'direct', node},
};

ctx.addDirective(
new Reference(node),
binder,
scope.schemas,
templateContext,
hostBindingsContext,
meta.meta.isStandalone,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {ClassDeclaration} from '../../../reflection';
import {SubsetOfKeys} from '../../../util/src/typescript';

import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
import {HostBindingNodes} from '../../directive';

/**
* These fields of `R3ComponentMetadata` are updated in the `resolve` phase.
Expand Down Expand Up @@ -108,6 +109,9 @@ export interface ComponentAnalysisData {

/** Raw expression that defined the host directives array. Used for diagnostics. */
rawHostDirectives: ts.Expression | null;

/** Raw nodes representing the host bindings of the directive. */
hostBindingNodes: HostBindingNodes;
}

export interface ComponentResolutionData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -745,10 +745,10 @@ export function extractInlineStyleResources(component: Map<string, ts.Expression
if (stylesExpr !== undefined) {
if (ts.isArrayLiteralExpression(stylesExpr)) {
for (const expression of stringLiteralElements(stylesExpr)) {
styles.add({path: null, expression});
styles.add({path: null, node: expression});
}
} else if (ts.isStringLiteralLike(stylesExpr)) {
styles.add({path: null, expression: stylesExpr});
styles.add({path: null, node: stylesExpr});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function setup(
/* strictStandalone */ false,
/* enableHmr */ false,
/* implicitStandaloneValue */ true,
/* typeCheckHostBindings */ true,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}
Expand Down Expand Up @@ -231,7 +232,7 @@ runInEachFileSystem(() => {
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.template?.path).toBeNull();
expect(analysis?.resources.template?.expression.getText()).toEqual(`'${template}'`);
expect(analysis?.resources.template?.node.getText()).toEqual(`'${template}'`);
});

it('should keep track of external template', () => {
Expand Down Expand Up @@ -264,7 +265,7 @@ runInEachFileSystem(() => {
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.template?.path).toContain(templateUrl);
expect(analysis?.resources.template?.expression.getText()).toContain(`'${templateUrl}'`);
expect(analysis?.resources.template?.node.getText()).toContain(`'${templateUrl}'`);
});

it('should keep track of internal and external styles', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"@npm//@types/node",
"@npm//typescript",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
makeBindingParser,
R3ClassMetadata,
R3DirectiveMetadata,
R3TargetBinder,
WrappedNodeExpr,
} from '@angular/compiler';
import ts from 'typescript';
Expand All @@ -27,6 +28,7 @@ import {
} from '../../../incremental/semantic_graph';
import {
ClassPropertyMapping,
DirectiveResources,
DirectiveTypeCheckMeta,
extractDirectiveTypeCheckMeta,
HostDirectiveMeta,
Expand All @@ -35,6 +37,7 @@ import {
MetadataReader,
MetadataRegistry,
MetaKind,
ResourceRegistry,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
Expand All @@ -45,7 +48,7 @@ import {
Decorator,
ReflectionHost,
} from '../../../reflection';
import {LocalModuleScopeRegistry} from '../../../scope';
import {LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../../scope';
import {
AnalysisOutput,
CompilationMode,
Expand Down Expand Up @@ -74,9 +77,15 @@ import {
validateHostDirectives,
} from '../../common';

import {extractDirectiveMetadata} from './shared';
import {extractDirectiveMetadata, extractHostBindingResources, HostBindingNodes} from './shared';
import {DirectiveSymbol} from './symbol';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {
HostBindingsContext,
TypeCheckableDirectiveMeta,
TypeCheckContext,
} from '../../../typecheck/api';
import {createHostElement} from '../../../typecheck';

const FIELD_DECORATORS = [
'Input',
Expand Down Expand Up @@ -113,6 +122,8 @@ export interface DirectiveHandlerData {
decorator: ts.Decorator | null;
hostDirectives: HostDirectiveMeta[] | null;
rawHostDirectives: ts.Expression | null;
hostBindingNodes: HostBindingNodes;
resources: DirectiveResources;
}

export class DirectiveDecoratorHandler
Expand All @@ -134,10 +145,14 @@ export class DirectiveDecoratorHandler
private perf: PerfRecorder,
private importTracker: ImportedSymbolsTracker,
private includeClassMetadata: boolean,
private typeCheckScopeRegistry: TypeCheckScopeRegistry,
private readonly compilationMode: CompilationMode,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
private readonly resourceRegistry: ResourceRegistry,
private readonly strictStandalone: boolean,
private readonly implicitStandaloneValue: boolean,
private readonly usePoisonedData: boolean,
private readonly typeCheckHostBindings: boolean,
) {}

readonly precedence = HandlerPrecedence.PRIMARY;
Expand Down Expand Up @@ -231,6 +246,12 @@ export class DirectiveDecoratorHandler
isPoisoned: false,
isStructural: directiveResult.isStructural,
decorator: (decorator?.node as ts.Decorator | null) ?? null,
hostBindingNodes: directiveResult.hostBindingNodes,
resources: {
template: null,
styles: null,
hostBindings: extractHostBindingResources(directiveResult.hostBindingNodes),
},
},
};
}
Expand Down Expand Up @@ -286,11 +307,59 @@ export class DirectiveDecoratorHandler
isExplicitlyDeferred: false,
});

this.resourceRegistry.registerResources(analysis.resources, node);
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
}

typeCheck(
ctx: TypeCheckContext,
node: ClassDeclaration,
meta: Readonly<DirectiveHandlerData>,
): void {
// Currently type checking in directives is only supported for host bindings
// so we can skip everything below if type checking is disabled.
if (!this.typeCheckHostBindings) {
return;
}

if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
return;
}
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
if (scope.isPoisoned && !this.usePoisonedData) {
// Don't type-check components that had errors in their scopes, unless requested.
return;
}

const hostElement = createHostElement(
'directive',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
);

if (hostElement !== null) {
const binder = new R3TargetBinder<TypeCheckableDirectiveMeta>(scope.matcher);
const hostBindingsContext: HostBindingsContext = {
node: hostElement,
sourceMapping: {type: 'direct', node},
};

ctx.addDirective(
new Reference(node),
binder,
scope.schemas,
null,
hostBindingsContext,
meta.meta.isStandalone,
);
}
}

resolve(
node: ClassDeclaration,
analysis: DirectiveHandlerData,
Expand Down Expand Up @@ -342,7 +411,13 @@ export class DirectiveDecoratorHandler
diagnostics.push(...hostDirectivesDiagnotics);
}

return {diagnostics: diagnostics.length > 0 ? diagnostics : undefined};
if (diagnostics.length > 0) {
return {diagnostics};
}

// Note: we need to produce *some* sort of the data in order
// for the host binding diagnostics to be surfaced.
return {data: {}};
}

compileFull(
Expand Down
Loading