diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 36d3c512ce742..e2cc4086b6d05 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -21,6 +21,7 @@ import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from ' import {NgccClassSymbol, NgccReflectionHost} from '../host/ngcc_host'; import {Migration} from '../migrations/migration'; import {MissingInjectableMigration} from '../migrations/missing_injectable_migration'; +import {UndecoratedChildMigration} from '../migrations/undecorated_child_migration'; import {UndecoratedParentMigration} from '../migrations/undecorated_parent_migration'; import {EntryPointBundle} from '../packages/entry_point_bundle'; import {isDefined} from '../utils'; @@ -29,6 +30,7 @@ import {DefaultMigrationHost} from './migration_host'; import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses} from './types'; import {analyzeDecorators, isWithinPackage} from './util'; + /** * Simple class that resolves and loads files directly from the filesystem. */ @@ -104,7 +106,11 @@ export class DecorationAnalyzer { this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), ]; - migrations: Migration[] = [new UndecoratedParentMigration(), new MissingInjectableMigration()]; + migrations: Migration[] = [ + new UndecoratedParentMigration(), + new UndecoratedChildMigration(), + new MissingInjectableMigration(), + ]; constructor( private fs: FileSystem, private bundle: EntryPointBundle, diff --git a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts index c100a11443b8a..4ff258e44b661 100644 --- a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts +++ b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts @@ -12,7 +12,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {MetadataReader} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; -import {DecoratorHandler} from '../../../src/ngtsc/transform'; +import {DecoratorHandler, HandlerFlags} from '../../../src/ngtsc/transform'; import {NgccReflectionHost} from '../host/ngcc_host'; import {MigrationHost} from '../migrations/migration'; @@ -29,9 +29,10 @@ export class DefaultMigrationHost implements MigrationHost { readonly evaluator: PartialEvaluator, private handlers: DecoratorHandler[], private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[]) {} - injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void { + injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): + void { const classSymbol = this.reflectionHost.getClassSymbol(clazz) !; - const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers); + const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers, flags); if (newAnalyzedClass === null) { return; } diff --git a/packages/compiler-cli/ngcc/src/analysis/util.ts b/packages/compiler-cli/ngcc/src/analysis/util.ts index b7837d9f90f2a..66530ed4af5e5 100644 --- a/packages/compiler-cli/ngcc/src/analysis/util.ts +++ b/packages/compiler-cli/ngcc/src/analysis/util.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {AbsoluteFsPath, absoluteFromSourceFile, relative} from '../../../src/ngtsc/file_system'; import {Decorator} from '../../../src/ngtsc/reflection'; -import {DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; +import {DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence} from '../../../src/ngtsc/transform'; import {NgccClassSymbol} from '../host/ngcc_host'; import {AnalyzedClass, MatchingHandler} from './types'; @@ -21,7 +21,7 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.Sour export function analyzeDecorators( classSymbol: NgccClassSymbol, decorators: Decorator[] | null, - handlers: DecoratorHandler[]): AnalyzedClass|null { + handlers: DecoratorHandler[], flags?: HandlerFlags): AnalyzedClass|null { const declaration = classSymbol.declaration.valueDeclaration; const matchingHandlers = handlers .map(handler => { @@ -64,7 +64,7 @@ export function analyzeDecorators( const allDiagnostics: ts.Diagnostic[] = []; for (const {handler, detected} of detections) { try { - const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata); + const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata, flags); if (diagnostics !== undefined) { allDiagnostics.push(...diagnostics); } diff --git a/packages/compiler-cli/ngcc/src/migrations/migration.ts b/packages/compiler-cli/ngcc/src/migrations/migration.ts index 104eb7fbfc75b..2738dc3e53a92 100644 --- a/packages/compiler-cli/ngcc/src/migrations/migration.ts +++ b/packages/compiler-cli/ngcc/src/migrations/migration.ts @@ -6,11 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; + import {MetadataReader} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; +import {HandlerFlags} from '../../../src/ngtsc/transform'; import {NgccReflectionHost} from '../host/ngcc_host'; + /** * Implement this interface and add it to the `DecorationAnalyzer.migrations` collection to get ngcc * to modify the analysis of the decorators in the program in order to migrate older code to work @@ -41,7 +44,8 @@ export interface MigrationHost { * @param clazz the class to receive the new decorator. * @param decorator the decorator to inject. */ - injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void; + injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): + void; /** * Retrieves all decorators that are associated with the class, including synthetic decorators diff --git a/packages/compiler-cli/ngcc/src/migrations/undecorated_child_migration.ts b/packages/compiler-cli/ngcc/src/migrations/undecorated_child_migration.ts new file mode 100644 index 0000000000000..cb100fe7c4ac3 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/migrations/undecorated_child_migration.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {readBaseClass} from '../../../src/ngtsc/annotations/src/util'; +import {Reference} from '../../../src/ngtsc/imports'; +import {ClassDeclaration} from '../../../src/ngtsc/reflection'; +import {HandlerFlags} from '../../../src/ngtsc/transform'; + +import {Migration, MigrationHost} from './migration'; +import {createComponentDecorator, createDirectiveDecorator, hasDirectiveDecorator, hasPipeDecorator} from './utils'; + +export class UndecoratedChildMigration implements Migration { + apply(clazz: ClassDeclaration, host: MigrationHost): ts.Diagnostic|null { + // This migration looks at NgModules and considers the directives (and pipes) it declares. + // It verifies that these classes have decorators. + const moduleMeta = host.metadata.getNgModuleMetadata(new Reference(clazz)); + if (moduleMeta === null) { + // Not an NgModule; don't care. + return null; + } + + // Examine each of the declarations to see if it needs to be migrated. + for (const decl of moduleMeta.declarations) { + this.maybeMigrate(decl, host); + } + + return null; + } + + maybeMigrate(ref: Reference, host: MigrationHost): void { + if (hasDirectiveDecorator(host, ref.node) || hasPipeDecorator(host, ref.node)) { + // Stop if one of the classes in the chain is actually decorated with @Directive, @Component, + // or @Pipe. + return; + } + + const baseRef = readBaseClass(ref.node, host.reflectionHost, host.evaluator); + if (baseRef === null) { + // Stop: can't migrate a class with no parent. + return; + } else if (baseRef === 'dynamic') { + // Stop: can't migrate a class with an indeterminate parent. + return; + } + + // Apply the migration recursively, to handle inheritance chains. + this.maybeMigrate(baseRef, host); + + // After the above call, `host.metadata` should have metadata for the base class, if indeed this + // is a directive inheritance chain. + const baseMeta = host.metadata.getDirectiveMetadata(baseRef); + if (baseMeta === null) { + // Stop: this isn't a directive inheritance chain after all. + return; + } + + // Otherwise, decorate the class with @Component() or @Directive(), as appropriate. + if (baseMeta.isComponent) { + host.injectSyntheticDecorator( + ref.node, createComponentDecorator(ref.node, baseMeta), HandlerFlags.FULL_INHERITANCE); + } else { + host.injectSyntheticDecorator( + ref.node, createDirectiveDecorator(ref.node, baseMeta), HandlerFlags.FULL_INHERITANCE); + } + + // Success! + } +} diff --git a/packages/compiler-cli/ngcc/src/migrations/utils.ts b/packages/compiler-cli/ngcc/src/migrations/utils.ts index e988dde29bc86..5524661a022a5 100644 --- a/packages/compiler-cli/ngcc/src/migrations/utils.ts +++ b/packages/compiler-cli/ngcc/src/migrations/utils.ts @@ -23,6 +23,14 @@ export function hasDirectiveDecorator(host: MigrationHost, clazz: ClassDeclarati return host.metadata.getDirectiveMetadata(ref) !== null; } +/** + * Returns true if the `clazz` is decorated as a `Pipe`. + */ +export function hasPipeDecorator(host: MigrationHost, clazz: ClassDeclaration): boolean { + const ref = new Reference(clazz); + return host.metadata.getPipeMetadata(ref) !== null; +} + /** * Returns true if the `clazz` has its own constructor function. */ @@ -33,14 +41,53 @@ export function hasConstructor(host: MigrationHost, clazz: ClassDeclaration): bo /** * Create an empty `Directive` decorator that will be associated with the `clazz`. */ -export function createDirectiveDecorator(clazz: ClassDeclaration): Decorator { +export function createDirectiveDecorator( + clazz: ClassDeclaration, + metadata?: {selector: string | null, exportAs: string[] | null}): Decorator { + const args: ts.Expression[] = []; + if (metadata !== undefined) { + const metaArgs: ts.PropertyAssignment[] = []; + if (metadata.selector !== null) { + metaArgs.push(property('selector', metadata.selector)); + } + if (metadata.exportAs !== null) { + metaArgs.push(property('exportAs', metadata.exportAs)); + } + args.push(reifySourceFile(ts.createObjectLiteral(metaArgs))); + } return { name: 'Directive', identifier: null, import: {name: 'Directive', from: '@angular/core'}, node: null, + synthesizedFor: clazz.name, args, + }; +} + +/** + * Create an empty `Component` decorator that will be associated with the `clazz`. + */ +export function createComponentDecorator( + clazz: ClassDeclaration, + metadata: {selector: string | null, exportAs: string[] | null}): Decorator { + const metaArgs: ts.PropertyAssignment[] = [ + property('template', ''), + ]; + if (metadata.selector !== null) { + metaArgs.push(property('selector', metadata.selector)); + } + if (metadata.exportAs !== null) { + metaArgs.push(property('exportAs', metadata.exportAs)); + } + return { + name: 'Component', + identifier: null, + import: {name: 'Component', from: '@angular/core'}, + node: null, synthesizedFor: clazz.name, - args: [], + args: [ + reifySourceFile(ts.createObjectLiteral(metaArgs)), + ], }; } @@ -58,6 +105,15 @@ export function createInjectableDecorator(clazz: ClassDeclaration): Decorator { }; } +function property(name: string, value: string | string[]): ts.PropertyAssignment { + if (typeof value === 'string') { + return ts.createPropertyAssignment(name, ts.createStringLiteral(value)); + } else { + return ts.createPropertyAssignment( + name, ts.createArrayLiteral(value.map(v => ts.createStringLiteral(v)))); + } +} + const EMPTY_SF = ts.createSourceFile('(empty)', '', ts.ScriptTarget.Latest); /** diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 4a72d20160d2a..d0644397c23a6 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -56,6 +56,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/test/helpers", "@npm//rxjs", + "@npm//typescript", ], ) diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 1ee644a9c36e7..9832733ff84aa 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -19,6 +19,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTI import {Transformer} from '../../src/packages/transformer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater'; import {MockLogger} from '../helpers/mock_logger'; +import {genNodeModules} from './util'; const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true}); @@ -752,6 +753,135 @@ runInEachFileSystem(() => { }); }); + describe('undecorated child class migration', () => { + it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive', + () => { + genNodeModules({ + 'test-package': { + '/index.ts': ` + import {Directive, NgModule} from '@angular/core'; + + @Directive({ + selector: '[base]', + }) + export class BaseDir {} + + export class DerivedDir extends BaseDir {} + + @NgModule({ + declarations: [DerivedDir], + }) + export class Module {} + `, + }, + }); + + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: ['main'], + }); + + + const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); + expect(jsContents) + .toContain( + 'DerivedDir.ɵdir = ɵngcc0.ɵɵdefineDirective({ type: DerivedDir, selectors: [["", "base", ""]], ' + + 'features: [ɵngcc0.ɵɵInheritDefinitionFeature, ɵngcc0.ɵɵCopyDefinitionFeature] });'); + + const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`)); + expect(dtsContents) + .toContain( + 'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;'); + }); + + it('should generate a component definition with CopyDefinitionFeature for an undecorated child component', + () => { + genNodeModules({ + 'test-package': { + '/index.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: '[base]', + template: 'This is the base template', + }) + export class BaseCmp {} + + export class DerivedCmp extends BaseCmp {} + + @NgModule({ + declarations: [DerivedCmp], + }) + export class Module {} + `, + }, + }); + + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: ['main'], + }); + + + const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); + expect(jsContents).toContain('DerivedCmp.ɵcmp = ɵngcc0.ɵɵdefineComponent'); + expect(jsContents) + .toContain( + 'features: [ɵngcc0.ɵɵInheritDefinitionFeature, ɵngcc0.ɵɵCopyDefinitionFeature]'); + + const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`)); + expect(dtsContents) + .toContain( + 'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta;'); + }); + + it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain', + () => { + genNodeModules({ + 'test-package': { + '/index.ts': ` + import {Directive, NgModule} from '@angular/core'; + + @Directive({ + selector: '[base]', + }) + export class BaseDir {} + + export class DerivedDir1 extends BaseDir {} + + export class DerivedDir2 extends DerivedDir1 {} + + export class DerivedDir3 extends DerivedDir2 {} + + @NgModule({ + declarations: [DerivedDir3], + }) + export class Module {} + `, + }, + }); + + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: ['main'], + }); + + const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`)); + expect(dtsContents) + .toContain( + 'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;'); + expect(dtsContents) + .toContain( + 'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;'); + expect(dtsContents) + .toContain( + 'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;'); + }); + }); + describe('aliasing re-exports in commonjs', () => { it('should add re-exports to commonjs files', () => { loadTestFiles([ diff --git a/packages/compiler-cli/ngcc/test/integration/util.ts b/packages/compiler-cli/ngcc/test/integration/util.ts new file mode 100644 index 0000000000000..a0271d9b0fadb --- /dev/null +++ b/packages/compiler-cli/ngcc/test/integration/util.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; +import {MockFileSystemPosix} from '../../../src/ngtsc/file_system/testing'; + +import {loadStandardTestFiles} from '../../../test/helpers'; + +export type NodeModulesDef = { + [name: string]: Package +}; + +export type Package = { + [path: string]: string; +}; + +/** + * Compile one or more testing packages into the top-level `FileSystem`. + * + * Instead of writing ESM5 code by hand, and manually describing the Angular Package Format + * structure of that code in a mock NPM package, `genNodeModules` allows for the generation of one + * or more NPM packages from TypeScript source code. Each named NPM package in `def` is + * independently transpiled with `compileNodeModuleToFs` and written into `node_modules` in the + * top-level filesystem, ready for use in testing ngcc. + */ +export function genNodeModules(def: NodeModulesDef): void { + const fs = getFileSystem(); + for (const pkgName of Object.keys(def)) { + compileNodeModuleToFs(fs, pkgName, def[pkgName]); + } +} + +/** + * Takes the TypeScript project defined in the `Package` structure, compiles it to ESM5, and sets it + * up as a package in `node_modules` in `fs`. + * + * TODO(alxhub): over time, expand this to other bundle formats and make it more faithful to the + * shape of real NPM packages. + */ +function compileNodeModuleToFs(fs: FileSystem, pkgName: string, pkg: Package): void { + const compileFs = new MockFileSystemPosix(true); + compileFs.init(loadStandardTestFiles({fakeCore: false})); + + const options: ts.CompilerOptions = { + declaration: true, + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES5, + lib: [], + }; + + const rootNames = Object.keys(pkg); + + for (const fileName of rootNames) { + compileFs.writeFile(compileFs.resolve(fileName), pkg[fileName]); + } + + const host = new MockCompilerHost(compileFs); + const program = ts.createProgram({host, rootNames, options}); + program.emit(); + + // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file. + for (const inFileTs of rootNames) { + const inFileBase = inFileTs.replace(/\.ts$/, ''); + fs.writeFile( + fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), + compileFs.readFile(compileFs.resolve(`${inFileBase}.d.ts`))); + const jsContents = compileFs.readFile(compileFs.resolve(`${inFileBase}.js`)); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.js`), jsContents); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.metadata.json`), '{}'); + } + + // Write the package.json + const pkgJson: unknown = { + name: pkgName, + version: '0.0.1', + main: './index.js', + typings: './index.d.ts', + }; + + fs.writeFile( + fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); +} + +/** + * A simple `ts.CompilerHost` that uses a `FileSystem` instead of the real FS. + * + * TODO(alxhub): convert this into a first class `FileSystemCompilerHost` and use it as the base for + * the entire compiler. + */ +class MockCompilerHost implements ts.CompilerHost { + constructor(private fs: FileSystem) {} + getSourceFile( + fileName: string, languageVersion: ts.ScriptTarget, + onError?: ((message: string) => void)|undefined, + shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { + return ts.createSourceFile( + fileName, this.fs.readFile(this.fs.resolve(fileName)), languageVersion, true, + ts.ScriptKind.TS); + } + + getDefaultLibFileName(options: ts.CompilerOptions): string { + return ts.getDefaultLibFileName(options); + } + + writeFile(fileName: string, data: string): void { + this.fs.writeFile(this.fs.resolve(fileName), data); + } + + getCurrentDirectory(): string { return this.fs.pwd(); } + getCanonicalFileName(fileName: string): string { return fileName; } + useCaseSensitiveFileNames(): boolean { return true; } + getNewLine(): string { return '\n'; } + fileExists(fileName: string): boolean { return this.fs.exists(this.fs.resolve(fileName)); } + readFile(fileName: string): string|undefined { + const abs = this.fs.resolve(fileName); + return this.fs.exists(abs) ? this.fs.readFile(abs) : undefined; + } +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 23cbe879228ab..28f8d03f8bc4b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -19,7 +19,7 @@ import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance' import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope'; -import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; +import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck'; import {NoopResourceDependencyRecorder, ResourceDependencyRecorder} from '../../util/src/resource_recorder'; import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; @@ -137,7 +137,8 @@ export class ComponentDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput { + analyze(node: ClassDeclaration, decorator: Decorator, flags: HandlerFlags = HandlerFlags.NONE): + AnalysisOutput { const containingFile = node.getSourceFile().fileName; this.literalCache.delete(decorator); @@ -145,7 +146,7 @@ export class ComponentDecoratorHandler implements // on it. const directiveResult = extractDirectiveMetadata( node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore, - this.elementSchemaRegistry.getDefaultComponentElementName()); + flags, this.elementSchemaRegistry.getDefaultComponentElementName()); if (directiveResult === undefined) { // `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this // case, compilation of the decorator is skipped. Returning an empty object signifies diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 47be27966cd4a..adbb5069408d5 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -15,7 +15,7 @@ import {MetadataRegistry} from '../../metadata'; import {extractDirectiveGuards} from '../../metadata/src/util'; import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; -import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; +import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence} from '../../transform'; import {compileNgFactoryDefField} from './factory'; import {generateSetClassMetadataCall} from './metadata'; @@ -51,9 +51,11 @@ export class DirectiveDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput { + analyze(node: ClassDeclaration, decorator: Decorator, flags = HandlerFlags.NONE): + AnalysisOutput { const directiveResult = extractDirectiveMetadata( - node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore); + node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore, + flags); const analysis = directiveResult && directiveResult.metadata; if (analysis === undefined) { return {}; @@ -111,7 +113,7 @@ export class DirectiveDecoratorHandler implements export function extractDirectiveMetadata( clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, - defaultSelector: string | null = null): { + flags: HandlerFlags, defaultSelector: string | null = null): { decorator: Map, metadata: R3DirectiveMetadata, decoratedElements: ClassMember[], @@ -236,6 +238,7 @@ export function extractDirectiveMetadata( }, inputs: {...inputsFromMeta, ...inputsFromFields}, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, viewQueries, selector, + fullInheritance: !!(flags & HandlerFlags.FULL_INHERITANCE), type: new WrappedNodeExpr(clazz.name), typeArgumentCount: reflector.getGenericArityOfClass(clazz) || 0, typeSourceSpan: EMPTY_SOURCE_SPAN, usesInheritance, exportAs, providers diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 466ba9b7bd903..170965cb97970 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -313,15 +313,10 @@ export function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr|'dynamic'|null { - if (!isNamedClassDeclaration(node)) { - // If the node isn't a ts.ClassDeclaration, consider any base class to be dynamic for now. - return reflector.hasBaseClass(node) ? 'dynamic' : null; - } - const baseExpression = reflector.getBaseClassExpression(node); if (baseExpression !== null) { const baseClass = evaluator.evaluate(baseExpression); - if (baseClass instanceof Reference && isNamedClassDeclaration(baseClass.node)) { + if (baseClass instanceof Reference && reflector.isClass(baseClass.node)) { return baseClass as Reference; } else { return 'dynamic'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 7f1db50c7c9ba..6c01f812b4a49 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -37,6 +37,27 @@ export enum HandlerPrecedence { WEAK, } +/** + * A set of options which can be passed to a `DecoratorHandler` by a consumer, to tailor the output + * of compilation beyond the decorators themselves. + */ +export enum HandlerFlags { + /** + * No flags set. + */ + NONE = 0x0, + + /** + * Indicates that this decorator is fully inherited from its parent at runtime. In addition to + * normally inherited aspects such as inputs and queries, full inheritance applies to every aspect + * of the component or directive, such as the template function itself. + * + * Its primary effect is to cause the `CopyDefinitionFeature` to be applied to the definition + * being compiled. See that class for more information. + */ + FULL_INHERITANCE = 0x00000001, +} + /** * Provides the interface between a decorator compiler from @angular/compiler and the Typescript @@ -75,7 +96,7 @@ export interface DecoratorHandler { * if successful, or an array of diagnostic messages if the analysis fails or the decorator * isn't valid. */ - analyze(node: ClassDeclaration, metadata: M): AnalysisOutput; + analyze(node: ClassDeclaration, metadata: M, handlerFlags?: HandlerFlags): AnalysisOutput; /** * Registers information about the decorator for the indexing phase in a diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 8be8159abd013..c4a2c307abf49 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -271,6 +271,7 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3 queries: facade.queries.map(convertToR3QueryMetadata), providers: facade.providers != null ? new WrappedNodeExpr(facade.providers) : null, viewQueries: facade.viewQueries.map(convertToR3QueryMetadata), + fullInheritance: false, }; } diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 44ad40ef1b53e..4db8c1d4f6fe6 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -90,6 +90,11 @@ export interface R3DirectiveMetadata { */ usesInheritance: boolean; + /** + * Whether or not the component or directive inherits its entire decorator from its base class. + */ + fullInheritance: boolean; + /** * Reference name under which to export the directive's type in a template, * if any. diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 9769f31a5a1ab..97251967acb5b 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -104,6 +104,9 @@ function addFeatures( if (meta.usesInheritance) { features.push(o.importExpr(R3.InheritDefinitionFeature)); } + if (meta.fullInheritance) { + features.push(o.importExpr(R3.CopyDefinitionFeature)); + } if (meta.lifecycle.usesOnChanges) { features.push(o.importExpr(R3.NgOnChangesFeature).callFn(EMPTY_ARRAY)); }