从官方版本合并到本地

This commit is contained in:
2021-08-10 19:26:23 -04:00
4084 changed files with 79611 additions and 328046 deletions
+19 -9
View File
@@ -1,6 +1,10 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "api_golden_test", "pkg_npm", "ts_config", "ts_library")
load("//tools:defaults.bzl", "pkg_npm", "ts_api_guardian_test", "ts_config", "ts_library")
# Load ng_perf_flag explicitly from ng_perf.bzl as it's private API, and not exposed to other
# consumers of @angular/bazel.
load("//packages/bazel/src:ng_perf.bzl", "ng_perf_flag")
package(default_visibility = ["//visibility:public"])
# Load ng_perf_flag explicitly from ng_perf.bzl as it's private API, and not exposed to other
# consumers of @angular/bazel.
@@ -33,6 +37,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/program_driver",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/translator",
@@ -41,7 +46,6 @@ ts_library(
"@npm//@bazel/typescript",
"@npm//@types/node",
"@npm//chokidar",
"@npm//fs-extra",
"@npm//minimist",
"@npm//reflect-metadata",
"@npm//tsickle",
@@ -73,24 +77,30 @@ pkg_npm(
],
)
ts_api_guardian_test(
api_golden_test(
name = "error_code_api",
actual = "angular/packages/compiler-cli/npm_package/src/ngtsc/diagnostics/src/error_code.d.ts",
data = [
":npm_package",
"//goldens:public-api",
],
golden = "angular/goldens/public-api/compiler-cli/error_code.d.ts",
entry_point = "angular/packages/compiler-cli/npm_package/src/ngtsc/diagnostics/src/error_code.d.ts",
golden = "angular/goldens/public-api/compiler-cli/error_code.md",
)
ts_api_guardian_test(
api_golden_test(
name = "compiler_options_api",
actual = "angular/packages/compiler-cli/npm_package/src/ngtsc/core/api/src/public_options.d.ts",
data = [
":npm_package",
"//goldens:public-api",
],
golden = "angular/goldens/public-api/compiler-cli/compiler_options.d.ts",
entry_point = "angular/packages/compiler-cli/npm_package/src/ngtsc/core/api/src/public_options.d.ts",
golden = "angular/goldens/public-api/compiler-cli/compiler_options.md",
)
# Controls whether the Ivy compiler produces performance traces as part of each build
ng_perf_flag(
name = "ng_perf",
build_setting_default = False,
)
# Controls whether the Ivy compiler produces performance traces as part of each build
@@ -31,7 +31,6 @@ nodejs_test(
"@nodejs//:node",
"@npm//domino",
"@npm//chokidar",
"@npm//fs-extra",
"@npm//source-map-support",
"@npm//shelljs",
"@npm//typescript",
@@ -46,7 +46,6 @@ const requiredNodeModules = {
'tslib': resolveNpmTreeArtifact('npm/node_modules/tslib'),
'domino': resolveNpmTreeArtifact('npm/node_modules/domino'),
'xhr2': resolveNpmTreeArtifact('npm/node_modules/xhr2'),
'fs-extra': resolveNpmTreeArtifact('npm/node_modules/fs-extra'),
// Fine grained dependencies which are used by the integration test Angular modules, and
// need to be symlinked so that they can be resolved by NodeJS or NGC.
+4 -1
View File
@@ -5,4 +5,7 @@
* 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
*/
export {createEs2015LinkerPlugin} from './src/es2015_linker_plugin';
import {defaultLinkerPlugin} from './src/babel_plugin';
export {createEs2015LinkerPlugin} from './src/es2015_linker_plugin';
export default defaultLinkerPlugin;
@@ -39,6 +39,7 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
switch (operator) {
case '&&':
case '||':
case '??':
return t.logicalExpression(operator, leftOperand, rightOperand);
default:
return t.binaryExpression(operator, leftOperand, rightOperand);
@@ -38,11 +38,18 @@ export class BabelAstHost implements AstHost<t.Expression> {
return num.value;
}
isBooleanLiteral = t.isBooleanLiteral;
isBooleanLiteral(bool: t.Expression): boolean {
return t.isBooleanLiteral(bool) || isMinifiedBooleanLiteral(bool);
}
parseBooleanLiteral(bool: t.Expression): boolean {
assert(bool, t.isBooleanLiteral, 'a boolean literal');
return bool.value;
if (t.isBooleanLiteral(bool)) {
return bool.value;
} else if (isMinifiedBooleanLiteral(bool)) {
return !bool.argument.value;
} else {
throw new FatalLinkerError(bool, 'Unsupported syntax, expected a boolean literal.');
}
}
isArrayLiteral = t.isArrayExpression;
@@ -165,3 +172,13 @@ type ArgumentType = t.CallExpression['arguments'][number];
function isNotSpreadArgument(arg: ArgumentType): arg is Exclude<ArgumentType, t.SpreadElement> {
return !t.isSpreadElement(arg);
}
type MinifiedBooleanLiteral = t.Expression&t.UnaryExpression&{argument: t.NumericLiteral};
/**
* Return true if the node is either `!0` or `!1`.
*/
function isMinifiedBooleanLiteral(node: t.Expression): node is MinifiedBooleanLiteral {
return t.isUnaryExpression(node) && node.prefix && node.operator === '!' &&
t.isNumericLiteral(node.argument) && (node.argument.value === 0 || node.argument.value === 1);
}
@@ -0,0 +1,39 @@
/**
* @license
* Copyright Google LLC 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 {ConfigAPI, PluginObj} from '@babel/core';
import {NodeJSFileSystem} from '../../../src/ngtsc/file_system';
import {ConsoleLogger, LogLevel} from '../../../src/ngtsc/logging';
import {LinkerOptions} from '../../src/file_linker/linker_options';
import {createEs2015LinkerPlugin} from './es2015_linker_plugin';
/**
* This is the Babel plugin definition that is provided as a default export from the package, such
* that the plugin can be used using the module specifier of the package. This is the recommended
* way of integrating the Angular Linker into a build pipeline other than the Angular CLI.
*
* When the module specifier `@angular/compiler-cli/linker/babel` is used as a plugin in a Babel
* configuration, Babel invokes this function (by means of the default export) to create the plugin
* instance according to the provided options.
*
* The linker plugin that is created uses the native NodeJS filesystem APIs to interact with the
* filesystem. Any logging output is printed to the console.
*
* @param api Provides access to the Babel environment that is configuring this plugin.
* @param options The plugin options that have been configured.
*/
export function defaultLinkerPlugin(api: ConfigAPI, options: Partial<LinkerOptions>): PluginObj {
api.assertVersion(7);
return createEs2015LinkerPlugin({
...options,
fileSystem: new NodeJSFileSystem(),
logger: new ConsoleLogger(LogLevel.info),
});
}
@@ -139,6 +139,8 @@ function getCalleeName(call: NodePath<t.CallExpression>): string|null {
return callee.name;
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
return callee.property.name;
} else if (t.isMemberExpression(callee) && t.isStringLiteral(callee.property)) {
return callee.property.value;
} else {
return null;
}
@@ -96,6 +96,11 @@ describe('BabelAstHost', () => {
expect(host.isBooleanLiteral(expr('false'))).toBe(true);
});
it('should return true if the expression is a minified boolean literal', () => {
expect(host.isBooleanLiteral(expr('!0'))).toBe(true);
expect(host.isBooleanLiteral(expr('!1'))).toBe(true);
});
it('should return false if the expression is not a boolean literal', () => {
expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false);
@@ -106,6 +111,8 @@ describe('BabelAstHost', () => {
expect(host.isBooleanLiteral(expr('null'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false);
expect(host.isBooleanLiteral(expr('!2'))).toBe(false);
expect(host.isBooleanLiteral(expr('~1'))).toBe(false);
});
});
@@ -115,6 +122,11 @@ describe('BabelAstHost', () => {
expect(host.parseBooleanLiteral(expr('false'))).toEqual(false);
});
it('should extract a minified boolean value', () => {
expect(host.parseBooleanLiteral(expr('!0'))).toEqual(true);
expect(host.parseBooleanLiteral(expr('!1'))).toEqual(false);
});
it('should error if the value is not a boolean literal', () => {
expect(() => host.parseBooleanLiteral(expr('"moo"')))
.toThrowError('Unsupported syntax, expected a boolean literal.');
@@ -0,0 +1,56 @@
/**
* @license
* Copyright Google LLC 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 {transformSync} from '@babel/core';
describe('default babel plugin entry-point', () => {
it('should work as a Babel plugin using the module specifier', () => {
const result = transformSync(
`
import * as i0 from "@angular/core";
export class MyMod {}
export class MyComponent {}
MyMod.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyMod, declarations: [MyComponent] });
`,
{
plugins: [
'@angular/compiler-cli/linker/babel',
],
filename: 'test.js',
})!;
expect(result).not.toBeNull();
expect(result.code).not.toContain('ɵɵngDeclareNgModule');
expect(result.code).toContain('i0.ɵɵdefineNgModule');
expect(result.code).not.toMatch(/declarations:\s*\[MyComponent]/);
});
it('should be configurable', () => {
const result = transformSync(
`
import * as i0 from "@angular/core";
export class MyMod {}
export class MyComponent {}
MyMod.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyMod, declarations: [MyComponent] });
`,
{
plugins: [
['@angular/compiler-cli/linker/babel', {linkerJitMode: true}],
],
filename: 'test.js',
})!;
expect(result).not.toBeNull();
expect(result.code).not.toContain('ɵɵngDeclareNgModule');
expect(result.code).toContain('i0.ɵɵdefineNgModule');
expect(result.code).toMatch(/declarations:\s*\[MyComponent]/);
});
});
@@ -69,10 +69,11 @@ describe('createEs2015LinkerPlugin()', () => {
transformSync(
[
'var core;',
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`,
`ɵɵngDeclareComponent({version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`,
`x.qux(() => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`,
`i0.ɵɵngDeclareComponent({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`,
`x.qux(() => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`,
'spread(...x);',
`i0['ɵɵngDeclareDirective']({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 4});`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
@@ -81,13 +82,23 @@ describe('createEs2015LinkerPlugin()', () => {
});
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:1}'],
[
'ɵɵngDeclareDirective',
'{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:1}'
],
[
'ɵɵngDeclareComponent',
'{version:\'0.0.0-PLACEHOLDER\',ngImport:core,foo:()=>ɵɵngDeclareDirective({version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:2})}'
'{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,foo:()=>ɵɵngDeclareDirective({minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:2})}'
],
// Note we do not process `x:2` declaration since it is nested within another declaration
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:3}']
[
'ɵɵngDeclareDirective',
'{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:3}'
],
[
'ɵɵngDeclareDirective',
'{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:4}'
],
]);
});
@@ -125,9 +136,9 @@ describe('createEs2015LinkerPlugin()', () => {
[
'import * as core from \'some-module\';',
'import {id} from \'other-module\';',
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
@@ -149,9 +160,9 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'var core;',
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
@@ -174,9 +185,10 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'function run(core) {',
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`, '}'
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
'}'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
@@ -198,9 +210,9 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'function run() {',
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
'}',
].join('\n'),
{
@@ -226,7 +238,7 @@ describe('createEs2015LinkerPlugin()', () => {
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`,
].join('\n'),
{
plugins: [
@@ -267,7 +279,7 @@ describe('createEs2015LinkerPlugin()', () => {
const result = transformSync(
[
'import * as core from \'some-module\';',
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
`ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
@@ -36,11 +36,17 @@ export interface AstHost<TExpression> {
parseNumericLiteral(num: TExpression): number;
/**
* Return `true` if the given expression is a boolean literal, or false otherwise.
* Return `true` if the given expression can be considered a boolean literal, or false otherwise.
*
* Note that this should also cover the special case of some minified code where `true` and
* `false` are replaced by `!0` and `!1` respectively.
*/
isBooleanLiteral(node: TExpression): boolean;
/**
* Parse the boolean value from the given expression, or throw if it is not a boolean literal.
*
* Note that this should also cover the special case of some minified code where `true` and
* `false` are replaced by `!0` and `!1` respectively.
*/
parseBooleanLiteral(bool: TExpression): boolean;
@@ -47,13 +47,18 @@ export class TypeScriptAstHost implements AstHost<ts.Expression> {
return parseInt(num.text);
}
isBooleanLiteral(node: ts.Expression): node is ts.FalseLiteral|ts.TrueLiteral {
return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword;
isBooleanLiteral(node: ts.Expression): boolean {
return isBooleanLiteral(node) || isMinifiedBooleanLiteral(node);
}
parseBooleanLiteral(bool: ts.Expression): boolean {
assert(bool, this.isBooleanLiteral, 'a boolean literal');
return bool.kind === ts.SyntaxKind.TrueKeyword;
if (isBooleanLiteral(bool)) {
return bool.kind === ts.SyntaxKind.TrueKeyword;
} else if (isMinifiedBooleanLiteral(bool)) {
return !(+bool.operand.text);
} else {
throw new FatalLinkerError(bool, 'Unsupported syntax, expected a boolean literal.');
}
}
isArrayLiteral = ts.isArrayLiteralExpression;
@@ -160,3 +165,20 @@ function isNotSpreadElement(e: ts.Expression|ts.SpreadElement): e is ts.Expressi
function isPropertyName(e: ts.PropertyName): e is ts.Identifier|ts.StringLiteral|ts.NumericLiteral {
return ts.isIdentifier(e) || ts.isStringLiteral(e) || ts.isNumericLiteral(e);
}
/**
* Return true if the node is either `true` or `false` literals.
*/
function isBooleanLiteral(node: ts.Expression): node is ts.TrueLiteral|ts.FalseLiteral {
return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword;
}
type MinifiedBooleanLiteral = ts.PrefixUnaryExpression&{operand: ts.NumericLiteral};
/**
* Return true if the node is either `!0` or `!1`.
*/
function isMinifiedBooleanLiteral(node: ts.Expression): node is MinifiedBooleanLiteral {
return ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken &&
ts.isNumericLiteral(node.operand) && (node.operand.text === '0' || node.operand.text === '1');
}
@@ -30,7 +30,7 @@ export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement
* Wraps the output from `EmitScope.translateDefinition()` and `EmitScope.getConstantStatements()`
* in an IIFE.
*/
translateDefinition(definition: o.Expression): TExpression {
override translateDefinition(definition: o.Expression): TExpression {
const constantStatements = super.getConstantStatements();
const returnStatement =
@@ -44,7 +44,7 @@ export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement
* It is not valid to call this method, since there will be no shared constant statements - they
* are already emitted in the IIFE alongside the translated definition.
*/
getConstantStatements(): TStatement[] {
override getConstantStatements(): TStatement[] {
throw new Error('BUG - IifeEmitScope should not expose any constant statements');
}
}
@@ -12,7 +12,7 @@ import {DeclarationScope} from './declaration_scope';
import {EmitScope} from './emit_scopes/emit_scope';
import {IifeEmitScope} from './emit_scopes/iife_emit_scope';
import {LinkerEnvironment} from './linker_environment';
import {PartialLinkerSelector} from './partial_linkers/partial_linker_selector';
import {createLinkerMap, PartialLinkerSelector} from './partial_linkers/partial_linker_selector';
export const NO_STATEMENTS: Readonly<any[]> = [] as const;
@@ -20,14 +20,15 @@ export const NO_STATEMENTS: Readonly<any[]> = [] as const;
* This class is responsible for linking all the partial declarations found in a single file.
*/
export class FileLinker<TConstantScope, TStatement, TExpression> {
private linkerSelector: PartialLinkerSelector<TStatement, TExpression>;
private linkerSelector: PartialLinkerSelector<TExpression>;
private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression>>();
constructor(
private linkerEnvironment: LinkerEnvironment<TStatement, TExpression>,
sourceUrl: AbsoluteFsPath, code: string) {
this.linkerSelector =
new PartialLinkerSelector<TStatement, TExpression>(this.linkerEnvironment, sourceUrl, code);
this.linkerSelector = new PartialLinkerSelector<TExpression>(
createLinkerMap(this.linkerEnvironment, sourceUrl, code), this.linkerEnvironment.logger,
this.linkerEnvironment.options.unknownDeclarationVersionHandling);
}
/**
@@ -63,8 +64,9 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
const ngImport = metaObj.getNode('ngImport');
const emitScope = this.getEmitScope(ngImport, declarationScope);
const minVersion = metaObj.getString('minVersion');
const version = metaObj.getString('version');
const linker = this.linkerSelector.getLinker(declarationFn, version);
const linker = this.linkerSelector.getLinker(declarationFn, minVersion, version);
const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj);
return emitScope.translateDefinition(definition);
@@ -29,13 +29,10 @@ export class LinkerEnvironment<TStatement, TExpression> {
factory: AstFactory<TStatement, TExpression>,
options: Partial<LinkerOptions>): LinkerEnvironment<TStatement, TExpression> {
return new LinkerEnvironment(fileSystem, logger, host, factory, {
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat ??
DEFAULT_LINKER_OPTIONS.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs ??
DEFAULT_LINKER_OPTIONS.i18nNormalizeLineEndingsInICUs,
i18nUseExternalIds: options.i18nUseExternalIds ?? DEFAULT_LINKER_OPTIONS.i18nUseExternalIds,
sourceMapping: options.sourceMapping ?? DEFAULT_LINKER_OPTIONS.sourceMapping,
linkerJitMode: options.linkerJitMode ?? DEFAULT_LINKER_OPTIONS.linkerJitMode,
unknownDeclarationVersionHandling: options.unknownDeclarationVersionHandling ??
DEFAULT_LINKER_OPTIONS.unknownDeclarationVersionHandling,
});
}
}
@@ -10,24 +10,6 @@
* Options to configure the linking behavior.
*/
export interface LinkerOptions {
/**
* Whether to generate legacy i18n message ids.
* The default is `true`.
*/
enableI18nLegacyMessageIdFormat: boolean;
/**
* Whether to convert all line-endings in ICU expressions to `\n` characters.
* The default is `false`.
*/
i18nNormalizeLineEndingsInICUs: boolean;
/**
* Whether translation variable name should contain external message id
* (used by Closure Compiler's output of `goog.getMsg` for transition period)
* The default is `false`.
*/
i18nUseExternalIds: boolean;
/**
* Whether to use source-mapping to compute the original source for external templates.
* The default is `true`.
@@ -41,15 +23,27 @@ export interface LinkerOptions {
* `exports`, etc, which are otherwise not needed.
*/
linkerJitMode: boolean;
/**
* How to handle a situation where a partial declaration matches none of the supported
* partial-linker versions.
*
* - `error` - the version mismatch is a fatal error.
* - `warn` - a warning is sent to the logger but the most recent partial-linker
* will attempt to process the declaration anyway.
* - `ignore` - the most recent partial-linker will, silently, attempt to process
* the declaration.
*
* The default is `error`.
*/
unknownDeclarationVersionHandling: 'ignore'|'warn'|'error';
}
/**
* The default linker options to use if properties are not provided.
*/
export const DEFAULT_LINKER_OPTIONS: LinkerOptions = {
enableI18nLegacyMessageIdFormat: true,
i18nNormalizeLineEndingsInICUs: false,
i18nUseExternalIds: false,
sourceMapping: true,
linkerJitMode: false,
unknownDeclarationVersionHandling: 'error',
};
@@ -0,0 +1,38 @@
/**
* @license
* Copyright Google LLC 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 {compileClassMetadata, ConstantPool, R3ClassMetadata, R3DeclareClassMetadata, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {PartialLinker} from './partial_linker';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareClassMetadata()` call expressions.
*/
export class PartialClassMetadataLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3ClassMetadata(metaObj);
return compileClassMetadata(meta);
}
}
/**
* Derives the `R3ClassMetadata` structure from the AST object.
*/
export function toR3ClassMetadata<TExpression>(
metaObj: AstObject<R3DeclareClassMetadata, TExpression>): R3ClassMetadata {
return {
type: metaObj.getOpaque('type'),
decorators: metaObj.getOpaque('decorators'),
ctorParameters: metaObj.has('ctorParameters') ? metaObj.getOpaque('ctorParameters') : null,
propDecorators: metaObj.has('propDecorators') ? metaObj.getOpaque('propDecorators') : null,
};
}
@@ -14,24 +14,17 @@ import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {GetSourceFileFn} from '../get_source_file';
import {LinkerEnvironment} from '../linker_environment';
import {toR3DirectiveMeta} from './partial_directive_linker_1';
import {PartialLinker} from './partial_linker';
import {extractForwardRef} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions.
*/
export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
PartialLinker<TExpression> {
private readonly i18nNormalizeLineEndingsInICUs =
this.environment.options.i18nNormalizeLineEndingsInICUs;
private readonly enableI18nLegacyMessageIdFormat =
this.environment.options.enableI18nLegacyMessageIdFormat;
private readonly i18nUseExternalIds = this.environment.options.i18nUseExternalIds;
constructor(
private readonly environment: LinkerEnvironment<TStatement, TExpression>,
private readonly getSourceFile: GetSourceFileFn, private sourceUrl: AbsoluteFsPath,
private code: string) {}
@@ -53,18 +46,15 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
const isInline = metaObj.has('isInline') ? metaObj.getBoolean('isInline') : false;
const templateInfo = this.getTemplateInfo(templateSource, isInline);
// We always normalize line endings if the template is inline.
const i18nNormalizeLineEndingsInICUs = isInline || this.i18nNormalizeLineEndingsInICUs;
const template = parseTemplate(templateInfo.code, templateInfo.sourceUrl, {
escapedString: templateInfo.isEscaped,
interpolationConfig: interpolation,
range: templateInfo.range,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
enableI18nLegacyMessageIdFormat: false,
preserveWhitespaces:
metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false,
i18nNormalizeLineEndingsInICUs,
isInline,
// We normalize line endings if the template is was inline.
i18nNormalizeLineEndingsInICUs: isInline,
});
if (template.errors !== null) {
const errors = template.errors.map(err => err.toString()).join('\n');
@@ -81,10 +71,8 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
const type = directiveExpr.getValue('type');
const selector = directiveExpr.getString('selector');
let typeExpr = type.getOpaque();
const forwardRefType = extractForwardRef(type);
if (forwardRefType !== null) {
typeExpr = forwardRefType;
const {expression: typeExpr, isForwardRef} = extractForwardRef(type);
if (isForwardRef) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
@@ -115,13 +103,11 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
let pipes = new Map<string, o.Expression>();
if (metaObj.has('pipes')) {
pipes = metaObj.getObject('pipes').toMap(pipe => {
const forwardRefType = extractForwardRef(pipe);
if (forwardRefType !== null) {
const {expression: pipeType, isForwardRef} = extractForwardRef(pipe);
if (isForwardRef) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
return forwardRefType;
} else {
return pipe.getOpaque();
}
return pipeType;
});
}
@@ -144,7 +130,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: this.sourceUrl,
i18nUseExternalIds: this.i18nUseExternalIds,
i18nUseExternalIds: false,
pipes,
directives,
};
@@ -277,35 +263,3 @@ function parseChangeDetectionStrategy<TExpression>(
}
return enumValue;
}
/**
* Extract the type reference expression from a `forwardRef` function call. For example, the
* expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note that this
* expression is required to be wrapped in a closure, as otherwise the forward reference would be
* resolved before initialization.
*/
function extractForwardRef<TExpression>(expr: AstValue<unknown, TExpression>):
o.WrappedNodeExpr<TExpression>|null {
if (!expr.isCallExpression()) {
return null;
}
const callee = expr.getCallee();
if (callee.getSymbolName() !== 'forwardRef') {
throw new FatalLinkerError(
callee.expression, 'Unsupported directive type, expected forwardRef or a type reference');
}
const args = expr.getArguments();
if (args.length !== 1) {
throw new FatalLinkerError(expr, 'Unsupported forwardRef call, expected a single argument');
}
const wrapperFn = args[0] as AstValue<Function, TExpression>;
if (!wrapperFn.isFunction()) {
throw new FatalLinkerError(
wrapperFn, 'Unsupported forwardRef call, expected a function argument');
}
return wrapperFn.getFunctionReturnValue().getOpaque();
}
@@ -5,14 +5,14 @@
* 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 {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareDependencyMetadata, R3DeclareFactoryMetadata, R3DependencyMetadata, R3FactoryMetadata, R3PartialDeclaration} from '@angular/compiler';
import {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareFactoryMetadata, R3DependencyMetadata, R3FactoryMetadata, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {PartialLinker} from './partial_linker';
import {parseEnum, wrapReference} from './util';
import {getDependency, parseEnum, wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareFactory()` call expressions.
@@ -45,11 +45,11 @@ export function toR3FactoryMeta<TExpression>(
internalType: metaObj.getOpaque('type'),
typeArgumentCount: 0,
target: parseEnum(metaObj.getValue('target'), FactoryTarget),
deps: getDeps(metaObj, 'deps'),
deps: getDependencies(metaObj, 'deps'),
};
}
function getDeps<TExpression>(
function getDependencies<TExpression>(
metaObj: AstObject<R3DeclareFactoryMetadata, TExpression>,
propName: keyof R3DeclareFactoryMetadata): R3DependencyMetadata[]|null|'invalid' {
if (!metaObj.has(propName)) {
@@ -57,32 +57,10 @@ function getDeps<TExpression>(
}
const deps = metaObj.getValue(propName);
if (deps.isArray()) {
return deps.getArray().map(dep => getDep(dep.getObject()));
return deps.getArray().map(dep => getDependency(dep.getObject()));
}
if (deps.isString()) {
return 'invalid';
}
return null;
}
function getDep<TExpression>(depObj: AstObject<R3DeclareDependencyMetadata, TExpression>):
R3DependencyMetadata {
const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute');
const token = depObj.getOpaque('token');
// Normally `attribute` is a string literal and so its `attributeNameType` is the same string
// literal. If the `attribute` is some other expression, the `attributeNameType` would be the
// `unknown` type. It is not possible to generate this when linking, since it only deals with JS
// and not typings. When linking the existence of the `attributeNameType` only acts as a marker to
// change the injection instruction that is generated, so we just pass the literal string
// `"unknown"`.
const attributeNameType = isAttribute ? o.literal('unknown') : null;
const dep: R3DependencyMetadata = {
token,
attributeNameType,
host: depObj.has('host') && depObj.getBoolean('host'),
optional: depObj.has('optional') && depObj.getBoolean('optional'),
self: depObj.has('self') && depObj.getBoolean('self'),
skipSelf: depObj.has('skipSelf') && depObj.getBoolean('skipSelf'),
};
return dep;
}
@@ -0,0 +1,69 @@
/**
* @license
* Copyright Google LLC 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 {compileInjectable, ConstantPool, createR3ProviderExpression, R3DeclareInjectableMetadata, R3InjectableMetadata, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {PartialLinker} from './partial_linker';
import {extractForwardRef, getDependency, wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareInjectable()` call expressions.
*/
export class PartialInjectableLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3InjectableMeta(metaObj);
const def = compileInjectable(meta, /* resolveForwardRefs */ false);
return def.expression;
}
}
/**
* Derives the `R3InjectableMetadata` structure from the AST object.
*/
export function toR3InjectableMeta<TExpression>(
metaObj: AstObject<R3DeclareInjectableMetadata, TExpression>): R3InjectableMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
throw new FatalLinkerError(
typeExpr.expression, 'Unsupported type, its name could not be determined');
}
const meta: R3InjectableMetadata = {
name: typeName,
type: wrapReference(typeExpr.getOpaque()),
internalType: typeExpr.getOpaque(),
typeArgumentCount: 0,
providedIn: metaObj.has('providedIn') ? extractForwardRef(metaObj.getValue('providedIn')) :
createR3ProviderExpression(o.literal(null), false),
};
if (metaObj.has('useClass')) {
meta.useClass = extractForwardRef(metaObj.getValue('useClass'));
}
if (metaObj.has('useFactory')) {
meta.useFactory = metaObj.getOpaque('useFactory');
}
if (metaObj.has('useExisting')) {
meta.useExisting = extractForwardRef(metaObj.getValue('useExisting'));
}
if (metaObj.has('useValue')) {
meta.useValue = extractForwardRef(metaObj.getValue('useValue'));
}
if (metaObj.has('deps')) {
meta.deps = metaObj.getArray('deps').map(dep => getDependency(dep.getObject()));
}
return meta;
}
@@ -5,59 +5,126 @@
* 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 {satisfies} from 'semver';
import {intersects, Range, SemVer} from 'semver';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Logger} from '../../../../src/ngtsc/logging';
import {createGetSourceFile} from '../get_source_file';
import {LinkerEnvironment} from '../linker_environment';
import {PartialClassMetadataLinkerVersion1} from './partial_class_metadata_linker_1';
import {PartialComponentLinkerVersion1} from './partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
import {PartialFactoryLinkerVersion1} from './partial_factory_linker_1';
import {PartialInjectableLinkerVersion1} from './partial_injectable_linker_1';
import {PartialInjectorLinkerVersion1} from './partial_injector_linker_1';
import {PartialLinker} from './partial_linker';
import {PartialNgModuleLinkerVersion1} from './partial_ng_module_linker_1';
import {PartialPipeLinkerVersion1} from './partial_pipe_linker_1';
export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareClassMetadata = 'ɵɵngDeclareClassMetadata';
export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent';
export const ɵɵngDeclareFactory = 'ɵɵngDeclareFactory';
export const ɵɵngDeclareInjectable = 'ɵɵngDeclareInjectable';
export const ɵɵngDeclareInjector = 'ɵɵngDeclareInjector';
export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule';
export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe';
export const declarationFunctions = [
ɵɵngDeclareDirective, ɵɵngDeclareComponent, ɵɵngDeclareFactory, ɵɵngDeclareInjector,
ɵɵngDeclareNgModule, ɵɵngDeclarePipe
ɵɵngDeclareDirective, ɵɵngDeclareClassMetadata, ɵɵngDeclareComponent, ɵɵngDeclareFactory,
ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe
];
interface LinkerRange<TExpression> {
range: string;
export interface LinkerRange<TExpression> {
range: Range;
linker: PartialLinker<TExpression>;
}
/**
* Create a mapping between partial-declaration call name and collections of partial-linkers.
*
* Each collection of partial-linkers will contain a version range that will be matched against the
* `minVersion` of the partial-declaration. (Additionally, a partial-linker may modify its behaviour
* internally based on the `version` property of the declaration.)
*
* Versions should be sorted in ascending order. The most recent partial-linker will be used as the
* fallback linker if none of the other version ranges match. For example:
*
* ```
* {range: getRange('<=', '13.0.0'), linker PartialDirectiveLinkerVersion2(...) },
* {range: getRange('<=', '13.1.0'), linker PartialDirectiveLinkerVersion3(...) },
* {range: getRange('<=', '14.0.0'), linker PartialDirectiveLinkerVersion4(...) },
* {range: LATEST_VERSION_RANGE, linker: new PartialDirectiveLinkerVersion1(...)},
* ```
*
* If the `LATEST_VERSION_RANGE` is `<=15.0.0` then the fallback linker would be
* `PartialDirectiveLinkerVersion1` for any version greater than `15.0.0`.
*
* When there is a change to a declaration interface that requires a new partial-linker, the
* `minVersion` of the partial-declaration should be updated, the new linker implementation should
* be added to the end of the collection, and the version of the previous linker should be updated.
*/
export function createLinkerMap<TStatement, TExpression>(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string): Map<string, LinkerRange<TExpression>[]> {
const linkers = new Map<string, LinkerRange<TExpression>[]>();
const LATEST_VERSION_RANGE = getRange('<=', '0.0.0-PLACEHOLDER');
linkers.set(ɵɵngDeclareDirective, [
{range: LATEST_VERSION_RANGE, linker: new PartialDirectiveLinkerVersion1(sourceUrl, code)},
]);
linkers.set(ɵɵngDeclareClassMetadata, [
{range: LATEST_VERSION_RANGE, linker: new PartialClassMetadataLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareComponent, [
{
range: LATEST_VERSION_RANGE,
linker: new PartialComponentLinkerVersion1(
createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), sourceUrl, code)
},
]);
linkers.set(ɵɵngDeclareFactory, [
{range: LATEST_VERSION_RANGE, linker: new PartialFactoryLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareInjectable, [
{range: LATEST_VERSION_RANGE, linker: new PartialInjectableLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareInjector, [
{range: LATEST_VERSION_RANGE, linker: new PartialInjectorLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareNgModule, [
{
range: LATEST_VERSION_RANGE,
linker: new PartialNgModuleLinkerVersion1(environment.options.linkerJitMode)
},
]);
linkers.set(ɵɵngDeclarePipe, [
{range: LATEST_VERSION_RANGE, linker: new PartialPipeLinkerVersion1()},
]);
return linkers;
}
/**
* A helper that selects the appropriate `PartialLinker` for a given declaration.
*
* The selection is made from a database of linker instances, chosen if their given semver range
* satisfies the version found in the code to be linked.
* satisfies the `minVersion` of the partial declaration to be linked.
*
* Note that the ranges are checked in order, and the first matching range will be selected, so
* ranges should be most restrictive first.
* Note that the ranges are checked in order, and the first matching range will be selected. So
* ranges should be most restrictive first. In practice, since ranges are always `<=X.Y.Z` this
* means that ranges should be in ascending order.
*
* Also, ranges are matched to include "pre-releases", therefore if the range is `>=11.1.0-next.1`
* then this includes `11.1.0-next.2` and also `12.0.0-next.1`.
*
* Finally, note that we always start with the current version (i.e. `0.0.0-PLACEHOLDER`). This
* allows the linker to work on local builds effectively.
* Note that any "pre-release" versions are stripped from ranges. Therefore if a `minVersion` is
* `11.1.0-next.1` then this would match `11.1.0-next.2` and also `12.0.0-next.1`. (This is
* different to standard semver range checking, where pre-release versions do not cross full version
* boundaries.)
*/
export class PartialLinkerSelector<TStatement, TExpression> {
private readonly linkers: Map<string, LinkerRange<TExpression>[]>;
export class PartialLinkerSelector<TExpression> {
constructor(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string) {
this.linkers = this.createLinkerMap(environment, sourceUrl, code);
}
private readonly linkers: Map<string, LinkerRange<TExpression>[]>,
private readonly logger: Logger,
private readonly unknownDeclarationVersionHandling: 'ignore'|'warn'|'error') {}
/**
* Returns true if there are `PartialLinker` classes that can handle functions with this name.
@@ -70,59 +137,55 @@ export class PartialLinkerSelector<TStatement, TExpression> {
* Returns the `PartialLinker` that can handle functions with the given name and version.
* Throws an error if there is none.
*/
getLinker(functionName: string, version: string): PartialLinker<TExpression> {
getLinker(functionName: string, minVersion: string, version: string): PartialLinker<TExpression> {
if (!this.linkers.has(functionName)) {
throw new Error(`Unknown partial declaration function ${functionName}.`);
}
const versions = this.linkers.get(functionName)!;
for (const {range, linker} of versions) {
if (satisfies(version, range, {includePrerelease: true})) {
const linkerRanges = this.linkers.get(functionName)!;
if (version === '0.0.0-PLACEHOLDER') {
// Special case if the `version` is the same as the current compiler version.
// This helps with compliance tests where the version placeholders have not been replaced.
return linkerRanges[linkerRanges.length - 1].linker;
}
const declarationRange = getRange('>=', minVersion);
for (const {range: linkerRange, linker} of linkerRanges) {
if (intersects(declarationRange, linkerRange)) {
return linker;
}
}
throw new Error(
`Unsupported partial declaration version ${version} for ${functionName}.\n` +
'Valid version ranges are:\n' + versions.map(v => ` - ${v.range}`).join('\n'));
}
private createLinkerMap(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string): Map<string, LinkerRange<TExpression>[]> {
const partialDirectiveLinkerVersion1 = new PartialDirectiveLinkerVersion1(sourceUrl, code);
const partialComponentLinkerVersion1 = new PartialComponentLinkerVersion1(
environment, createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), sourceUrl,
code);
const partialFactoryLinkerVersion1 = new PartialFactoryLinkerVersion1();
const partialInjectorLinkerVersion1 = new PartialInjectorLinkerVersion1();
const partialNgModuleLinkerVersion1 =
new PartialNgModuleLinkerVersion1(environment.options.linkerJitMode);
const partialPipeLinkerVersion1 = new PartialPipeLinkerVersion1();
const message =
`This application depends upon a library published using Angular version ${version}, ` +
`which requires Angular version ${minVersion} or newer to work correctly.\n` +
`Consider upgrading your application to use a more recent version of Angular.`;
const linkers = new Map<string, LinkerRange<TExpression>[]>();
linkers.set(ɵɵngDeclareDirective, [
{range: '0.0.0-PLACEHOLDER', linker: partialDirectiveLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialDirectiveLinkerVersion1},
]);
linkers.set(ɵɵngDeclareComponent, [
{range: '0.0.0-PLACEHOLDER', linker: partialComponentLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialComponentLinkerVersion1},
]);
linkers.set(ɵɵngDeclareFactory, [
{range: '0.0.0-PLACEHOLDER', linker: partialFactoryLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialFactoryLinkerVersion1},
]);
linkers.set(ɵɵngDeclareInjector, [
{range: '0.0.0-PLACEHOLDER', linker: partialInjectorLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialInjectorLinkerVersion1},
]);
linkers.set(ɵɵngDeclareNgModule, [
{range: '0.0.0-PLACEHOLDER', linker: partialNgModuleLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialNgModuleLinkerVersion1},
]);
linkers.set(ɵɵngDeclarePipe, [
{range: '0.0.0-PLACEHOLDER', linker: partialPipeLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialPipeLinkerVersion1},
]);
return linkers;
if (this.unknownDeclarationVersionHandling === 'error') {
throw new Error(message);
} else if (this.unknownDeclarationVersionHandling === 'warn') {
this.logger.warn(`${message}\nAttempting to continue using this version of Angular.`);
}
// No linker was matched for this declaration, so just use the most recent one.
return linkerRanges[linkerRanges.length - 1].linker;
}
}
/**
* Compute a semver Range from the `version` and comparator.
*
* The range is computed as any version greater/less than or equal to the given `versionStr`
* depending upon the `comparator` (ignoring any prerelease versions).
*
* @param comparator a string that determines whether the version specifies a minimum or a maximum
* range.
* @param versionStr the version given in the partial declaration
* @returns A semver range for the provided `version` and comparator.
*/
function getRange(comparator: '<='|'>=', versionStr: string): Range {
const version = new SemVer(versionStr);
// Wipe out any prerelease versions
version.prerelease = [];
return new Range(`${comparator}${version.format()}`);
}
@@ -5,9 +5,10 @@
* 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 {R3Reference} from '@angular/compiler';
import {createR3ProviderExpression, R3DeclareDependencyMetadata, R3DependencyMetadata, R3ProviderExpression, R3Reference} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstValue} from '../../ast/ast_value';
import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
export function wrapReference<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
@@ -29,3 +30,66 @@ export function parseEnum<TExpression, TEnum>(
}
return enumValue;
}
/**
* Parse a dependency structure from an AST object.
*/
export function getDependency<TExpression>(
depObj: AstObject<R3DeclareDependencyMetadata, TExpression>): R3DependencyMetadata {
const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute');
const token = depObj.getOpaque('token');
// Normally `attribute` is a string literal and so its `attributeNameType` is the same string
// literal. If the `attribute` is some other expression, the `attributeNameType` would be the
// `unknown` type. It is not possible to generate this when linking, since it only deals with JS
// and not typings. When linking the existence of the `attributeNameType` only acts as a marker to
// change the injection instruction that is generated, so we just pass the literal string
// `"unknown"`.
const attributeNameType = isAttribute ? o.literal('unknown') : null;
return {
token,
attributeNameType,
host: depObj.has('host') && depObj.getBoolean('host'),
optional: depObj.has('optional') && depObj.getBoolean('optional'),
self: depObj.has('self') && depObj.getBoolean('self'),
skipSelf: depObj.has('skipSelf') && depObj.getBoolean('skipSelf'),
};
}
/**
* Return an `R3ProviderExpression` that represents either the extracted type reference expression
* from a `forwardRef` function call, or the type itself.
*
* For example, the expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note
* that this expression is required to be wrapped in a closure, as otherwise the forward reference
* would be resolved before initialization.
*
* If there is no forwardRef call expression then we just return the opaque type.
*/
export function extractForwardRef<TExpression>(expr: AstValue<unknown, TExpression>):
R3ProviderExpression<o.WrappedNodeExpr<TExpression>> {
if (!expr.isCallExpression()) {
return createR3ProviderExpression(expr.getOpaque(), /* isForwardRef */ false);
}
const callee = expr.getCallee();
if (callee.getSymbolName() !== 'forwardRef') {
throw new FatalLinkerError(
callee.expression,
'Unsupported expression, expected a `forwardRef()` call or a type reference');
}
const args = expr.getArguments();
if (args.length !== 1) {
throw new FatalLinkerError(
expr, 'Unsupported `forwardRef(fn)` call, expected a single argument');
}
const wrapperFn = args[0] as AstValue<Function, TExpression>;
if (!wrapperFn.isFunction()) {
throw new FatalLinkerError(
wrapperFn, 'Unsupported `forwardRef(fn)` call, expected its argument to be a function');
}
return createR3ProviderExpression(wrapperFn.getFunctionReturnValue().getOpaque(), true);
}
@@ -16,6 +16,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@types/semver",
"@npm//semver",
"@npm//typescript",
],
)
@@ -94,6 +94,11 @@ describe('TypeScriptAstHost', () => {
expect(host.isBooleanLiteral(expr('false'))).toBe(true);
});
it('should return true if the expression is a minified boolean literal', () => {
expect(host.isBooleanLiteral(expr('!0'))).toBe(true);
expect(host.isBooleanLiteral(expr('!1'))).toBe(true);
});
it('should return false if the expression is not a boolean literal', () => {
expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false);
@@ -104,6 +109,8 @@ describe('TypeScriptAstHost', () => {
expect(host.isBooleanLiteral(expr('null'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false);
expect(host.isBooleanLiteral(expr('!2'))).toBe(false);
expect(host.isBooleanLiteral(expr('~1'))).toBe(false);
});
});
@@ -113,6 +120,11 @@ describe('TypeScriptAstHost', () => {
expect(host.parseBooleanLiteral(expr('false'))).toEqual(false);
});
it('should extract a minified boolean value', () => {
expect(host.parseBooleanLiteral(expr('!0'))).toEqual(true);
expect(host.parseBooleanLiteral(expr('!1'))).toEqual(false);
});
it('should error if the value is not a boolean literal', () => {
expect(() => host.parseBooleanLiteral(expr('"moo"')))
.toThrowError('Unsupported syntax, expected a boolean literal.');
@@ -44,6 +44,7 @@ describe('FileLinker', () => {
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
]);
@@ -53,10 +54,26 @@ describe('FileLinker', () => {
.toThrowError('Unknown partial declaration function foo.');
});
it('should throw an error if the metadata object does not have a `version` property', () => {
it('should throw an error if the metadata object does not have a `minVersion` property', () => {
const {fileLinker} = createFileLinker();
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
]);
expect(
() => fileLinker.linkPartialDeclaration(
'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()))
.toThrowError(`Expected property 'minVersion' to be present.`);
});
it('should throw an error if the metadata object does not have a `version` property', () => {
const {fileLinker} = createFileLinker();
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
]);
expect(
@@ -67,9 +84,10 @@ describe('FileLinker', () => {
it('should throw an error if the metadata object does not have a `ngImport` property', () => {
const {fileLinker} = createFileLinker();
const ngImport = factory.createIdentifier('core');
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'version', quoted: false, value: ngImport},
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
]);
expect(
() => fileLinker.linkPartialDeclaration(
@@ -86,6 +104,7 @@ describe('FileLinker', () => {
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
]);
@@ -107,6 +126,11 @@ describe('FileLinker', () => {
// constant statements.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
{
propertyName: 'minVersion',
quoted: false,
value: factory.createLiteral('0.0.0-PLACEHOLDER')
},
{propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')},
]);
@@ -130,6 +154,11 @@ describe('FileLinker', () => {
// statements to be emitted in an IIFE rather than added to the shared constant scope.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
{
propertyName: 'minVersion',
quoted: false,
value: factory.createLiteral('0.0.0-PLACEHOLDER')
},
{
propertyName: 'version',
quoted: false,
@@ -5,126 +5,119 @@
* 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 {Range} from 'semver';
import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from '../../..';
import {FileSystem} from '../../../../src/ngtsc/file_system';
import {MockFileSystemNative} from '../../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../../src/ngtsc/logging/testing';
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
import {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_linker_1';
import {PartialFactoryLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_factory_linker_1';
import {PartialInjectorLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_injector_linker_1';
import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector';
import {PartialNgModuleLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_ng_module_linker_1';
import {PartialPipeLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_pipe_linker_1';
import {PartialLinker} from '../../../src/file_linker/partial_linkers/partial_linker';
import {LinkerRange, PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector';
describe('PartialLinkerSelector', () => {
const options: LinkerOptions = DEFAULT_LINKER_OPTIONS;
let environment: LinkerEnvironment<ts.Statement, ts.Expression>;
let fs: FileSystem;
let logger: MockLogger;
const linkerA = {name: 'linkerA'} as any;
const linkerA2 = {name: 'linkerA2'} as any;
const linkerB = {name: 'linkerB'} as any;
const linkerB2 = {name: 'linkerB2'} as any;
beforeEach(() => {
fs = new MockFileSystemNative();
const logger = new MockLogger();
environment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
fs, logger, new TypeScriptAstHost(),
new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), options);
logger = new MockLogger();
});
describe('supportsDeclaration()', () => {
it('should return true if there is at least one linker that matches the given function name',
() => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.supportsDeclaration('ɵɵngDeclareDirective')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareFactory')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareInjector')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareNgModule')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclarePipe')).toBe(true);
expect(selector.supportsDeclaration('$foo')).toBe(false);
const selector = createSelector('error');
expect(selector.supportsDeclaration('declareA')).toBe(true);
expect(selector.supportsDeclaration('invalid')).toBe(false);
});
it('should return false for methods on `Object`', () => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
const selector = createSelector('error');
expect(selector.supportsDeclaration('toString')).toBe(false);
});
});
describe('getLinker()', () => {
it('should return the latest linker if the version is "0.0.0-PLACEHOLDER"', () => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareComponent', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialComponentLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareFactory', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialFactoryLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareInjector', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialInjectorLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareNgModule', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialNgModuleLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclarePipe', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialPipeLinkerVersion1);
it('should return the linker that matches the name and version', () => {
const selector = createSelector('error');
expect(selector.getLinker('declareA', '11.1.2', '11.1.4')).toBe(linkerA);
expect(selector.getLinker('declareA', '12.0.0', '12.1.0')).toBe(linkerA);
expect(selector.getLinker('declareA', '12.0.1', '12.1.0')).toBe(linkerA2);
expect(selector.getLinker('declareB', '11.2.5', '11.3.0')).toBe(linkerB);
expect(selector.getLinker('declareB', '12.0.5', '12.0.5')).toBe(linkerB2);
});
it('should return the linker that matches the name and valid full version', () => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.2'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.2.5'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '12.0.0'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
it('should return the linker that matches the name and version, ignoring pre-releases', () => {
const selector = createSelector('error');
expect(selector.getLinker('declareA', '11.1.0-next.1', '11.1.0-next.1')).toBe(linkerA);
expect(selector.getLinker('declareA', '11.1.0-next.7', '11.1.0-next.7')).toBe(linkerA);
expect(selector.getLinker('declareA', '12.0.0-next.7', '12.0.0-next.7')).toBe(linkerA);
expect(selector.getLinker('declareA', '12.0.1-next.7', '12.0.1-next.7')).toBe(linkerA2);
});
it('should return the linker that matches the name and valid pre-release versions', () => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.1'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.7'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '12.0.0-next.7'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
});
it('should return the most recent linker if `version` is `0.0.0-PLACEHOLDER`, regardless of `minVersion`',
() => {
const selector = createSelector('error');
expect(selector.getLinker('declareA', '11.1.2', '0.0.0-PLACEHOLDER')).toBe(linkerA2);
expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '11.1.2')).toBe(linkerA);
expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '0.0.0-PLACEHOLDER'))
.toBe(linkerA2);
});
it('should throw an error if there is no linker that matches the given name or version', () => {
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
// `$foo` is not a valid name, even though `0.0.0-PLACEHOLDER` is a valid version
expect(() => selector.getLinker('$foo', '0.0.0-PLACEHOLDER'))
.toThrowError('Unknown partial declaration function $foo.');
// `$foo` is not a valid name, even though `11.1.0` is a valid version
expect(() => selector.getLinker('$foo', '11.1.0'))
it('should throw an error if there is no linker that matches the given name', () => {
const selector = createSelector('error');
// `$foo` is not a valid name, even though `11.1.2` is a valid version for other declarations
expect(() => selector.getLinker('$foo', '11.1.2', '11.2.0'))
.toThrowError('Unknown partial declaration function $foo.');
});
// There are no linkers earlier than 11.1.0-next.1
expect(() => selector.getLinker('ɵɵngDeclareDirective', '10.2.1'))
.toThrowError(
'Unsupported partial declaration version 10.2.1 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
expect(() => selector.getLinker('ɵɵngDeclareDirective', '11.0.2'))
.toThrowError(
'Unsupported partial declaration version 11.0.2 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
expect(() => selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.0'))
.toThrowError(
'Unsupported partial declaration version 11.1.0-next.0 for ɵɵngDeclareDirective.\n' +
'Valid version ranges are:\n' +
' - 0.0.0-PLACEHOLDER\n' +
' - >=11.1.0-next.1');
describe('[unknown declaration version]', () => {
describe('[unknownDeclarationVersionHandling is "ignore"]', () => {
it('should use the most recent linker, with no log warning', () => {
const selector = createSelector('ignore');
expect(selector.getLinker('declareA', '13.1.0', '13.1.5')).toBe(linkerA2);
expect(logger.logs.warn).toEqual([]);
});
});
describe('[unknownDeclarationVersionHandling is "warn"]', () => {
it('should use the most recent linker and log a warning', () => {
const selector = createSelector('warn');
expect(selector.getLinker('declareA', '13.1.0', '14.0.5')).toBe(linkerA2);
expect(logger.logs.warn).toEqual([
[`This application depends upon a library published using Angular version 14.0.5, ` +
`which requires Angular version 13.1.0 or newer to work correctly.\n` +
`Consider upgrading your application to use a more recent version of Angular.\n` +
'Attempting to continue using this version of Angular.']
]);
});
});
describe('[unknownDeclarationVersionHandling is "error"]', () => {
it('should throw an error', () => {
const selector = createSelector('error');
expect(() => selector.getLinker('declareA', '13.1.0', '14.0.5'))
.toThrowError(
`This application depends upon a library published using Angular version 14.0.5, ` +
`which requires Angular version 13.1.0 or newer to work correctly.\n` +
`Consider upgrading your application to use a more recent version of Angular.`);
});
});
});
});
/**
* Create a selector for testing
*/
function createSelector(unknownDeclarationVersionHandling: 'error'|'warn'|'ignore') {
const linkerMap = new Map<string, LinkerRange<unknown>[]>();
linkerMap.set('declareA', [
{range: new Range('<=12.0.0'), linker: linkerA},
{range: new Range('<=13.0.0'), linker: linkerA2}
]);
linkerMap.set('declareB', [
{range: new Range('<=12.0.0'), linker: linkerB},
{range: new Range('<=12.1.0'), linker: linkerB2},
]);
return new PartialLinkerSelector(linkerMap, logger, unknownDeclarationVersionHandling);
}
});
@@ -14,7 +14,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../src/ngtsc/cycles';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {absoluteFromSourceFile, LogicalFileSystem, ReadonlyFileSystem} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
@@ -107,8 +107,8 @@ export class DecorationAnalyzer {
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
/* usePoisonedData */ false,
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,
CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry,
CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEPENDENCY_TRACKER,
this.injectableRegistry,
/* semanticDepGraphUpdater */ null, !!this.compilerOptions.annotateForClosureCompiler,
NOOP_PERF_RECORDER),
@@ -116,7 +116,7 @@ export class DecorationAnalyzer {
// clang-format off
new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry,
this.fullMetaReader, NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore,
this.fullMetaReader, this.injectableRegistry, this.isCore,
/* semanticDepGraphUpdater */ null,
!!this.compilerOptions.annotateForClosureCompiler,
// In ngcc we want to compile undecorated classes with Angular features. As of
@@ -131,18 +131,17 @@ export class DecorationAnalyzer {
// before injectable factories (so injectable factories can delegate to them)
new PipeDecoratorHandler(
this.reflectionHost, this.evaluator, this.metaRegistry, this.scopeRegistry,
NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore, NOOP_PERF_RECORDER),
this.injectableRegistry, this.isCore, NOOP_PERF_RECORDER),
new InjectableDecoratorHandler(
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
this.reflectionHost, this.isCore,
/* strictCtorDeps */ false, this.injectableRegistry, NOOP_PERF_RECORDER,
/* errorOnDuplicateProv */ false),
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
this.refEmitter,
/* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER,
!!this.compilerOptions.annotateForClosureCompiler, this.injectableRegistry,
NOOP_PERF_RECORDER),
/* factoryTracker */ null, !!this.compilerOptions.annotateForClosureCompiler,
this.injectableRegistry, NOOP_PERF_RECORDER),
];
compiler = new NgccTraitCompiler(this.handlers, this.reflectionHost);
migrations: Migration[] = [
@@ -85,7 +85,7 @@ export class NgccTraitCompiler extends TraitCompiler {
}
class NoIncrementalBuild implements IncrementalBuild<any, any> {
priorWorkFor(sf: ts.SourceFile): any[]|null {
priorAnalysisFor(sf: ts.SourceFile): any[]|null {
return null;
}
@@ -16,11 +16,11 @@ import {DependencyHostBase} from './dependency_host';
* Helper functions for computing dependencies.
*/
export class CommonJsDependencyHost extends DependencyHostBase {
protected canSkipFile(fileContents: string): boolean {
protected override canSkipFile(fileContents: string): boolean {
return !hasRequireCalls(fileContents);
}
protected extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
protected override extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(file, fileContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
@@ -23,7 +23,7 @@ export class DtsDependencyHost extends EsmDependencyHost {
/**
* Attempts to process the `importPath` directly and also inside `@types/...`.
*/
protected processImport(
protected override processImport(
importPath: string, file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>,
missing: Set<string>, deepImports: Set<string>, alreadySeen: Set<AbsoluteFsPath>): boolean {
return super.processImport(importPath, file, dependencies, missing, deepImports, alreadySeen) ||
@@ -23,7 +23,7 @@ export class EsmDependencyHost extends DependencyHostBase {
// It has no relevance to capturing imports.
private scanner = ts.createScanner(ts.ScriptTarget.Latest, /* skipTrivia */ true);
protected canSkipFile(fileContents: string): boolean {
protected override canSkipFile(fileContents: string): boolean {
return !hasImportOrReexportStatements(fileContents);
}
@@ -43,7 +43,7 @@ export class EsmDependencyHost extends DependencyHostBase {
* Specifically, backticked strings are particularly challenging since it is possible
* to recursively nest backticks and TypeScript expressions within each other.
*/
protected extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
protected override extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
const imports = new Set<string>();
const templateStack: ts.SyntaxKind[] = [];
let lastToken: ts.SyntaxKind = ts.SyntaxKind.Unknown;
@@ -17,11 +17,11 @@ import {DependencyHostBase} from './dependency_host';
* Helper functions for computing dependencies.
*/
export class UmdDependencyHost extends DependencyHostBase {
protected canSkipFile(fileContents: string): boolean {
protected override canSkipFile(fileContents: string): boolean {
return !hasRequireCalls(fileContents);
}
protected extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
protected override extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(file, fileContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
@@ -45,7 +45,7 @@ export class ProgramBasedEntryPointFinder extends TracingEntryPointFinder {
* Return an array containing the external import paths that were extracted from the source-files
* of the program defined by the tsconfig.json.
*/
protected getInitialEntryPointPaths(): AbsoluteFsPath[] {
protected override getInitialEntryPointPaths(): AbsoluteFsPath[] {
const moduleResolver = new ModuleResolver(this.fs, this.pathMappings, ['', '.ts', '/index.ts']);
const host = new EsmDependencyHost(this.fs, moduleResolver);
const dependencies = createDependencyInfo();
@@ -71,7 +71,8 @@ export class ProgramBasedEntryPointFinder extends TracingEntryPointFinder {
* @returns the entry-point and its dependencies or `null` if the entry-point is not compiled by
* Angular or cannot be determined.
*/
protected getEntryPointWithDeps(entryPointPath: AbsoluteFsPath): EntryPointWithDependencies|null {
protected override getEntryPointWithDeps(entryPointPath: AbsoluteFsPath):
EntryPointWithDependencies|null {
const entryPoints = this.findOrLoadEntryPoints();
if (!entryPoints.has(entryPointPath)) {
return null;
@@ -35,7 +35,7 @@ export class TargetedEntryPointFinder extends TracingEntryPointFinder {
* Search for Angular entry-points that can be reached from the entry-point specified by the given
* `targetPath`.
*/
findEntryPoints(): SortedEntryPointsInfo {
override findEntryPoints(): SortedEntryPointsInfo {
const entryPoints = super.findEntryPoints();
const invalidTarget =
@@ -83,7 +83,7 @@ export class TargetedEntryPointFinder extends TracingEntryPointFinder {
/**
* Return an array containing the `targetPath` from which to start the trace.
*/
protected getInitialEntryPointPaths(): AbsoluteFsPath[] {
protected override getInitialEntryPointPaths(): AbsoluteFsPath[] {
return [this.targetPath];
}
@@ -97,7 +97,8 @@ export class TargetedEntryPointFinder extends TracingEntryPointFinder {
* @returns the entry-point and its dependencies or `null` if the entry-point is not compiled by
* Angular or cannot be determined.
*/
protected getEntryPointWithDeps(entryPointPath: AbsoluteFsPath): EntryPointWithDependencies|null {
protected override getEntryPointWithDeps(entryPointPath: AbsoluteFsPath):
EntryPointWithDependencies|null {
const packagePath = this.computePackagePath(entryPointPath);
const entryPoint =
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath);
@@ -27,7 +27,7 @@ export class ParallelTaskQueue extends BaseTaskQueue {
this.blockedTasks = getBlockedTasks(dependencies);
}
computeNextTask(): Task|null {
override computeNextTask(): Task|null {
// Look for the first available (i.e. not blocked) task.
// (NOTE: Since tasks are sorted by priority, the first available one is the best choice.)
const nextTaskIdx = this.tasks.findIndex(task => !this.blockedTasks.has(task));
@@ -41,7 +41,7 @@ export class ParallelTaskQueue extends BaseTaskQueue {
return nextTask;
}
markAsCompleted(task: Task): void {
override markAsCompleted(task: Task): void {
super.markAsCompleted(task);
if (!this.dependencies.has(task)) {
@@ -62,7 +62,7 @@ export class ParallelTaskQueue extends BaseTaskQueue {
}
}
toString(): string {
override toString(): string {
return `${super.toString()}\n` +
` Blocked tasks (${this.blockedTasks.size}): ${this.stringifyBlockedTasks(' ')}`;
}
@@ -17,7 +17,7 @@ import {BaseTaskQueue} from './base_task_queue';
* before requesting the next one.
*/
export class SerialTaskQueue extends BaseTaskQueue {
computeNextTask(): Task|null {
override computeNextTask(): Task|null {
const nextTask = this.tasks.shift() || null;
if (nextTask) {
@@ -35,7 +35,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
this.compilerHost = src.host;
}
getImportOfIdentifier(id: ts.Identifier): Import|null {
override getImportOfIdentifier(id: ts.Identifier): Import|null {
const requireCall = this.findCommonJsImport(id);
if (requireCall === null) {
return null;
@@ -43,11 +43,11 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
return {from: requireCall.arguments[0].text, name: id.text};
}
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
override getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
return this.getCommonJsModuleDeclaration(id) || super.getDeclarationOfIdentifier(id);
}
getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
override getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
return super.getExportsOfModule(module) || this.commonJsExports.get(module.getSourceFile());
}
@@ -64,7 +64,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
* in.
* @returns an array of nodes of calls to the helper with the given name.
*/
protected getHelperCallsForClass(classSymbol: NgccClassSymbol, helperNames: string[]):
protected override getHelperCallsForClass(classSymbol: NgccClassSymbol, helperNames: string[]):
ts.CallExpression[] {
const esm5HelperCalls = super.getHelperCallsForClass(classSymbol, helperNames);
if (esm5HelperCalls.length > 0) {
@@ -221,7 +221,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
* If this is an IFE then try to grab the outer and inner classes otherwise fallback on the super
* class.
*/
protected getDeclarationOfExpression(expression: ts.Expression): Declaration|null {
protected override getDeclarationOfExpression(expression: ts.Expression): Declaration|null {
const inner = getInnerClassDeclaration(expression);
if (inner !== null) {
const outer = getOuterNodeFromInnerDeclaration(inner);
@@ -161,4 +161,8 @@ export class DelegatingReflectionHost implements NgccReflectionHost {
detectKnownDeclaration<T extends Declaration>(decl: T): T {
return this.ngccHost.detectKnownDeclaration(decl);
}
isStaticallyExported(clazz: ClassDeclaration): boolean {
return this.ngccHost.isStaticallyExported(clazz);
}
}
@@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, DeclarationNode, Decorator, EnumMember, Import, isConcreteDeclaration, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {isSymbolWithValueDeclaration, SymbolWithValueDeclaration} from '../../../src/ngtsc/util/src/typescript';
import {isWithinPackage} from '../analysis/util';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
@@ -150,7 +151,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* `null` if either no decorators were present or if the declaration is not of a decoratable
* type.
*/
getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null {
override getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null {
const symbol = this.getClassSymbol(declaration);
if (!symbol) {
return null;
@@ -168,7 +169,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
override getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`);
@@ -191,7 +192,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null {
override getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(
@@ -204,7 +205,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return null;
}
getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
override getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
// First try getting the base class from an ES2015 class declaration
const superBaseClassIdentifier = super.getBaseClassExpression(clazz);
if (superBaseClassIdentifier) {
@@ -213,14 +214,14 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
// That didn't work so now try getting it from the "inner" declaration.
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined ||
if (classSymbol?.implementation.valueDeclaration === undefined ||
!isNamedDeclaration(classSymbol.implementation.valueDeclaration)) {
return null;
}
return super.getBaseClassExpression(classSymbol.implementation.valueDeclaration);
}
getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier {
override getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier {
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
throw new Error(`getInternalNameOfClass() called on a non-class: expected ${
@@ -230,7 +231,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
classSymbol, classSymbol.implementation.valueDeclaration);
}
getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier {
override getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier {
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
throw new Error(`getAdjacentNameOfClass() called on a non-class: expected ${
@@ -241,7 +242,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
private getNameFromClassSymbolDeclaration(
classSymbol: NgccClassSymbol, declaration: ts.Declaration): ts.Identifier {
classSymbol: NgccClassSymbol, declaration: ts.Declaration|undefined): ts.Identifier {
if (declaration === undefined) {
throw new Error(
`getInternalNameOfClass() called on a class with an undefined internal declaration. External class name: ${
@@ -258,7 +259,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/**
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ClassDeclaration {
override isClass(node: ts.Node): node is ClassDeclaration {
return super.isClass(node) || this.getClassSymbol(node) !== undefined;
}
@@ -278,7 +279,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
* otherwise.
*/
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
override getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
const superDeclaration = super.getDeclarationOfIdentifier(id);
// If no declaration was found, return.
@@ -354,7 +355,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
[];
}
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null {
override getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null {
const value = super.getVariableValue(declaration);
if (value) {
return value;
@@ -433,7 +434,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns the number of type parameters of the class, if known, or `null` if the declaration
* is not a class or has an unknown number of type parameters.
*/
getGenericArityOfClass(clazz: ClassDeclaration): number|null {
override getGenericArityOfClass(clazz: ClassDeclaration): number|null {
const dtsDeclaration = this.getDtsDeclaration(clazz);
if (dtsDeclaration && ts.isClassDeclaration(dtsDeclaration)) {
return dtsDeclaration.typeParameters ? dtsDeclaration.typeParameters.length : 0;
@@ -453,7 +454,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclaration(declaration: DeclarationNode): ts.Declaration|null {
override getDtsDeclaration(declaration: DeclarationNode): ts.Declaration|null {
if (this.dts === null) {
return null;
}
@@ -721,12 +722,12 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return undefined;
}
let implementationSymbol = declarationSymbol;
let implementationSymbol: ts.Symbol|undefined = declarationSymbol;
if (innerDeclaration !== null && isNamedDeclaration(innerDeclaration)) {
implementationSymbol = this.checker.getSymbolAtLocation(innerDeclaration.name) as ClassSymbol;
implementationSymbol = this.checker.getSymbolAtLocation(innerDeclaration.name);
}
if (implementationSymbol === undefined) {
if (!isSymbolWithValueDeclaration(implementationSymbol)) {
return undefined;
}
@@ -740,8 +741,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return classSymbol;
}
private getAdjacentSymbol(declarationSymbol: ClassSymbol, implementationSymbol: ClassSymbol):
ClassSymbol|undefined {
private getAdjacentSymbol(
declarationSymbol: ClassSymbol,
implementationSymbol: SymbolWithValueDeclaration): SymbolWithValueDeclaration|undefined {
if (declarationSymbol === implementationSymbol) {
return undefined;
}
@@ -755,9 +757,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (adjacentDeclaration === undefined || !isNamedVariableDeclaration(adjacentDeclaration)) {
return undefined;
}
const adjacentSymbol =
this.checker.getSymbolAtLocation(adjacentDeclaration.name) as ClassSymbol;
if (adjacentSymbol === declarationSymbol || adjacentSymbol === implementationSymbol) {
const adjacentSymbol = this.checker.getSymbolAtLocation(adjacentDeclaration.name);
if (adjacentSymbol === declarationSymbol || adjacentSymbol === implementationSymbol ||
!isSymbolWithValueDeclaration(adjacentSymbol)) {
return undefined;
}
return adjacentSymbol;
@@ -767,8 +769,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known
* declaration.
*/
protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration
|null {
protected override getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null):
Declaration|null {
const declaration = super.getDeclarationOfSymbol(symbol, originalId);
if (declaration === null) {
return null;
@@ -34,7 +34,7 @@ import {NgccClassSymbol} from './ngcc_host';
*
*/
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
override getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
const superBaseClassExpression = super.getBaseClassExpression(clazz);
if (superBaseClassExpression !== null) {
return superBaseClassExpression;
@@ -71,7 +71,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
* otherwise.
*/
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
override getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
const declaration = super.getDeclarationOfIdentifier(id);
if (declaration === null) {
@@ -127,7 +127,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @param node the function declaration to parse.
* @returns an object containing the node, statements and parameters of the function.
*/
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
override getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
const definition = super.getDefinitionOfFunction(node);
if (definition === null) {
return null;
@@ -155,7 +155,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @param decl The `Declaration` to check.
* @return The passed in `Declaration` (potentially enhanced with a `KnownDeclaration`).
*/
detectKnownDeclaration<T extends Declaration>(decl: T): T {
override detectKnownDeclaration<T extends Declaration>(decl: T): T {
decl = super.detectKnownDeclaration(decl);
// Also check for TS helpers
@@ -180,7 +180,8 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
protected override getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol
|undefined {
const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
if (classSymbol !== undefined) {
return classSymbol;
@@ -210,7 +211,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
* the class's constructor or `null` if there is no constructor.
*/
protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol):
protected override getConstructorParameterDeclarations(classSymbol: NgccClassSymbol):
ts.ParameterDeclaration[]|null {
const constructor = classSymbol.implementation.valueDeclaration;
if (!ts.isFunctionDeclaration(constructor)) return null;
@@ -248,7 +249,8 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @param paramDecoratorsProperty the property that holds the parameter info we want to get.
* @returns an array of objects containing the type and decorators for each parameter.
*/
protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null {
protected override getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol):
ParamInfo[]|null {
const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty);
// The decorators array may be wrapped in a function. If so unwrap it.
const returnStatement = getReturnStatement(paramDecorators);
@@ -284,15 +286,15 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @param isStatic true if this member is static, false if it is an instance property.
* @returns the reflected member information, or null if the symbol is not a member.
*/
protected reflectMembers(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean):
ClassMember[]|null {
protected override reflectMembers(
symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean): ClassMember[]|null {
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
const propertyDefinition = node && getPropertyDefinition(node);
if (propertyDefinition) {
const members: ClassMember[] = [];
if (propertyDefinition.setter) {
members.push({
node,
node: node!,
implementation: propertyDefinition.setter,
kind: ClassMemberKind.Setter,
type: null,
@@ -310,7 +312,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
}
if (propertyDefinition.getter) {
members.push({
node,
node: node!,
implementation: propertyDefinition.getter,
kind: ClassMemberKind.Getter,
type: null,
@@ -348,7 +350,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* to reference the inner identifier inside the IIFE.
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
protected override getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
@@ -8,6 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, Declaration, Decorator, ReflectionHost} from '../../../src/ngtsc/reflection';
import {SymbolWithValueDeclaration} from '../../../src/ngtsc/util/src/typescript';
export const PRE_R3_MARKER = '__PRE_R3__';
export const POST_R3_MARKER = '__POST_R3__';
@@ -47,13 +48,13 @@ export interface NgccClassSymbol {
* inner declaration does not need to satisfy the requirements imposed on a publicly visible class
* declaration.
*/
implementation: ts.Symbol;
implementation: SymbolWithValueDeclaration;
/**
* Represents the symbol corresponding to a variable within a class IIFE that may be used to
* attach static properties or decorated.
*/
adjacent?: ts.Symbol;
adjacent?: SymbolWithValueDeclaration;
}
/**
+12 -10
View File
@@ -36,7 +36,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
this.compilerHost = src.host;
}
getImportOfIdentifier(id: ts.Identifier): Import|null {
override getImportOfIdentifier(id: ts.Identifier): Import|null {
// Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`?
// If so capture the symbol of the namespace, e.g. `core`.
const nsIdentifier = findNamespaceOfIdentifier(id);
@@ -45,7 +45,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
return from !== null ? {from, name: id.text} : null;
}
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
override getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
// First we try one of the following:
// 1. The `exports` identifier - referring to the current file/module.
// 2. An identifier (e.g. `foo`) that refers to an imported UMD module.
@@ -83,7 +83,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
};
}
getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
override getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
return super.getExportsOfModule(module) || this.umdExports.get(module.getSourceFile());
}
@@ -107,12 +107,13 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
* @param sourceFile The module whose statements we want.
* @returns An array of top level statements for the given module.
*/
protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] {
protected override getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] {
const umdModule = this.getUmdModule(sourceFile);
return umdModule !== null ? Array.from(umdModule.factoryFn.body.statements) : [];
}
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
protected override getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol
|undefined {
const superSymbol = super.getClassSymbolFromOuterDeclaration(declaration);
if (superSymbol) {
return superSymbol;
@@ -143,7 +144,8 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
}
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
protected override getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol
|undefined {
const superClassSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
if (superClassSymbol !== undefined) {
return superClassSymbol;
@@ -164,7 +166,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
/**
* Extract all "classes" from the `statement` and add them to the `classes` map.
*/
protected addClassSymbolsFromStatement(
protected override addClassSymbolsFromStatement(
classes: Map<ts.Symbol, NgccClassSymbol>, statement: ts.Statement): void {
super.addClassSymbolsFromStatement(classes, statement);
@@ -184,7 +186,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
*
* @param statement The statement that needs to be preprocessed.
*/
protected preprocessStatement(statement: ts.Statement): void {
protected override preprocessStatement(statement: ts.Statement): void {
super.preprocessStatement(statement);
if (!isExportsStatement(statement)) {
@@ -421,7 +423,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
const exportsSymbol = this.checker.getSymbolsInScope(id, ts.SymbolFlags.Variable)
.find(symbol => symbol.name === 'exports');
const node = exportsSymbol !== undefined &&
const node = exportsSymbol?.valueDeclaration !== undefined &&
!ts.isFunctionExpression(exportsSymbol.valueDeclaration.parent) ?
// There is a locally defined `exports` variable that is not a function parameter.
// So this `exports` identifier must be a local variable and does not represent the module.
@@ -474,7 +476,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
* If this is an IIFE then try to grab the outer and inner classes otherwise fallback on the super
* class.
*/
protected getDeclarationOfExpression(expression: ts.Expression): Declaration|null {
protected override getDeclarationOfExpression(expression: ts.Expression): Declaration|null {
const inner = getInnerClassDeclaration(expression);
if (inner !== null) {
const outer = getOuterNodeFromInnerDeclaration(inner);
@@ -25,6 +25,12 @@ export interface NgccProjectConfig<T = RawNgccPackageConfig> {
* Options that control how locking the process is handled.
*/
locking?: ProcessLockingConfiguration;
/**
* Name of hash algorithm used to generate hashes of the configuration.
*
* Defaults to `sha256`.
*/
hashAlgorithm?: string;
}
/**
@@ -230,10 +236,12 @@ export class NgccConfiguration {
private projectConfig: PartiallyProcessedConfig;
private cache = new Map<string, VersionedPackageConfig>();
readonly hash: string;
readonly hashAlgorithm: string;
constructor(private fs: ReadonlyFileSystem, baseDir: AbsoluteFsPath) {
this.defaultConfig = this.processProjectConfig(DEFAULT_NGCC_CONFIG);
this.projectConfig = this.processProjectConfig(this.loadProjectConfig(baseDir));
this.hashAlgorithm = this.projectConfig.hashAlgorithm;
this.hash = this.computeHash();
}
@@ -299,7 +307,8 @@ export class NgccConfiguration {
}
private processProjectConfig(projectConfig: NgccProjectConfig): PartiallyProcessedConfig {
const processedConfig: PartiallyProcessedConfig = {packages: {}, locking: {}};
const processedConfig:
PartiallyProcessedConfig = {packages: {}, locking: {}, hashAlgorithm: 'sha256'};
// locking configuration
if (projectConfig.locking !== undefined) {
@@ -317,6 +326,11 @@ export class NgccConfiguration {
}
}
// hash algorithm config
if (projectConfig.hashAlgorithm !== undefined) {
processedConfig.hashAlgorithm = projectConfig.hashAlgorithm;
}
return processedConfig;
}
@@ -378,7 +392,7 @@ export class NgccConfiguration {
}
private computeHash(): string {
return createHash('md5').update(JSON.stringify(this.projectConfig)).digest('hex');
return createHash(this.hashAlgorithm).update(JSON.stringify(this.projectConfig)).digest('hex');
}
}
@@ -159,7 +159,7 @@ export class EntryPointManifest {
const lockFilePath = this.fs.resolve(directory, lockFileName);
if (this.fs.exists(lockFilePath)) {
const lockFileContents = this.fs.readFile(lockFilePath);
return createHash('md5').update(lockFileContents).digest('hex');
return createHash(this.config.hashAlgorithm).update(lockFileContents).digest('hex');
}
}
return null;
@@ -175,7 +175,8 @@ export class EntryPointManifest {
* is called.
*/
export class InvalidatingEntryPointManifest extends EntryPointManifest {
readEntryPointsUsingManifest(_basePath: AbsoluteFsPath): EntryPointWithDependencies[]|null {
override readEntryPointsUsingManifest(_basePath: AbsoluteFsPath):
EntryPointWithDependencies[]|null {
return null;
}
}
@@ -26,7 +26,8 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
override getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile
|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
@@ -78,7 +79,8 @@ export class NgccDtsCompilerHost extends NgtscCompilerHost {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
override getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile
|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
@@ -31,7 +31,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
/**
* Add the imports below any in situ imports as `require` calls.
*/
addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
override addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
// Avoid unnecessary work if there are no imports to add.
if (imports.length === 0) {
return;
@@ -46,7 +46,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
/**
* Add the exports to the bottom of the file.
*/
addExports(
override addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void {
exports.forEach(e => {
@@ -61,7 +61,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
});
}
addDirectExports(
override addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void {
for (const e of exports) {
@@ -72,7 +72,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
}
}
protected findEndOfImports(sf: ts.SourceFile): number {
protected override findEndOfImports(sf: ts.SourceFile): number {
for (const statement of sf.statements) {
if (ts.isExpressionStatement(statement) && isRequireCall(statement.expression)) {
continue;
@@ -24,7 +24,8 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
* Add the definitions, directly before the return statement, inside the IIFE of each decorated
* class.
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
override addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string):
void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(
@@ -63,7 +64,8 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
*
* @return The JavaScript code corresponding to `stmt` (in the appropriate format).
*/
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
override printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager):
string {
const node = translateStatement(
stmt, importManager, {downlevelTaggedTemplates: true, downlevelVariableDeclarations: true});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
@@ -50,7 +50,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
*
* (See that the `z` import is not being used by the factory function.)
*/
addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
override addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
if (imports.length === 0) {
return;
}
@@ -73,7 +73,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
/**
* Add the exports to the bottom of the UMD module factory function.
*/
addExports(
override addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void {
const umdModule = this.umdHost.getUmdModule(file);
@@ -97,7 +97,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
});
}
addDirectExports(
override addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void {
const umdModule = this.umdHost.getUmdModule(file);
@@ -120,7 +120,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
/**
* Add the constants to the top of the UMD factory function.
*/
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
override addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
if (constants === '') {
return;
}
@@ -34,7 +34,7 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter {
super(fs, logger, errorOnFailedEntryPoint);
}
writeBundle(
override writeBundle(
bundle: EntryPointBundle, transformedFiles: FileToWrite[],
formatProperties: EntryPointJsonProperty[]) {
// The new folder is at the root of the overall package
@@ -45,7 +45,7 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter {
this.updatePackageJson(entryPoint, formatProperties, ngccFolder);
}
revertBundle(
override revertBundle(
entryPoint: EntryPoint, transformedFilePaths: AbsoluteFsPath[],
formatProperties: EntryPointJsonProperty[]): void {
// IMPLEMENTATION NOTE:
@@ -323,7 +323,8 @@ class TestHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
}
class AlwaysDetectHandler extends TestHandler {
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
override detect(node: ClassDeclaration, decorators: Decorator[]|null):
DetectResult<unknown>|undefined {
super.detect(node, decorators);
const decorator = decorators !== null ? decorators[0] : null;
return {trigger: node, decorator, metadata: {}};
@@ -331,11 +332,12 @@ class AlwaysDetectHandler extends TestHandler {
}
class DetectDecoratorHandler extends TestHandler {
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {
constructor(private decorator: string, override readonly precedence: HandlerPrecedence) {
super(decorator, []);
}
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
override detect(node: ClassDeclaration, decorators: Decorator[]|null):
DetectResult<unknown>|undefined {
super.detect(node, decorators);
if (decorators === null) {
return undefined;
@@ -349,7 +351,7 @@ class DetectDecoratorHandler extends TestHandler {
}
class DiagnosticProducingHandler extends AlwaysDetectHandler {
analyze(node: ClassDeclaration): AnalysisOutput<any> {
override analyze(node: ClassDeclaration): AnalysisOutput<any> {
super.analyze(node);
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
@@ -7,7 +7,7 @@
*/
import {DepGraph} from 'dependency-graph';
import {DtsProcessing, PartiallyOrderedTasks, Task} from '../../src/execution/tasks/api';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPoint, EntryPointJsonProperty} from '../../src/packages/entry_point';
/**
* Create a set of tasks and a graph of their interdependencies.
@@ -53,7 +53,13 @@ export function createTasksAndGraph(
for (let tIdx = 0; tIdx < tasksPerEntryPointCount; tIdx++) {
const processDts = tIdx === 0 ? DtsProcessing.Yes : DtsProcessing.No;
tasks.push({entryPoint, formatProperty: `prop-${tIdx}`, processDts} as Task);
const formatProperty = `prop-${tIdx}` as EntryPointJsonProperty;
tasks.push({
entryPoint,
formatProperty: formatProperty,
formatPropertiesToMarkAsProcessed: [],
processDts
});
}
}
@@ -33,7 +33,12 @@ describe('SerialTaskQueue', () => {
const entryPoint = {name: `entry-point-${i}`, path: `/path/to/entry/point/${i}`} as
EntryPoint;
const processDts = i % 2 === 0 ? DtsProcessing.Yes : DtsProcessing.No;
tasks.push({entryPoint: entryPoint, formatProperty: `prop-${i}`, processDts} as Task);
tasks.push({
entryPoint: entryPoint,
formatProperty: `prop-${i}`,
formatPropertiesToMarkAsProcessed: [],
processDts
} as Task);
graph.addNode(entryPoint.path);
}
const dependencies = computeTaskDependencies(tasks, graph);
@@ -2506,7 +2506,7 @@ runInEachFileSystem(() => {
const externalLibWithoutTypingsIndex = _('/an_external_lib_without_typings/index.js');
class TestEsm2015ReflectionHost extends Esm2015ReflectionHost {
getExportsOfModule(node: ts.Node) {
override getExportsOfModule(node: ts.Node) {
if (ts.isSourceFile(node) && (node.fileName === externalLibWithoutTypingsIndex)) {
throw new Error(
`'getExportsOfModule()' called on '${externalLibWithoutTypingsIndex}'.`);
@@ -829,7 +829,7 @@ runInEachFileSystem(() => {
it('should use the correct type name in typings files when an export has a different name in source files',
() => {
// We need to make sure that changes to the typings files use the correct name
// static ɵprov: ɵngcc0.ɵɵInjectableDef<ɵangular_packages_common_common_a>;
// static ɵprov: ɵngcc0.ɵɵInjectableDeclaration<ɵangular_packages_common_common_a>;
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common',
@@ -28,20 +28,20 @@ runInEachFileSystem(() => {
super(fs, new MockLogger());
fs.ensureDir(fs.dirname(this.path));
}
remove() {
override remove() {
this.log.push('remove()');
super.remove();
}
write() {
override write() {
this.log.push('write()');
super.write();
}
read() {
override read() {
const contents = super.read();
this.log.push('read() => ' + contents);
return contents;
}
createUnlocker(): ChildProcess {
override createUnlocker(): ChildProcess {
this.log = this.log || [];
this.log.push('createUnlocker()');
const log = this.log;
@@ -48,9 +48,9 @@ runInEachFileSystem(() => {
}]);
const project1Conf = new NgccConfiguration(fs, project1);
const expectedProject1Config =
`{"packages":{"package-1":[{"entryPoints":{"./entry-point-1":{}},"versionRange":"*"}]},"locking":{}}`;
`{"packages":{"package-1":[{"entryPoints":{"./entry-point-1":{}},"versionRange":"*"}]},"locking":{},"hashAlgorithm":"sha256"}`;
expect(project1Conf.hash)
.toEqual(createHash('md5').update(expectedProject1Config).digest('hex'));
.toEqual(createHash('sha256').update(expectedProject1Config).digest('hex'));
const project2 = _Abs('/project-2');
const project2Config = fs.resolve(project2, 'ngcc.config.js');
@@ -66,18 +66,41 @@ runInEachFileSystem(() => {
}]);
const project2Conf = new NgccConfiguration(fs, project2);
const expectedProject2Config =
`{"packages":{"package-1":[{"entryPoints":{"./entry-point-1":{"ignore":true}},"versionRange":"*"}]},"locking":{}}`;
`{"packages":{"package-1":[{"entryPoints":{"./entry-point-1":{"ignore":true}},"versionRange":"*"}]},"locking":{},"hashAlgorithm":"sha256"}`;
expect(project2Conf.hash)
.toEqual(createHash('md5').update(expectedProject2Config).digest('hex'));
.toEqual(createHash('sha256').update(expectedProject2Config).digest('hex'));
});
it('should compute a hash even if there is no project configuration', () => {
loadTestFiles([{name: _Abs('/project-1/empty.js'), contents: ``}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(configuration.hash)
.toEqual(createHash('md5')
.update(JSON.stringify({packages: {}, locking: {}}))
.digest('hex'));
.toEqual(
createHash('sha256')
.update(JSON.stringify({packages: {}, locking: {}, hashAlgorithm: 'sha256'}))
.digest('hex'));
});
it('should use a custom hash algorithm if specified in the config', () => {
const project1 = _Abs('/project-1');
const project1Config = fs.resolve(project1, 'ngcc.config.js');
loadTestFiles([{
name: project1Config,
contents: `
module.exports = {
packages: {
'package-1': {entryPoints: {'./entry-point-1': {}}},
},
hashAlgorithm: 'md5',
};`
}]);
const project1Conf = new NgccConfiguration(fs, project1);
const expectedProject1Config =
`{"packages":{"package-1":[{"entryPoints":{"./entry-point-1":{}},"versionRange":"*"}]},"locking":{},"hashAlgorithm":"md5"}`;
expect(JSON.stringify((project1Conf as any).projectConfig)).toEqual(expectedProject1Config);
expect(project1Conf.hash)
.toEqual(createHash('md5').update(expectedProject1Config).digest('hex'));
});
});
@@ -40,7 +40,7 @@ runInEachFileSystem(() => {
beforeEach(() => {
manifestFile = {
ngccVersion: NGCC_VERSION,
lockFileHash: createHash('md5').update('LOCK FILE CONTENTS').digest('hex'),
lockFileHash: createHash('sha256').update('LOCK FILE CONTENTS').digest('hex'),
configFileHash: config.hash,
entryPointPaths: []
};
@@ -278,7 +278,7 @@ runInEachFileSystem(() => {
JSON.parse(fs.readFile(_Abs('/project/node_modules/__ngcc_entry_points__.json'))) as
EntryPointManifestFile;
expect(file.lockFileHash)
.toEqual(createHash('md5').update('LOCK FILE CONTENTS').digest('hex'));
.toEqual(createHash('sha256').update('LOCK FILE CONTENTS').digest('hex'));
});
it('should write a hash of the package-lock.json file', () => {
@@ -288,7 +288,7 @@ runInEachFileSystem(() => {
JSON.parse(fs.readFile(_Abs('/project/node_modules/__ngcc_entry_points__.json'))) as
EntryPointManifestFile;
expect(file.lockFileHash)
.toEqual(createHash('md5').update('LOCK FILE CONTENTS').digest('hex'));
.toEqual(createHash('sha256').update('LOCK FILE CONTENTS').digest('hex'));
});
it('should write a hash of the project config', () => {
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Statement} from '@angular/compiler';
import {fromObject, generateMapFileComment, SourceMapConverter} from 'convert-source-map';
import {fromObject, fromSource, generateMapFileComment, SourceMapConverter} from 'convert-source-map';
import MagicString from 'magic-string';
import {encode, SourceMapMappings} from 'sourcemap-codec';
import * as ts from 'typescript';
@@ -201,19 +201,84 @@ runInEachFileSystem(() => {
'file': 'file.js',
'sources': ['file.js'],
'names': [],
'mappings': encode([[], [], [], [], [], [], [], [], [], [], [], [], [[0, 0, 0, 0]]]),
'mappings': encode([
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[[0, 0, 0, 0]],
[[0, 0, 1, 0]],
[[0, 0, 2, 0]],
[[0, 0, 3, 0]],
[[0, 0, 4, 0]],
[[0, 0, 5, 0]],
[[0, 0, 6, 0]],
[[0, 0, 7, 0]],
[[0, 0, 8, 0]]
]),
'sourcesContent': [JS_CONTENT.contents],
});
const MERGED_OUTPUT_PROGRAM_MAPPINGS: SourceMapMappings =
[[], [], [], [], [], [], [], [], [], [], [], [], ...JS_CONTENT_MAPPINGS];
MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3,
'file': 'file.js',
'sources': ['file.ts'],
'names': [],
'mappings': encode(MERGED_OUTPUT_PROGRAM_MAPPINGS),
'mappings': encode([
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[
[0, 0, 0, 0], [7, 0, 0, 7], [9, 0, 0, 8], [18, 0, 0, 17], [20, 0, 0, 18],
[26, 0, 0, 24], [41, 0, 0, 39], [42, 0, 0, 40]
],
[
[0, 0, 2, 0], [4, 0, 2, 13], [5, 0, 2, 14], [8, 0, 2, 0], [14, 0, 2, 13], [15, 0, 2, 14]
],
[[0, 0, 2, 16], [4, 0, 3, 2], [7, 0, 3, 5], [8, 0, 3, 6], [9, 0, 3, 15]],
[
[0, 0, 3, 27], [7, 0, 3, 34], [8, 0, 3, 35], [9, 0, 3, 36], [10, 0, 3, 37],
[11, 0, 3, 38], [1, 0, 4, 1], [2, 0, 4, 1]
],
[[0, 0, 2, 13], [1, 0, 2, 14]],
[[0, 0, 3, 3]],
[
[0, 0, 3, 5], [2, 0, 1, 1], [11, 0, 1, 10], [12, 0, 1, 11], [14, 0, 1, 12],
[3, 0, 2, 13], [4, 0, 2, 14], [5, 0, 4, 1]
],
[
[0, 0, 4, 13], [5, 0, 1, 20], [7, 0, 1, 22], [12, 0, 1, 27], [14, 0, 1, 28],
[15, 0, 1, 29], [9, 0, 2, 13], [10, 0, 2, 14]
],
[[0, 0, 4, 2]],
[],
[
[0, 0, 0, 2], [0, 0, 0, 2], [0, 0, 0, 2], [0, 0, 0, 2], [0, 0, 0, 2], [0, 0, 0, 2],
[0, 0, 0, 2], [0, 0, 0, 2], [0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 2, 2],
[0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2],
[0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 2],
[0, 0, 4, 2], [0, 0, 4, 2], [0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 1, 2], [0, 0, 1, 2],
[0, 0, 1, 2], [0, 0, 1, 2], [0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 4, 2], [0, 0, 1, 2],
[0, 0, 1, 2], [0, 0, 1, 2], [0, 0, 1, 2], [0, 0, 1, 2], [0, 0, 2, 2], [0, 0, 2, 2]
],
]),
'sourcesContent': [TS_CONTENT.contents],
});
});
@@ -226,8 +291,9 @@ runInEachFileSystem(() => {
const [sourceFile, mapFile] = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(sourceFile.path).toEqual(_('/node_modules/test-package/src/file.js'));
expect(sourceFile.contents)
.toEqual(RENDERED_CONTENTS + '\n' + OUTPUT_PROGRAM_MAP.toComment());
expect(sourceFile.contents).toContain(RENDERED_CONTENTS);
expect(fromSource(sourceFile.contents)!.toObject())
.toEqual(OUTPUT_PROGRAM_MAP.toObject());
expect(mapFile).toBeUndefined();
});
@@ -616,8 +682,9 @@ UndecoratedBase.ɵdir = /*@__PURE__*/ ɵngcc0.ɵɵdefineDirective({ type: Undeco
const [sourceFile, mapFile] = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(sourceFile.path).toEqual(_('/node_modules/test-package/src/file.js'));
expect(sourceFile.contents)
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
expect(sourceFile.contents).toContain(RENDERED_CONTENTS);
expect(fromSource(sourceFile.contents)!.toObject())
.toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject());
expect(mapFile).toBeUndefined();
});
+8 -12
View File
@@ -18,21 +18,17 @@
"canonical-path": "1.0.0",
"chokidar": "^3.0.0",
"convert-source-map": "^1.5.1",
"dependency-graph": "^0.7.2",
"fs-extra": "4.0.2",
"dependency-graph": "^0.11.0",
"magic-string": "^0.25.0",
"semver": "^6.3.0",
"semver": "^7.0.0",
"source-map": "^0.6.1",
"sourcemap-codec": "^1.4.8",
"tslib": "^2.1.0",
"yargs": "^16.2.0"
"tslib": "^2.2.0",
"yargs": "^17.0.0"
},
"peerDependencies": {
"@angular/compiler": "0.0.0-PLACEHOLDER",
"typescript": ">=4.2.3 <4.3"
},
"engines": {
"node": ">=10.0"
"typescript": ">=4.2.3 <4.4"
},
"repository": {
"type": "git",
@@ -44,14 +40,14 @@
"compiler"
],
"license": "MIT",
"engines": {
"node": "^12.14.1 || >=14.0.0"
},
"bugs": {
"url": "https://github.com/angular/angular/issues"
},
"homepage": "https://github.com/angular/angular/tree/master/packages/compiler-cli",
"ng-update": {
"packageGroup": "NG_UPDATE_PACKAGE_GROUP"
},
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ export function isMetadataSymbolicExpression(value: any): value is MetadataSymbo
export interface MetadataSymbolicBinaryExpression {
__symbolic: 'binary';
operator: '&&'|'||'|'|'|'^'|'&'|'=='|'!='|'==='|'!=='|'<'|'>'|'<='|'>='|'instanceof'|'in'|'as'|
'<<'|'>>'|'>>>'|'+'|'-'|'*'|'/'|'%'|'**';
'<<'|'>>'|'>>>'|'+'|'-'|'*'|'/'|'%'|'**'|'??';
left: MetadataValue;
right: MetadataValue;
}
@@ -27,6 +27,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//@types/node",
"@npm//typescript",
],
@@ -6,31 +6,32 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, FactoryTarget, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentMetadata, R3FactoryMetadata, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import {compileClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, FactoryTarget, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ClassMetadata, R3ComponentMetadata, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import {ViewEncapsulation} from '@angular/compiler/src/core';
import * as ts from 'typescript';
import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../cycles';
import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../diagnostics';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {DependencyTracker} from '../../incremental/api';
import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaType, Resource, ResourceRegistry} from '../../metadata';
import {EnumValue, PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
import {SubsetOfKeys} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
import {ResourceLoader} from './api';
import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics} from './diagnostics';
import {DirectiveSymbol, extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileDeclareFactory, compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {extractClassMetadata} from './metadata';
import {NgModuleSymbol} from './ng_module';
import {compileResults, findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, toFactoryMetadata, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
@@ -55,7 +56,7 @@ export interface ComponentAnalysisData {
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
typeCheckMeta: DirectiveTypeCheckMeta;
template: ParsedTemplateWithSource;
metadataStmt: Statement|null;
classMetadata: R3ClassMetadata|null;
inputs: ClassPropertyMapping;
outputs: ClassPropertyMapping;
@@ -122,7 +123,8 @@ export class ComponentSymbol extends DirectiveSymbol {
usedPipes: SemanticReference[] = [];
isRemotelyScoped = false;
isEmitAffected(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>): boolean {
override isEmitAffected(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>):
boolean {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
}
@@ -145,7 +147,7 @@ export class ComponentSymbol extends DirectiveSymbol {
!isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isSymbolUnaffected);
}
isTypeCheckBlockAffected(
override isTypeCheckBlockAffected(
previousSymbol: SemanticSymbol, typeCheckApiAffected: Set<SemanticSymbol>): boolean {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
@@ -205,7 +207,6 @@ export class ComponentDecoratorHandler implements
private i18nNormalizeLineEndingsInICUs: boolean|undefined,
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
private cycleHandlingStrategy: CycleHandlingStrategy, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
@@ -262,13 +263,16 @@ export class ComponentDecoratorHandler implements
const component = reflectObjectLiteral(meta);
const containingFile = node.getSourceFile().fileName;
const resolveStyleUrl =
(styleUrl: string, nodeForError: ts.Node,
resourceType: ResourceTypeForDiagnostics): Promise<void>|undefined => {
const resourceUrl =
this._resolveResourceOrThrow(styleUrl, containingFile, nodeForError, resourceType);
return this.resourceLoader.preload(resourceUrl, {type: 'style', containingFile});
};
const resolveStyleUrl = (styleUrl: string): Promise<void>|undefined => {
try {
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
return this.resourceLoader.preload(resourceUrl, {type: 'style', containingFile});
} catch {
// Don't worry about failures to preload. We can handle this problem during analysis by
// producing a diagnostic.
return undefined;
}
};
// A Promise that waits for the template and all <link>ed styles within it to be preloaded.
const templateAndTemplateStyleResources =
@@ -278,12 +282,7 @@ export class ComponentDecoratorHandler implements
return undefined;
}
const nodeForError = getTemplateDeclarationNodeForError(template.declaration);
return Promise
.all(template.styleUrls.map(
styleUrl => resolveStyleUrl(
styleUrl, nodeForError,
ResourceTypeForDiagnostics.StylesheetFromTemplate)))
return Promise.all(template.styleUrls.map(styleUrl => resolveStyleUrl(styleUrl)))
.then(() => undefined);
});
@@ -305,16 +304,15 @@ export class ComponentDecoratorHandler implements
this.preanalyzeStylesCache.set(node, styles);
});
}
} else {
this.preanalyzeStylesCache.set(node, null);
}
// Wait for both the template and all styleUrl resources to resolve.
return Promise
.all([
templateAndTemplateStyleResources, inlineStyles,
...componentStyleUrls.map(
styleUrl => resolveStyleUrl(
styleUrl.url, styleUrl.nodeForError,
ResourceTypeForDiagnostics.StylesheetFromDecorator))
...componentStyleUrls.map(styleUrl => resolveStyleUrl(styleUrl.url))
])
.then(() => undefined);
}
@@ -326,11 +324,13 @@ export class ComponentDecoratorHandler implements
const containingFile = node.getSourceFile().fileName;
this.literalCache.delete(decorator);
let diagnostics: ts.Diagnostic[]|undefined;
let isPoisoned = false;
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
flags, this.annotateForClosureCompiler,
node, decorator, this.reflector, this.evaluator, this.isCore, flags,
this.annotateForClosureCompiler,
this.elementSchemaRegistry.getDefaultComponentElementName());
if (directiveResult === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
@@ -341,6 +341,16 @@ export class ComponentDecoratorHandler implements
// Next, read the `@Component`-specific fields.
const {decorator: component, metadata, inputs, outputs} = directiveResult;
const encapsulation: number =
this._resolveEnumValue(component, 'encapsulation', 'ViewEncapsulation') ??
ViewEncapsulation.Emulated;
const changeDetection: number|null =
this._resolveEnumValue(component, 'changeDetection', 'ChangeDetectionStrategy');
let animations: Expression|null = null;
if (component.has('animations')) {
animations = new WrappedNodeExpr(component.get('animations')!);
}
// Go through the root directories for this project, and select the one with the smallest
// relative path representation.
@@ -392,7 +402,7 @@ export class ComponentDecoratorHandler implements
template = this.extractTemplate(node, templateDecl);
}
const templateResource =
template.isInline ? {path: null, expression: component.get('template')!} : {
template.declaration.isInline ? {path: null, expression: component.get('template')!} : {
path: absoluteFrom(template.declaration.resolvedTemplateUrl),
expression: template.sourceMapping.node
};
@@ -408,16 +418,36 @@ export class ComponentDecoratorHandler implements
];
for (const styleUrl of styleUrls) {
const resourceType = styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator ?
ResourceTypeForDiagnostics.StylesheetFromDecorator :
ResourceTypeForDiagnostics.StylesheetFromTemplate;
const resourceUrl = this._resolveResourceOrThrow(
styleUrl.url, containingFile, styleUrl.nodeForError, resourceType);
const resourceStr = this.resourceLoader.load(resourceUrl);
try {
const resourceUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
const resourceStr = this.resourceLoader.load(resourceUrl);
styles.push(resourceStr);
if (this.depTracker !== null) {
this.depTracker.addResourceDependency(node.getSourceFile(), absoluteFrom(resourceUrl));
}
} catch {
if (diagnostics === undefined) {
diagnostics = [];
}
const resourceType =
styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator ?
ResourceTypeForDiagnostics.StylesheetFromDecorator :
ResourceTypeForDiagnostics.StylesheetFromTemplate;
diagnostics.push(
this.makeResourceNotFoundError(styleUrl.url, styleUrl.nodeForError, resourceType)
.toDiagnostic());
}
}
styles.push(resourceStr);
if (this.depTracker !== null) {
this.depTracker.addResourceDependency(node.getSourceFile(), absoluteFrom(resourceUrl));
if (encapsulation === ViewEncapsulation.ShadowDom && metadata.selector !== null) {
const selectorError = checkCustomElementSelectorForErrors(metadata.selector);
if (selectorError !== null) {
if (diagnostics === undefined) {
diagnostics = [];
}
diagnostics.push(makeDiagnostic(
ErrorCode.COMPONENT_INVALID_SHADOW_DOM_SELECTOR, component.get('selector')!,
selectorError));
}
}
@@ -450,17 +480,6 @@ export class ComponentDecoratorHandler implements
styles.push(...template.styles);
}
const encapsulation: number =
this._resolveEnumValue(component, 'encapsulation', 'ViewEncapsulation') || 0;
const changeDetection: number|null =
this._resolveEnumValue(component, 'changeDetection', 'ChangeDetectionStrategy');
let animations: Expression|null = null;
if (component.has('animations')) {
animations = new WrappedNodeExpr(component.get('animations')!);
}
const output: AnalysisOutput<ComponentAnalysisData> = {
analysis: {
baseClass: readBaseClass(node, this.reflector, this.evaluator),
@@ -484,9 +503,8 @@ export class ComponentDecoratorHandler implements
relativeContextFilePath,
},
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
classMetadata: extractClassMetadata(
node, this.reflector, this.isCore, this.annotateForClosureCompiler),
template,
providersRequiringFactory,
viewProvidersRequiringFactory,
@@ -496,8 +514,9 @@ export class ComponentDecoratorHandler implements
styles: styleResources,
template: templateResource,
},
isPoisoned: false,
isPoisoned,
},
diagnostics,
};
if (changeDetection !== null) {
output.analysis!.meta.changeDetection = changeDetection;
@@ -518,6 +537,7 @@ export class ComponentDecoratorHandler implements
// the information about the component is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
type: MetaType.Directive,
ref,
name: node.name.text,
selector: analysis.meta.selector,
@@ -565,7 +585,7 @@ export class ComponentDecoratorHandler implements
selector,
boundTemplate,
templateMeta: {
isInline: analysis.template.isInline,
isInline: analysis.template.declaration.isInline,
file: analysis.template.file,
},
});
@@ -819,6 +839,13 @@ export class ComponentDecoratorHandler implements
return {data};
}
xi18n(ctx: Xi18nContext, node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
void {
ctx.updateFromTemplate(
analysis.template.content, analysis.template.declaration.resolvedTemplateUrl,
analysis.template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG);
}
updateResources(node: ClassDeclaration, analysis: ComponentAnalysisData): void {
const containingFile = node.getSourceFile().fileName;
@@ -833,14 +860,14 @@ export class ComponentDecoratorHandler implements
let styles: string[] = [];
if (analysis.styleUrls !== null) {
for (const styleUrl of analysis.styleUrls) {
const resourceType =
styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator ?
ResourceTypeForDiagnostics.StylesheetFromDecorator :
ResourceTypeForDiagnostics.StylesheetFromTemplate;
const resolvedStyleUrl = this._resolveResourceOrThrow(
styleUrl.url, containingFile, styleUrl.nodeForError, resourceType);
const styleText = this.resourceLoader.load(resolvedStyleUrl);
styles.push(styleText);
try {
const resolvedStyleUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
const styleText = this.resourceLoader.load(resolvedStyleUrl);
styles.push(styleText);
} catch (e) {
// Resource resolve failures should already be in the diagnostics list from the analyze
// stage. We do not need to do anything with them when updating resources.
}
}
}
if (analysis.inlineStyles !== null) {
@@ -864,7 +891,10 @@ export class ComponentDecoratorHandler implements
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
return compileResults(fac, def, analysis.metadataStmt, 'ɵcmp');
const classMetadata = analysis.classMetadata !== null ?
compileClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵcmp');
}
compilePartial(
@@ -873,10 +903,21 @@ export class ComponentDecoratorHandler implements
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const templateInfo: DeclareComponentTemplateInfo = {
content: analysis.template.content,
sourceUrl: analysis.template.declaration.resolvedTemplateUrl,
isInline: analysis.template.declaration.isInline,
inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ?
new WrappedNodeExpr(analysis.template.sourceMapping.node) :
null,
};
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileDeclareComponentFromMetadata(meta, analysis.template);
return compileResults(fac, def, analysis.metadataStmt, 'ɵcmp');
const def = compileDeclareComponentFromMetadata(meta, analysis.template, templateInfo);
const classMetadata = analysis.classMetadata !== null ?
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵcmp');
}
private _resolveLiteral(decorator: Decorator): ts.ObjectLiteralExpression {
@@ -978,10 +1019,14 @@ export class ComponentDecoratorHandler implements
const styleUrlsExpr = component.get('styleUrls');
if (styleUrlsExpr !== undefined && ts.isArrayLiteralExpression(styleUrlsExpr)) {
for (const expression of stringLiteralElements(styleUrlsExpr)) {
const resourceUrl = this._resolveResourceOrThrow(
expression.text, containingFile, expression,
ResourceTypeForDiagnostics.StylesheetFromDecorator);
styles.add({path: absoluteFrom(resourceUrl), expression});
try {
const resourceUrl = this.resourceLoader.resolve(expression.text, containingFile);
styles.add({path: absoluteFrom(resourceUrl), expression});
} catch {
// Errors in style resource extraction do not need to be handled here. We will produce
// diagnostics for each one that fails in the analysis, after we evaluate the `styleUrls`
// expression to determine _all_ style resources, not just the string literals.
}
}
}
@@ -1006,22 +1051,27 @@ export class ComponentDecoratorHandler implements
throw createValueHasWrongTypeError(
templateUrlExpr, templateUrl, 'templateUrl must be a string');
}
const resourceUrl = this._resolveResourceOrThrow(
templateUrl, containingFile, templateUrlExpr, ResourceTypeForDiagnostics.Template);
const templatePromise =
this.resourceLoader.preload(resourceUrl, {type: 'template', containingFile});
try {
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
const templatePromise =
this.resourceLoader.preload(resourceUrl, {type: 'template', containingFile});
// If the preload worked, then actually load and parse the template, and wait for any style
// URLs to resolve.
if (templatePromise !== undefined) {
return templatePromise.then(() => {
const templateDecl = this.parseTemplateDeclaration(decorator, component, containingFile);
const template = this.extractTemplate(node, templateDecl);
this.preanalyzeTemplateCache.set(node, template);
return template;
});
} else {
return Promise.resolve(null);
// If the preload worked, then actually load and parse the template, and wait for any style
// URLs to resolve.
if (templatePromise !== undefined) {
return templatePromise.then(() => {
const templateDecl =
this.parseTemplateDeclaration(decorator, component, containingFile);
const template = this.extractTemplate(node, templateDecl);
this.preanalyzeTemplateCache.set(node, template);
return template;
});
} else {
return Promise.resolve(null);
}
} catch (e) {
throw this.makeResourceNotFoundError(
templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template);
}
} else {
const templateDecl = this.parseTemplateDeclaration(decorator, component, containingFile);
@@ -1034,48 +1084,57 @@ export class ComponentDecoratorHandler implements
private extractTemplate(node: ClassDeclaration, template: TemplateDeclaration):
ParsedTemplateWithSource {
if (template.isInline) {
let templateStr: string;
let templateLiteral: ts.Node|null = null;
let templateUrl: string = '';
let templateRange: LexerRange|null = null;
let sourceStr: string;
let sourceParseRange: LexerRange|null = null;
let templateContent: string;
let sourceMapping: TemplateSourceMapping;
let escapedString = false;
let sourceMapUrl: string|null;
// We only support SourceMaps for inline templates that are simple string literals.
if (ts.isStringLiteral(template.expression) ||
ts.isNoSubstitutionTemplateLiteral(template.expression)) {
// the start and end of the `templateExpr` node includes the quotation marks, which we must
// strip
templateRange = getTemplateRange(template.expression);
templateStr = template.expression.getSourceFile().text;
templateLiteral = template.expression;
templateUrl = template.templateUrl;
sourceParseRange = getTemplateRange(template.expression);
sourceStr = template.expression.getSourceFile().text;
templateContent = template.expression.text;
escapedString = true;
sourceMapping = {
type: 'direct',
node: template.expression,
};
sourceMapUrl = template.resolvedTemplateUrl;
} else {
const resolvedTemplate = this.evaluator.evaluate(template.expression);
if (typeof resolvedTemplate !== 'string') {
throw createValueHasWrongTypeError(
template.expression, resolvedTemplate, 'template must be a string');
}
templateStr = resolvedTemplate;
// We do not parse the template directly from the source file using a lexer range, so
// the template source and content are set to the statically resolved template.
sourceStr = resolvedTemplate;
templateContent = resolvedTemplate;
sourceMapping = {
type: 'indirect',
node: template.expression,
componentClass: node,
template: templateStr,
template: templateContent,
};
// Indirect templates cannot be mapped to a particular byte range of any input file, since
// they're computed by expressions that may span many files. Don't attempt to map them back
// to a given file.
sourceMapUrl = null;
}
return {
...this._parseTemplate(template, templateStr, templateRange, escapedString),
...this._parseTemplate(template, sourceStr, sourceParseRange, escapedString, sourceMapUrl),
content: templateContent,
sourceMapping,
declaration: template,
};
} else {
const templateStr = this.resourceLoader.load(template.resolvedTemplateUrl);
const templateContent = this.resourceLoader.load(template.resolvedTemplateUrl);
if (this.depTracker !== null) {
this.depTracker.addResourceDependency(
node.getSourceFile(), absoluteFrom(template.resolvedTemplateUrl));
@@ -1083,15 +1142,17 @@ export class ComponentDecoratorHandler implements
return {
...this._parseTemplate(
template, templateStr, /* templateRange */ null,
/* escapedString */ false),
template, /* sourceStr */ templateContent, /* sourceParseRange */ null,
/* escapedString */ false,
/* sourceMapUrl */ template.resolvedTemplateUrl),
content: templateContent,
sourceMapping: {
type: 'external',
componentClass: node,
// TODO(alxhub): TS in g3 is unable to make this inference on its own, so cast it here
// until g3 is able to figure this out.
node: (template as ExternalTemplateDeclaration).templateUrlExpression,
template: templateStr,
template: templateContent,
templateUrl: template.resolvedTemplateUrl,
},
declaration: template,
@@ -1100,19 +1161,18 @@ export class ComponentDecoratorHandler implements
}
private _parseTemplate(
template: TemplateDeclaration, templateStr: string, templateRange: LexerRange|null,
escapedString: boolean): ParsedComponentTemplate {
template: TemplateDeclaration, sourceStr: string, sourceParseRange: LexerRange|null,
escapedString: boolean, sourceMapUrl: string|null): ParsedComponentTemplate {
// We always normalize line endings if the template has been escaped (i.e. is inline).
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
const parsedTemplate = parseTemplate(templateStr, template.sourceMapUrl, {
const parsedTemplate = parseTemplate(sourceStr, sourceMapUrl ?? '', {
preserveWhitespaces: template.preserveWhitespaces,
interpolationConfig: template.interpolationConfig,
range: templateRange ?? undefined,
range: sourceParseRange ?? undefined,
escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs,
isInline: template.isInline,
alwaysAttemptHtmlToR3AstConversion: this.usePoisonedData,
});
@@ -1131,26 +1191,22 @@ export class ComponentDecoratorHandler implements
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
// with the above options set to preserve source mappings.
const {nodes: diagNodes} = parseTemplate(templateStr, template.sourceMapUrl, {
const {nodes: diagNodes} = parseTemplate(sourceStr, sourceMapUrl ?? '', {
preserveWhitespaces: true,
preserveLineEndings: true,
interpolationConfig: template.interpolationConfig,
range: templateRange ?? undefined,
range: sourceParseRange ?? undefined,
escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs,
leadingTriviaChars: [],
isInline: template.isInline,
alwaysAttemptHtmlToR3AstConversion: this.usePoisonedData,
});
return {
...parsedTemplate,
diagNodes,
template: template.isInline ? new WrappedNodeExpr(template.expression) : templateStr,
templateUrl: template.resolvedTemplateUrl,
isInline: template.isInline,
file: new ParseSourceFile(templateStr, template.resolvedTemplateUrl),
file: new ParseSourceFile(sourceStr, sourceMapUrl ?? ''),
};
}
@@ -1186,18 +1242,20 @@ export class ComponentDecoratorHandler implements
throw createValueHasWrongTypeError(
templateUrlExpr, templateUrl, 'templateUrl must be a string');
}
const resourceUrl = this._resolveResourceOrThrow(
templateUrl, containingFile, templateUrlExpr, ResourceTypeForDiagnostics.Template);
return {
isInline: false,
interpolationConfig,
preserveWhitespaces,
templateUrl,
templateUrlExpression: templateUrlExpr,
resolvedTemplateUrl: resourceUrl,
sourceMapUrl: sourceMapUrl(resourceUrl),
};
try {
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
return {
isInline: false,
interpolationConfig,
preserveWhitespaces,
templateUrl,
templateUrlExpression: templateUrlExpr,
resolvedTemplateUrl: resourceUrl,
};
} catch (e) {
throw this.makeResourceNotFoundError(
templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template);
}
} else if (component.has('template')) {
return {
isInline: true,
@@ -1206,7 +1264,6 @@ export class ComponentDecoratorHandler implements
expression: component.get('template')!,
templateUrl: containingFile,
resolvedTemplateUrl: containingFile,
sourceMapUrl: containingFile,
};
} else {
throw new FatalDiagnosticError(
@@ -1260,33 +1317,24 @@ export class ComponentDecoratorHandler implements
this.cycleAnalyzer.recordSyntheticImport(origin, imported);
}
/**
* Resolve the url of a resource relative to the file that contains the reference to it.
*
* Throws a FatalDiagnosticError when unable to resolve the file.
*/
private _resolveResourceOrThrow(
file: string, basePath: string, nodeForError: ts.Node,
resourceType: ResourceTypeForDiagnostics): string {
try {
return this.resourceLoader.resolve(file, basePath);
} catch (e) {
let errorText: string;
switch (resourceType) {
case ResourceTypeForDiagnostics.Template:
errorText = `Could not find template file '${file}'.`;
break;
case ResourceTypeForDiagnostics.StylesheetFromTemplate:
errorText = `Could not find stylesheet file '${file}' linked from the template.`;
break;
case ResourceTypeForDiagnostics.StylesheetFromDecorator:
errorText = `Could not find stylesheet file '${file}'.`;
break;
}
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_RESOURCE_NOT_FOUND, nodeForError, errorText);
private makeResourceNotFoundError(
file: string, nodeForError: ts.Node,
resourceType: ResourceTypeForDiagnostics): FatalDiagnosticError {
let errorText: string;
switch (resourceType) {
case ResourceTypeForDiagnostics.Template:
errorText = `Could not find template file '${file}'.`;
break;
case ResourceTypeForDiagnostics.StylesheetFromTemplate:
errorText = `Could not find stylesheet file '${file}' linked from the template.`;
break;
case ResourceTypeForDiagnostics.StylesheetFromDecorator:
errorText = `Could not find stylesheet file '${file}'.`;
break;
}
return new FatalDiagnosticError(
ErrorCode.COMPONENT_RESOURCE_NOT_FOUND, nodeForError, errorText);
}
private _extractTemplateStyleUrls(template: ParsedTemplateWithSource): StyleUrlMeta[] {
@@ -1312,17 +1360,6 @@ function getTemplateRange(templateExpr: ts.Expression) {
};
}
function sourceMapUrl(resourceUrl: string): string {
if (!tsSourceMapBug29300Fixed()) {
// By removing the template URL we are telling the translator not to try to
// map the external source file to the generated code, since the version
// of TS that is running does not support it.
return '';
} else {
return resourceUrl;
}
}
/** Determines if the result of an evaluation is a string array. */
function isStringArray(resolvedValue: ResolvedValue): resolvedValue is string[] {
return Array.isArray(resolvedValue) && resolvedValue.every(elem => typeof elem === 'string');
@@ -1348,12 +1385,6 @@ function getTemplateDeclarationNodeForError(declaration: TemplateDeclaration): t
* some of which might be useful for re-parsing the template with different options.
*/
export interface ParsedComponentTemplate extends ParsedTemplate {
/**
* True if the original template was stored inline;
* False if the template was in an external file.
*/
isInline: boolean;
/**
* The template AST, parsed in a manner which preserves source map information for diagnostics.
*
@@ -1368,6 +1399,8 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
}
export interface ParsedTemplateWithSource extends ParsedComponentTemplate {
/** The string contents of the template. */
content: string;
sourceMapping: TemplateSourceMapping;
declaration: TemplateDeclaration;
}
@@ -1380,7 +1413,6 @@ interface CommonTemplateDeclaration {
interpolationConfig: InterpolationConfig;
templateUrl: string;
resolvedTemplateUrl: string;
sourceMapUrl: string;
}
/**
@@ -1402,7 +1434,7 @@ interface ExternalTemplateDeclaration extends CommonTemplateDeclaration {
/**
* The declaration of a template extracted from a component decorator.
*
* This data is extracted and stored separately to faciliate re-interpreting the template
* This data is extracted and stored separately to facilitate re-interpreting the template
* declaration whenever the compiler is notified of a change to a template file. With this
* information, `ComponentDecoratorHandler` is able to re-read the template and update the component
* record without needing to parse the original decorator again.
@@ -1420,3 +1452,31 @@ function makeCyclicImportInfo(
`The ${type} '${name}' is used in the template but importing it would create a cycle: `;
return makeRelatedInformation(ref.node, message + path);
}
/**
* Checks whether a selector is a valid custom element tag name.
* Based loosely on https://github.com/sindresorhus/validate-element-name.
*/
function checkCustomElementSelectorForErrors(selector: string): string|null {
// Avoid flagging components with an attribute or class selector. This isn't bulletproof since it
// won't catch cases like `foo[]bar`, but we don't need it to be. This is mainly to avoid flagging
// something like `foo-bar[baz]` incorrectly.
if (selector.includes('.') || (selector.includes('[') && selector.includes(']'))) {
return null;
}
if (!(/^[a-z]/.test(selector))) {
return 'Selector of a ShadowDom-encapsulated component must start with a lower case letter.';
}
if (/[A-Z]/.test(selector)) {
return 'Selector of a ShadowDom-encapsulated component must all be in lower case.';
}
if (!selector.includes('-')) {
return 'Selector of a component that uses ViewEncapsulation.ShadowDom must contain a hyphen.';
}
return null;
}
@@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, ExternalExpr, FactoryTarget, getSafePropertyAccessString, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DirectiveMetadata, R3FactoryMetadata, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, ExternalExpr, FactoryTarget, getSafePropertyAccessString, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3ClassMetadata, R3DirectiveMetadata, R3FactoryMetadata, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import {emitDistinctChangesOnlyDefaultValue} from '@angular/compiler/src/core';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports';
import {Reference} from '../../imports';
import {areTypeParametersEqual, extractSemanticTypeParameters, isArrayEqual, isSetEqual, isSymbolEqual, SemanticDepGraphUpdater, SemanticSymbol, SemanticTypeParameter} from '../../incremental/semantic_graph';
import {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, TemplateGuardMeta} from '../../metadata';
import {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaType, TemplateGuardMeta} from '../../metadata';
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../perf';
@@ -23,8 +23,8 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFl
import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics, getUndecoratedClassWithAngularFeaturesDiagnostic} from './diagnostics';
import {compileDeclareFactory, compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {compileResults, createSourceSpan, findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, resolveProvidersRequiringFactory, toFactoryMetadata, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util';
import {extractClassMetadata} from './metadata';
import {compileResults, createSourceSpan, findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, resolveProvidersRequiringFactory, toFactoryMetadata, tryUnwrapForwardRef, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
const FIELD_DECORATORS = [
@@ -40,7 +40,7 @@ export interface DirectiveHandlerData {
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
typeCheckMeta: DirectiveTypeCheckMeta;
meta: R3DirectiveMetadata;
metadataStmt: Statement|null;
classMetadata: R3ClassMetadata|null;
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
inputs: ClassPropertyMapping;
outputs: ClassPropertyMapping;
@@ -64,7 +64,7 @@ export class DirectiveSymbol extends SemanticSymbol {
super(decl);
}
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
override isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
// Note: since components and directives have exactly the same items contributing to their
// public API, it is okay for a directive to change into a component and vice versa without
// the API being affected.
@@ -83,7 +83,7 @@ export class DirectiveSymbol extends SemanticSymbol {
!isArrayEqual(this.exportAs, previousSymbol.exportAs);
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
override isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
// If the public API of the directive has changed, then so has its type-check API.
if (this.isPublicApiAffected(previousSymbol)) {
return true;
@@ -177,9 +177,8 @@ export class DirectiveDecoratorHandler implements
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private metaReader: MetadataReader, private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private metaReader: MetadataReader, private injectableRegistry: InjectableClassRegistry,
private isCore: boolean, private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private annotateForClosureCompiler: boolean,
private compileUndecoratedClassesWithAngularFeatures: boolean, private perf: PerfRecorder) {}
@@ -215,8 +214,8 @@ export class DirectiveDecoratorHandler implements
this.perf.eventCount(PerfEvent.AnalyzeDirective);
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
flags, this.annotateForClosureCompiler);
node, decorator, this.reflector, this.evaluator, this.isCore, flags,
this.annotateForClosureCompiler);
if (directiveResult === undefined) {
return {};
}
@@ -233,9 +232,8 @@ export class DirectiveDecoratorHandler implements
inputs: directiveResult.inputs,
outputs: directiveResult.outputs,
meta: analysis,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
classMetadata: extractClassMetadata(
node, this.reflector, this.isCore, this.annotateForClosureCompiler),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
providersRequiringFactory,
@@ -258,6 +256,7 @@ export class DirectiveDecoratorHandler implements
// the information about the directive is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
type: MetaType.Directive,
ref,
name: node.name.text,
selector: analysis.meta.selector,
@@ -304,7 +303,10 @@ export class DirectiveDecoratorHandler implements
resolution: Readonly<unknown>, pool: ConstantPool): CompileResult[] {
const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Directive));
const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser());
return compileResults(fac, def, analysis.metadataStmt, 'ɵdir');
const classMetadata = analysis.classMetadata !== null ?
compileClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵdir');
}
compilePartial(
@@ -312,7 +314,10 @@ export class DirectiveDecoratorHandler implements
resolution: Readonly<unknown>): CompileResult[] {
const fac = compileDeclareFactory(toFactoryMetadata(analysis.meta, FactoryTarget.Directive));
const def = compileDeclareDirectiveFromMetadata(analysis.meta);
return compileResults(fac, def, analysis.metadataStmt, 'ɵdir');
const classMetadata = analysis.classMetadata !== null ?
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵdir');
}
/**
@@ -345,9 +350,8 @@ export class DirectiveDecoratorHandler implements
*/
export function extractDirectiveMetadata(
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
flags: HandlerFlags, annotateForClosureCompiler: boolean,
defaultSelector: string|null = null): {
evaluator: PartialEvaluator, isCore: boolean, flags: HandlerFlags,
annotateForClosureCompiler: boolean, defaultSelector: string|null = null): {
decorator: Map<string, ts.Expression>,
metadata: R3DirectiveMetadata,
inputs: ClassPropertyMapping,
@@ -467,7 +471,7 @@ export function extractDirectiveMetadata(
exportAs = resolved.split(',').map(part => part.trim());
}
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore);
// Non-abstract directives (those with a selector) require valid constructor dependencies, whereas
// abstract directives are allowed to have invalid dependencies, given that a subclass may call
@@ -528,7 +532,7 @@ export function extractQueryMetadata(
ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`);
}
const first = name === 'ViewChild' || name === 'ContentChild';
const node = unwrapForwardRef(args[0], reflector);
const node = tryUnwrapForwardRef(args[0], reflector) ?? args[0];
const arg = evaluator.evaluate(node);
/** Whether or not this query should collect only static results (see view/api.ts) */
@@ -10,6 +10,8 @@ import {compileDeclareFactoryFunction, compileFactoryFunction, R3FactoryMetadata
import {CompileResult} from '../../transform';
export type CompileFactoryFn = (metadata: R3FactoryMetadata) => CompileResult;
export function compileNgFactoryDefField(metadata: R3FactoryMetadata): CompileResult {
const res = compileFactoryFunction(metadata);
return {name: 'ɵfac', initializer: res.expression, statements: res.statements, type: res.type};
@@ -6,35 +6,33 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileInjectable as compileIvyInjectable, Expression, FactoryTarget, LiteralExpr, R3DependencyMetadata, R3FactoryMetadata, R3InjectableMetadata, Statement, WrappedNodeExpr} from '@angular/compiler';
import {compileClassMetadata, CompileClassMetadataFn, compileDeclareClassMetadata, compileDeclareInjectableFromMetadata, compileInjectable, createR3ProviderExpression, Expression, FactoryTarget, LiteralExpr, R3ClassMetadata, R3CompiledExpression, R3DependencyMetadata, R3InjectableMetadata, R3ProviderExpression, Statement, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder} from '../../imports';
import {InjectableClassRegistry} from '../../metadata';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {compileDeclareFactory, compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, toFactoryMetadata, unwrapConstructorDependencies, unwrapForwardRef, validateConstructorDependencies, wrapTypeReference} from './util';
import {compileDeclareFactory, CompileFactoryFn, compileNgFactoryDefField} from './factory';
import {extractClassMetadata} from './metadata';
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, toFactoryMetadata, tryUnwrapForwardRef, unwrapConstructorDependencies, validateConstructorDependencies, wrapTypeReference} from './util';
export interface InjectableHandlerData {
meta: R3InjectableMetadata;
metadataStmt: Statement|null;
classMetadata: R3ClassMetadata|null;
ctorDeps: R3DependencyMetadata[]|'invalid'|null;
needsFactory: boolean;
}
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
* Adapts the `compileInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableDecoratorHandler implements
DecoratorHandler<Decorator, InjectableHandlerData, null, unknown> {
constructor(
private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private strictCtorDeps: boolean,
private reflector: ReflectionHost, private isCore: boolean, private strictCtorDeps: boolean,
private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder,
/**
* What to do if the injectable already contains a ɵprov property.
@@ -74,10 +72,8 @@ export class InjectableDecoratorHandler implements
analysis: {
meta,
ctorDeps: extractInjectableCtorDeps(
node, meta, decorator, this.reflector, this.defaultImportRecorder, this.isCore,
this.strictCtorDeps),
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
node, meta, decorator, this.reflector, this.isCore, this.strictCtorDeps),
classMetadata: extractClassMetadata(node, this.reflector, this.isCore),
// Avoid generating multiple factories if a class has
// more Angular decorators, apart from Injectable.
needsFactory: !decorators ||
@@ -95,48 +91,31 @@ export class InjectableDecoratorHandler implements
}
compileFull(node: ClassDeclaration, analysis: Readonly<InjectableHandlerData>): CompileResult[] {
const res = compileIvyInjectable(analysis.meta);
const statements = res.statements;
const results: CompileResult[] = [];
if (analysis.needsFactory) {
const meta = analysis.meta;
const factoryRes = compileNgFactoryDefField(
toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable));
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}
results.push(factoryRes);
}
const ɵprov = this.reflector.getMembersOfClass(node).find(member => member.name === 'ɵprov');
if (ɵprov !== undefined && this.errorOnDuplicateProv) {
throw new FatalDiagnosticError(
ErrorCode.INJECTABLE_DUPLICATE_PROV, ɵprov.nameNode || ɵprov.node || node,
'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.');
}
if (ɵprov === undefined) {
// Only add a new ɵprov if there is not one already
results.push({name: 'ɵprov', initializer: res.expression, statements, type: res.type});
}
return results;
return this.compile(
compileNgFactoryDefField, meta => compileInjectable(meta, false), compileClassMetadata,
node, analysis);
}
compilePartial(node: ClassDeclaration, analysis: Readonly<InjectableHandlerData>):
CompileResult[] {
const res = compileIvyInjectable(analysis.meta);
const statements = res.statements;
return this.compile(
compileDeclareFactory, compileDeclareInjectableFromMetadata, compileDeclareClassMetadata,
node, analysis);
}
private compile(
compileFactoryFn: CompileFactoryFn,
compileInjectableFn: (meta: R3InjectableMetadata) => R3CompiledExpression,
compileClassMetadataFn: CompileClassMetadataFn, node: ClassDeclaration,
analysis: Readonly<InjectableHandlerData>): CompileResult[] {
const results: CompileResult[] = [];
if (analysis.needsFactory) {
const meta = analysis.meta;
const factoryRes = compileDeclareFactory(
const factoryRes = compileFactoryFn(
toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable));
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
if (analysis.classMetadata !== null) {
factoryRes.statements.push(compileClassMetadataFn(analysis.classMetadata).toStmt());
}
results.push(factoryRes);
}
@@ -150,7 +129,9 @@ export class InjectableDecoratorHandler implements
if (ɵprov === undefined) {
// Only add a new ɵprov if there is not one already
results.push({name: 'ɵprov', initializer: res.expression, statements, type: res.type});
const res = compileInjectableFn(analysis.meta);
results.push(
{name: 'ɵprov', initializer: res.expression, statements: res.statements, type: res.type});
}
return results;
@@ -159,7 +140,7 @@ export class InjectableDecoratorHandler implements
/**
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the
* input metadata needed to run `compileIvyInjectable`.
* input metadata needed to run `compileInjectable`.
*
* A `null` return value indicates this is @Injectable has invalid data.
*/
@@ -181,7 +162,7 @@ function extractInjectableMetadata(
type,
typeArgumentCount,
internalType,
providedIn: new LiteralExpr(null),
providedIn: createR3ProviderExpression(new LiteralExpr(null), false),
};
} else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
@@ -196,12 +177,12 @@ function extractInjectableMetadata(
// Resolve the fields of the literal into a map of field name to expression.
const meta = reflectObjectLiteral(metaNode);
let providedIn: Expression = new LiteralExpr(null);
if (meta.has('providedIn')) {
providedIn = new WrappedNodeExpr(meta.get('providedIn')!);
}
let userDeps: R3DependencyMetadata[]|undefined = undefined;
const providedIn = meta.has('providedIn') ?
getProviderExpression(meta.get('providedIn')!, reflector) :
createR3ProviderExpression(new LiteralExpr(null), false);
let deps: R3DependencyMetadata[]|undefined = undefined;
if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) {
const depsExpr = meta.get('deps')!;
if (!ts.isArrayLiteralExpression(depsExpr)) {
@@ -209,62 +190,45 @@ function extractInjectableMetadata(
ErrorCode.VALUE_NOT_LITERAL, depsExpr,
`@Injectable deps metadata must be an inline array`);
}
userDeps = depsExpr.elements.map(dep => getDep(dep, reflector));
deps = depsExpr.elements.map(dep => getDep(dep, reflector));
}
const result: R3InjectableMetadata = {name, type, typeArgumentCount, internalType, providedIn};
if (meta.has('useValue')) {
return {
name,
type,
typeArgumentCount,
internalType,
providedIn,
useValue: new WrappedNodeExpr(unwrapForwardRef(meta.get('useValue')!, reflector)),
};
result.useValue = getProviderExpression(meta.get('useValue')!, reflector);
} else if (meta.has('useExisting')) {
return {
name,
type,
typeArgumentCount,
internalType,
providedIn,
useExisting: new WrappedNodeExpr(unwrapForwardRef(meta.get('useExisting')!, reflector)),
};
result.useExisting = getProviderExpression(meta.get('useExisting')!, reflector);
} else if (meta.has('useClass')) {
return {
name,
type,
typeArgumentCount,
internalType,
providedIn,
useClass: new WrappedNodeExpr(unwrapForwardRef(meta.get('useClass')!, reflector)),
userDeps,
};
result.useClass = getProviderExpression(meta.get('useClass')!, reflector);
result.deps = deps;
} else if (meta.has('useFactory')) {
// useFactory is special - the 'deps' property must be analyzed.
const factory = new WrappedNodeExpr(meta.get('useFactory')!);
return {
name,
type,
typeArgumentCount,
internalType,
providedIn,
useFactory: factory,
userDeps,
};
} else {
return {name, type, typeArgumentCount, internalType, providedIn};
result.useFactory = new WrappedNodeExpr(meta.get('useFactory')!);
result.deps = deps;
}
return result;
} else {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], 'Too many arguments to @Injectable');
}
}
/**
* Get the `R3ProviderExpression` for this `expression`.
*
* The `useValue`, `useExisting` and `useClass` properties might be wrapped in a `ForwardRef`, which
* needs to be unwrapped. This function will do that unwrapping and set a flag on the returned
* object to indicate whether the value needed unwrapping.
*/
function getProviderExpression(
expression: ts.Expression, reflector: ReflectionHost): R3ProviderExpression {
const forwardRefValue = tryUnwrapForwardRef(expression, reflector);
return createR3ProviderExpression(
new WrappedNodeExpr(forwardRefValue ?? expression), forwardRefValue !== null);
}
function extractInjectableCtorDeps(
clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator,
reflector: ReflectionHost, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
strictCtorDeps: boolean) {
reflector: ReflectionHost, isCore: boolean, strictCtorDeps: boolean) {
if (decorator.args === null) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_NOT_CALLED, Decorator.nodeForError(decorator),
@@ -283,15 +247,15 @@ function extractInjectableCtorDeps(
// constructor signature does not work for DI then a factory definition (ɵfac) that throws is
// generated.
if (strictCtorDeps) {
ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
ctorDeps = getValidConstructorDependencies(clazz, reflector, isCore);
} else {
ctorDeps = unwrapConstructorDependencies(
getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore));
ctorDeps =
unwrapConstructorDependencies(getConstructorDependencies(clazz, reflector, isCore));
}
return ctorDeps;
} else if (decorator.args.length === 1) {
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore);
if (strictCtorDeps && meta.useValue === undefined && meta.useExisting === undefined &&
meta.useClass === undefined && meta.useFactory === undefined) {
@@ -6,15 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {devOnlyGuardedExpression, Expression, ExternalExpr, FunctionExpr, Identifiers, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, literalMap, NONE_TYPE, ReturnStatement, Statement, WrappedNodeExpr} from '@angular/compiler';
import {Expression, FunctionExpr, LiteralArrayExpr, LiteralExpr, literalMap, R3ClassMetadata, ReturnStatement, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {DefaultImportRecorder} from '../../imports';
import {CtorParameter, DeclarationNode, Decorator, ReflectionHost, TypeValueReferenceKind} from '../../reflection';
import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';
/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields. An ngDevMode guard is used to allow the call to be
@@ -23,10 +21,9 @@ import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './uti
* If no such metadata is present, this function returns `null`. Otherwise, the call is returned
* as a `Statement` for inclusion along with the class.
*/
export function generateSetClassMetadataCall(
clazz: DeclarationNode, reflection: ReflectionHost,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
annotateForClosureCompiler?: boolean): Statement|null {
export function extractClassMetadata(
clazz: DeclarationNode, reflection: ReflectionHost, isCore: boolean,
annotateForClosureCompiler?: boolean): R3ClassMetadata|null {
if (!reflection.isClass(clazz)) {
return null;
}
@@ -50,21 +47,20 @@ export function generateSetClassMetadataCall(
if (ngClassDecorators.length === 0) {
return null;
}
const metaDecorators = ts.createArrayLiteral(ngClassDecorators);
const metaDecorators = new WrappedNodeExpr(ts.createArrayLiteral(ngClassDecorators));
// Convert the constructor parameters to metadata, passing null if none are present.
let metaCtorParameters: Expression = new LiteralExpr(null);
let metaCtorParameters: Expression|null = null;
const classCtorParameters = reflection.getConstructorParameters(clazz);
if (classCtorParameters !== null) {
const ctorParameters = classCtorParameters.map(
param => ctorParameterToMetadata(param, defaultImportRecorder, isCore));
const ctorParameters = classCtorParameters.map(param => ctorParameterToMetadata(param, isCore));
metaCtorParameters = new FunctionExpr([], [
new ReturnStatement(new LiteralArrayExpr(ctorParameters)),
]);
}
// Do the same for property decorators.
let metaPropDecorators: ts.Expression = ts.createNull();
let metaPropDecorators: Expression|null = null;
const classMembers = reflection.getMembersOfClass(clazz).filter(
member => !member.isStatic && member.decorators !== null && member.decorators.length > 0);
const duplicateDecoratedMemberNames =
@@ -80,34 +76,25 @@ export function generateSetClassMetadataCall(
const decoratedMembers = classMembers.map(
member => classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore));
if (decoratedMembers.length > 0) {
metaPropDecorators = ts.createObjectLiteral(decoratedMembers);
metaPropDecorators = new WrappedNodeExpr(ts.createObjectLiteral(decoratedMembers));
}
// Generate a pure call to setClassMetadata with the class identifier and its metadata.
const setClassMetadata = new ExternalExpr(Identifiers.setClassMetadata);
const fnCall = new InvokeFunctionExpr(
/* fn */ setClassMetadata,
/* args */
[
new WrappedNodeExpr(id),
new WrappedNodeExpr(metaDecorators),
metaCtorParameters,
new WrappedNodeExpr(metaPropDecorators),
]);
const iife = new FunctionExpr([], [devOnlyGuardedExpression(fnCall).toStmt()]);
return iife.callFn([]).toStmt();
return {
type: new WrappedNodeExpr(id),
decorators: metaDecorators,
ctorParameters: metaCtorParameters,
propDecorators: metaPropDecorators,
};
}
/**
* Convert a reflected constructor parameter to metadata.
*/
function ctorParameterToMetadata(
param: CtorParameter, defaultImportRecorder: DefaultImportRecorder,
isCore: boolean): Expression {
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Expression {
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
// its type is undefined.
const type = param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE ?
valueReferenceToExpression(param.typeValueReference, defaultImportRecorder) :
valueReferenceToExpression(param.typeValueReference) :
new LiteralExpr(undefined);
const mapEntries: {key: string, value: Expression, quoted: false}[] = [
@@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileInjector, compileNgModule, CUSTOM_ELEMENTS_SCHEMA, Expression, ExternalExpr, FactoryTarget, Identifiers as R3, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, NO_ERRORS_SCHEMA, R3CompiledExpression, R3FactoryMetadata, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, SchemaMetadata, Statement, STRING_TYPE, WrappedNodeExpr} from '@angular/compiler';
import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileInjector, compileNgModule, CUSTOM_ELEMENTS_SCHEMA, Expression, ExternalExpr, FactoryTarget, Identifiers as R3, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, NO_ERRORS_SCHEMA, R3ClassMetadata, R3CompiledExpression, R3FactoryMetadata, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, SchemaMetadata, Statement, STRING_TYPE, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports';
import {Reference, ReferenceEmitter} from '../../imports';
import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
@@ -24,7 +24,7 @@ import {getSourceFile} from '../../util/src/typescript';
import {createValueHasWrongTypeError, getProviderDiagnostics} from './diagnostics';
import {compileDeclareFactory, compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {extractClassMetadata} from './metadata';
import {ReferencesRegistry} from './references_registry';
import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, resolveProvidersRequiringFactory, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference} from './util';
@@ -32,7 +32,7 @@ export interface NgModuleAnalysis {
mod: R3NgModuleMetadata;
inj: R3InjectorMetadata;
fac: R3FactoryMetadata;
metadataStmt: Statement|null;
classMetadata: R3ClassMetadata|null;
declarations: Reference<ClassDeclaration>[];
rawDeclarations: ts.Expression|null;
schemas: SchemaMetadata[];
@@ -58,7 +58,7 @@ export class NgModuleSymbol extends SemanticSymbol {
usedPipes: SemanticReference[]
}[] = [];
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
override isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
@@ -67,7 +67,7 @@ export class NgModuleSymbol extends SemanticSymbol {
return false;
}
isEmitAffected(previousSymbol: SemanticSymbol): boolean {
override isEmitAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
@@ -104,7 +104,7 @@ export class NgModuleSymbol extends SemanticSymbol {
return false;
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
override isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
@@ -130,9 +130,7 @@ export class NgModuleDecoratorHandler implements
private scopeRegistry: LocalModuleScopeRegistry,
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
private factoryTracker: FactoryTracker|null,
private defaultImportRecorder: DefaultImportRecorder,
private annotateForClosureCompiler: boolean,
private factoryTracker: FactoryTracker|null, private annotateForClosureCompiler: boolean,
private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder,
private localeId?: string) {}
@@ -350,8 +348,7 @@ export class NgModuleDecoratorHandler implements
type,
internalType,
typeArgumentCount: 0,
deps: getValidConstructorDependencies(
node, this.reflector, this.defaultImportRecorder, this.isCore),
deps: getValidConstructorDependencies(node, this.reflector, this.isCore),
target: FactoryTarget.NgModule,
};
@@ -370,9 +367,8 @@ export class NgModuleDecoratorHandler implements
providersRequiringFactory: rawProviders ?
resolveProvidersRequiringFactory(rawProviders, this.reflector, this.evaluator) :
null,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
classMetadata: extractClassMetadata(
node, this.reflector, this.isCore, this.annotateForClosureCompiler),
factorySymbolName: node.name.text,
},
};
@@ -463,26 +459,28 @@ export class NgModuleDecoratorHandler implements
compileFull(
node: ClassDeclaration,
{inj, mod, fac, metadataStmt, declarations}: Readonly<NgModuleAnalysis>,
{inj, mod, fac, classMetadata, declarations}: Readonly<NgModuleAnalysis>,
{injectorImports}: Readonly<NgModuleResolution>): CompileResult[] {
const factoryFn = compileNgFactoryDefField(fac);
const ngInjectorDef = compileInjector(this.mergeInjectorImports(inj, injectorImports));
const ngModuleDef = compileNgModule(mod);
const statements = ngModuleDef.statements;
this.insertMetadataStatement(statements, metadataStmt);
const metadata = classMetadata !== null ? compileClassMetadata(classMetadata) : null;
this.insertMetadataStatement(statements, metadata);
this.appendRemoteScopingStatements(statements, node, declarations);
return this.compileNgModule(factoryFn, ngInjectorDef, ngModuleDef);
}
compilePartial(
node: ClassDeclaration, {inj, fac, mod, metadataStmt}: Readonly<NgModuleAnalysis>,
node: ClassDeclaration, {inj, fac, mod, classMetadata}: Readonly<NgModuleAnalysis>,
{injectorImports}: Readonly<NgModuleResolution>): CompileResult[] {
const factoryFn = compileDeclareFactory(fac);
const injectorDef =
compileDeclareInjectorFromMetadata(this.mergeInjectorImports(inj, injectorImports));
const ngModuleDef = compileDeclareNgModuleFromMetadata(mod);
this.insertMetadataStatement(ngModuleDef.statements, metadataStmt);
const metadata = classMetadata !== null ? compileDeclareClassMetadata(classMetadata) : null;
this.insertMetadataStatement(ngModuleDef.statements, metadata);
// NOTE: no remote scoping required as this is banned in partial compilation.
return this.compileNgModule(factoryFn, injectorDef, ngModuleDef);
}
@@ -499,10 +497,10 @@ export class NgModuleDecoratorHandler implements
/**
* Add class metadata statements, if provided, to the `ngModuleStatements`.
*/
private insertMetadataStatement(ngModuleStatements: Statement[], metadataStmt: Statement|null):
private insertMetadataStatement(ngModuleStatements: Statement[], metadata: Expression|null):
void {
if (metadataStmt !== null) {
ngModuleStatements.unshift(metadataStmt);
if (metadata !== null) {
ngModuleStatements.unshift(metadata.toStmt());
}
}
@@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileDeclarePipeFromMetadata, compilePipeFromMetadata, FactoryTarget, R3PipeMetadata, Statement, WrappedNodeExpr} from '@angular/compiler';
import {compileClassMetadata, compileDeclareClassMetadata, compileDeclarePipeFromMetadata, compilePipeFromMetadata, FactoryTarget, R3ClassMetadata, R3PipeMetadata, Statement, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports';
import {Reference} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataRegistry} from '../../metadata';
import {InjectableClassRegistry, MetadataRegistry, MetaType} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@@ -21,12 +21,13 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPr
import {createValueHasWrongTypeError} from './diagnostics';
import {compileDeclareFactory, compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {extractClassMetadata} from './metadata';
import {compileResults, findAngularDecorator, getValidConstructorDependencies, makeDuplicateDeclarationError, toFactoryMetadata, unwrapExpression, wrapTypeReference} from './util';
export interface PipeHandlerData {
meta: R3PipeMetadata;
metadataStmt: Statement|null;
classMetadata: R3ClassMetadata|null;
pipeNameExpr: ts.Expression;
}
/**
@@ -37,7 +38,7 @@ export class PipeSymbol extends SemanticSymbol {
super(decl);
}
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
override isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof PipeSymbol)) {
return true;
}
@@ -45,7 +46,7 @@ export class PipeSymbol extends SemanticSymbol {
return this.name !== previousSymbol.name;
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
override isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
return this.isPublicApiAffected(previousSymbol);
}
}
@@ -55,7 +56,6 @@ export class PipeDecoratorHandler implements
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean,
private perf: PerfRecorder) {}
@@ -131,12 +131,11 @@ export class PipeDecoratorHandler implements
internalType,
typeArgumentCount: this.reflector.getGenericArityOfClass(clazz) || 0,
pipeName,
deps: getValidConstructorDependencies(
clazz, this.reflector, this.defaultImportRecorder, this.isCore),
deps: getValidConstructorDependencies(clazz, this.reflector, this.isCore),
pure,
},
metadataStmt: generateSetClassMetadataCall(
clazz, this.reflector, this.defaultImportRecorder, this.isCore),
classMetadata: extractClassMetadata(clazz, this.reflector, this.isCore),
pipeNameExpr,
},
};
}
@@ -147,7 +146,8 @@ export class PipeDecoratorHandler implements
register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName});
this.metaRegistry.registerPipeMetadata(
{type: MetaType.Pipe, ref, name: analysis.meta.pipeName, nameExpr: analysis.pipeNameExpr});
this.injectableRegistry.registerInjectable(node);
}
@@ -167,12 +167,18 @@ export class PipeDecoratorHandler implements
compileFull(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): CompileResult[] {
const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Pipe));
const def = compilePipeFromMetadata(analysis.meta);
return compileResults(fac, def, analysis.metadataStmt, 'ɵpipe');
const classMetadata = analysis.classMetadata !== null ?
compileClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵpipe');
}
compilePartial(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): CompileResult[] {
const fac = compileDeclareFactory(toFactoryMetadata(analysis.meta, FactoryTarget.Pipe));
const def = compileDeclarePipeFromMetadata(analysis.meta);
return compileResults(fac, def, analysis.metadataStmt, 'ɵpipe');
const classMetadata = analysis.classMetadata !== null ?
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
null;
return compileResults(fac, def, classMetadata, 'ɵpipe');
}
}
@@ -12,7 +12,8 @@ import {FactoryTarget} from '@angular/compiler/src/render3/partial/api';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {DefaultImportRecorder, ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {attachDefaultImportDeclaration} from '../../imports/src/default';
import {ForeignFunctionResolver, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, CtorParameter, Decorator, Import, ImportedTypeValueReference, isNamedClassDeclaration, LocalTypeValueReference, ReflectionHost, TypeValueReference, TypeValueReferenceKind, UnavailableValue, ValueUnavailableKind} from '../../reflection';
import {DeclarationData} from '../../scope';
@@ -32,8 +33,7 @@ export interface ConstructorDepError {
}
export function getConstructorDependencies(
clazz: ClassDeclaration, reflector: ReflectionHost,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean): ConstructorDeps|null {
clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): ConstructorDeps|null {
const deps: R3DependencyMetadata[] = [];
const errors: ConstructorDepError[] = [];
let ctorParams = reflector.getConstructorParameters(clazz);
@@ -45,7 +45,7 @@ export function getConstructorDependencies(
}
}
ctorParams.forEach((param, idx) => {
let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder);
let token = valueReferenceToExpression(param.typeValueReference);
let attributeNameType: Expression|null = null;
let optional = false, self = false, skipSelf = false, host = false;
@@ -115,22 +115,18 @@ export function getConstructorDependencies(
* references are converted to an `ExternalExpr`. Note that this is only valid in the context of the
* file in which the `TypeValueReference` originated.
*/
export function valueReferenceToExpression(
valueRef: LocalTypeValueReference|ImportedTypeValueReference,
defaultImportRecorder: DefaultImportRecorder): Expression;
export function valueReferenceToExpression(
valueRef: TypeValueReference, defaultImportRecorder: DefaultImportRecorder): Expression|null;
export function valueReferenceToExpression(
valueRef: TypeValueReference, defaultImportRecorder: DefaultImportRecorder): Expression|null {
export function valueReferenceToExpression(valueRef: LocalTypeValueReference|
ImportedTypeValueReference): Expression;
export function valueReferenceToExpression(valueRef: TypeValueReference): Expression|null;
export function valueReferenceToExpression(valueRef: TypeValueReference): Expression|null {
if (valueRef.kind === TypeValueReferenceKind.UNAVAILABLE) {
return null;
} else if (valueRef.kind === TypeValueReferenceKind.LOCAL) {
if (defaultImportRecorder !== null && valueRef.defaultImportStatement !== null &&
ts.isIdentifier(valueRef.expression)) {
defaultImportRecorder.recordImportedIdentifier(
valueRef.expression, valueRef.defaultImportStatement);
const expr = new WrappedNodeExpr(valueRef.expression);
if (valueRef.defaultImportStatement !== null) {
attachDefaultImportDeclaration(expr, valueRef.defaultImportStatement);
}
return new WrappedNodeExpr(valueRef.expression);
return expr;
} else {
let importExpr: Expression =
new ExternalExpr({moduleName: valueRef.moduleName, name: valueRef.importedName});
@@ -163,10 +159,10 @@ export function unwrapConstructorDependencies(deps: ConstructorDeps|null): R3Dep
}
export function getValidConstructorDependencies(
clazz: ClassDeclaration, reflector: ReflectionHost,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean): R3DependencyMetadata[]|null {
clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]|
null {
return validateConstructorDependencies(
clazz, getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore));
clazz, getConstructorDependencies(clazz, reflector, isCore));
}
/**
@@ -332,36 +328,40 @@ function expandForwardRef(arg: ts.Expression): ts.Expression|null {
}
}
/**
* Possibly resolve a forwardRef() expression into the inner value.
* If the given `node` is a forwardRef() expression then resolve its inner value, otherwise return
* `null`.
*
* @param node the forwardRef() expression to resolve
* @param reflector a ReflectionHost
* @returns the resolved expression, if the original expression was a forwardRef(), or the original
* expression otherwise
* @returns the resolved expression, if the original expression was a forwardRef(), or `null`
* otherwise.
*/
export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression {
export function tryUnwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression|
null {
node = unwrapExpression(node);
if (!ts.isCallExpression(node) || node.arguments.length !== 1) {
return node;
return null;
}
const fn =
ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
if (!ts.isIdentifier(fn)) {
return node;
return null;
}
const expr = expandForwardRef(node.arguments[0]);
if (expr === null) {
return node;
return null;
}
const imp = reflector.getImportOfIdentifier(fn);
if (imp === null || imp.from !== '@angular/core' || imp.name !== 'forwardRef') {
return node;
} else {
return expr;
return null;
}
return expr;
}
/**
@@ -13,7 +13,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {ModuleResolver, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {NOOP_PERF_RECORDER} from '../../perf';
@@ -81,7 +81,6 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
cycleAnalyzer,
CycleHandlingStrategy.UseRemoteScoping,
refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER,
/* depTracker */ null,
injectableRegistry,
/* semanticDepGraphUpdater */ null,
@@ -230,6 +229,36 @@ runInEachFileSystem(() => {
expect(analysis?.resources.styles.size).toBe(3);
});
it('should use an empty source map URL for an indirect template', () => {
const template = '<span>indirect</span>';
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
const TEMPLATE = '${template}';
@Component({
template: TEMPLATE,
}) 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?.template.file?.url).toEqual('');
});
it('does not emit a program with template parse errors', () => {
const template = '{{x ? y }}';
const {program, options, host} = makeProgram([
@@ -303,6 +332,74 @@ runInEachFileSystem(() => {
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.inlineStyles).toEqual(jasmine.arrayWithExactContents(['.xyz {}']));
});
it('should error if canPreprocess is true and async analyze is not used', async () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '',
styles: ['.abc {}']
}) class TestCmp {}
`
},
]);
const {reflectionHost, handler, resourceLoader} = setup(program, options, host);
resourceLoader.canPreload = true;
resourceLoader.canPreprocess = true;
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');
}
expect(() => handler.analyze(TestCmp, detected.metadata))
.toThrowError('Inline resource processing requires asynchronous preanalyze.');
});
it('should not error if component has no inline styles and canPreprocess is true', async () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '',
}) class TestCmp {}
`
},
]);
const {reflectionHost, handler, resourceLoader} = setup(program, options, host);
resourceLoader.canPreload = true;
resourceLoader.canPreprocess = true;
resourceLoader.preprocessInline = async function(data, context) {
fail('preprocessInline should not have been called.');
return data;
};
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');
}
await handler.preanalyze(TestCmp, detected.metadata);
expect(() => handler.analyze(TestCmp, detected.metadata)).not.toThrow();
});
});
function ivyCode(code: ErrorCode): number {
@@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {NOOP_PERF_RECORDER} from '../../perf';
@@ -153,7 +153,7 @@ runInEachFileSystem(() => {
super(checker);
}
hasBaseClass(_class: ClassDeclaration): boolean {
override hasBaseClass(_class: ClassDeclaration): boolean {
return hasBaseClass;
}
}
@@ -168,8 +168,8 @@ runInEachFileSystem(() => {
null);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader,
NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false,
reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader, injectableRegistry,
/*isCore*/ false,
/*semanticDepGraphUpdater*/ null,
/*annotateForClosureCompiler*/ false,
/*detectUndecoratedClassesWithAngularFeatures*/ false, NOOP_PERF_RECORDER);
@@ -8,7 +8,6 @@
import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../imports';
import {InjectableClassRegistry} from '../../metadata';
import {NOOP_PERF_RECORDER} from '../../perf';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
@@ -70,7 +69,7 @@ function setupHandler(errorOnDuplicateProv: boolean) {
const reflectionHost = new TypeScriptReflectionHost(checker);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new InjectableDecoratorHandler(
reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, /* isCore */ false,
reflectionHost, /* isCore */ false,
/* strictCtorDeps */ false, injectableRegistry, NOOP_PERF_RECORDER, errorOnDuplicateProv);
const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration);
const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov');
@@ -5,15 +5,16 @@
* 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 {compileClassMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem, TestFile} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
import {NoopImportRewriter} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing';
import {ImportManager, translateStatement} from '../../translator';
import {generateSetClassMetadataCall} from '../src/metadata';
import {extractClassMetadata} from '../src/metadata';
runInEachFileSystem(() => {
describe('ngtsc setClassMetadata converter', () => {
@@ -127,13 +128,14 @@ runInEachFileSystem(() => {
{target: ts.ScriptTarget.ES2015});
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
const call = extractClassMetadata(target, host, false);
if (call === null) {
return '';
}
const sf = getSourceFileOrError(program, _('/index.ts'));
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im);
const stmt = compileClassMetadata(call).toStmt();
const tsStatement = translateStatement(stmt, im);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}
@@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {LocalIdentifierStrategy, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {NOOP_PERF_RECORDER} from '../../perf';
@@ -72,8 +72,7 @@ runInEachFileSystem(() => {
const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
/* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null,
NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false, injectableRegistry,
NOOP_PERF_RECORDER);
/* annotateForClosureCompiler */ false, injectableRegistry, NOOP_PERF_RECORDER);
const TestModule =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected =
@@ -23,9 +23,9 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/program_driver",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/resource",
"//packages/compiler-cli/src/ngtsc/routing",
@@ -38,6 +38,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//typescript",
],
)
@@ -33,8 +33,13 @@ export interface ResourceHost {
/**
* Converts a file path for a resource that is used in a source file or another resource
* into a filepath.
*
* The optional `fallbackResolve` method can be used as a way to attempt a fallback resolution if
* the implementation's `resourceNameToFileName` resolution fails.
*/
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
resourceNameToFileName(
resourceName: string, containingFilePath: string,
fallbackResolve?: (url: string, fromFile: string) => string | null): string|null;
/**
* Load a referenced resource either statically or asynchronously. If the host returns a
@@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {BazelAndG3Options, I18nOptions, LegacyNgcOptions, MiscOptions, NgcCompatibilityOptions, StrictTemplateOptions} from './public_options';
import {BazelAndG3Options, I18nOptions, LegacyNgcOptions, MiscOptions, NgcCompatibilityOptions, StrictTemplateOptions, TargetOptions} from './public_options';
/**
@@ -36,22 +36,6 @@ export interface TestOnlyOptions {
tracePerformance?: string;
}
/**
* Options that specify compilation target.
*/
export interface TargetOptions {
/**
* Specifies the compilation mode to use. The following modes are available:
* - 'full': generates fully AOT compiled code using Ivy instructions.
* - 'partial': generates code in a stable, but intermediate form suitable to be published to NPM.
*
* To become public once the linker is ready.
*
* @internal
*/
compilationMode?: 'full'|'partial';
}
/**
* A merged interface of all of the various Angular compiler options, as well as the standard
* `ts.CompilerOptions`.
@@ -309,6 +309,22 @@ export interface I18nOptions {
*/
i18nInLocale?: string;
/**
* Export format (xlf, xlf2 or xmb) when the xi18n operation is requested.
*/
i18nOutFormat?: string;
/**
* Path to the extracted message file to emit when the xi18n operation is requested.
*/
i18nOutFile?: string;
/**
* Locale of the application (used when xi18n is requested).
*/
i18nOutLocale?: string;
/**
* Render `$localize` messages with legacy format ids.
*
@@ -340,6 +356,22 @@ export interface I18nOptions {
i18nNormalizeLineEndingsInICUs?: boolean;
}
/**
* Options that specify compilation target.
*
* @publicApi
*/
export interface TargetOptions {
/**
* Specifies the compilation mode to use. The following modes are available:
* - 'full': generates fully AOT compiled code using Ivy instructions.
* - 'partial': generates code in a stable, but intermediate form suitable for publication to NPM.
*
* The default value is 'full'.
*/
compilationMode?: 'full'|'partial';
}
/**
* Miscellaneous options that don't fall into any other category
*
@@ -7,4 +7,4 @@
*/
export * from './src/compiler';
export {NgCompilerHost} from './src/host';
export {NgCompilerHost} from './src/host';
@@ -6,24 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '@angular/compiler';
import * as ts from 'typescript';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from '../../annotations';
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles';
import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, ngErrorCode} from '../../diagnostics';
import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
import {LogicalFileSystem, resolve} from '../../file_system';
import {absoluteFromSourceFile, AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system';
import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports';
import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental';
import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer';
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DirectiveMeta, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, PipeMeta, ResourceRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {ActivePerfRecorder} from '../../perf';
import {PerfCheckpoint, PerfEvent, PerfPhase} from '../../perf/src/api';
import {DelegatingPerfRecorder} from '../../perf/src/recorder';
import {ActivePerfRecorder, DelegatingPerfRecorder, PerfCheckpoint, PerfEvent, PerfPhase} from '../../perf';
import {FileUpdate, ProgramDriver, UpdateMode} from '../../program_driver';
import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {AdapterResourceLoader} from '../../resource';
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
@@ -32,8 +29,9 @@ import {generatedFactoryTransform} from '../../shims';
import {ivySwitchTransform} from '../../switch';
import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
import {TemplateTypeCheckerImpl} from '../../typecheck';
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api';
import {getSourceFileOrNull, isDtsPath, resolveModuleName, toUnredirectedSourceFile} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
import {compileUndecoratedClassesWithAngularFeatures} from './config';
@@ -52,8 +50,6 @@ interface LazyCompilationState {
exportReferenceGraph: ReferenceGraph|null;
routeAnalyzer: NgModuleRouteAnalyzer;
dtsTransforms: DtsTransformRegistry;
mwpScanner: ModuleWithProvidersScanner;
defaultImportTracker: DefaultImportTracker;
aliasingHost: AliasingHost|null;
refEmitter: ReferenceEmitter;
templateTypeChecker: TemplateTypeChecker;
@@ -78,7 +74,7 @@ export interface FreshCompilationTicket {
kind: CompilationTicketKind.Fresh;
options: NgCompilerOptions;
incrementalBuildStrategy: IncrementalBuildStrategy;
typeCheckingProgramStrategy: TypeCheckingProgramStrategy;
programDriver: ProgramDriver;
enableTemplateTypeChecker: boolean;
usePoisonedData: boolean;
tsProgram: ts.Program;
@@ -91,11 +87,10 @@ export interface FreshCompilationTicket {
export interface IncrementalTypeScriptCompilationTicket {
kind: CompilationTicketKind.IncrementalTypeScript;
options: NgCompilerOptions;
oldProgram: ts.Program;
newProgram: ts.Program;
incrementalBuildStrategy: IncrementalBuildStrategy;
typeCheckingProgramStrategy: TypeCheckingProgramStrategy;
newDriver: IncrementalDriver;
incrementalCompilation: IncrementalCompilation;
programDriver: ProgramDriver;
enableTemplateTypeChecker: boolean;
usePoisonedData: boolean;
perfRecorder: ActivePerfRecorder;
@@ -123,15 +118,15 @@ export type CompilationTicket = FreshCompilationTicket|IncrementalTypeScriptComp
*/
export function freshCompilationTicket(
tsProgram: ts.Program, options: NgCompilerOptions,
incrementalBuildStrategy: IncrementalBuildStrategy,
typeCheckingProgramStrategy: TypeCheckingProgramStrategy, perfRecorder: ActivePerfRecorder|null,
enableTemplateTypeChecker: boolean, usePoisonedData: boolean): CompilationTicket {
incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver,
perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean,
usePoisonedData: boolean): CompilationTicket {
return {
kind: CompilationTicketKind.Fresh,
tsProgram,
options,
incrementalBuildStrategy,
typeCheckingProgramStrategy,
programDriver,
enableTemplateTypeChecker,
usePoisonedData,
perfRecorder: perfRecorder ?? ActivePerfRecorder.zeroedToNow(),
@@ -144,25 +139,26 @@ export function freshCompilationTicket(
*/
export function incrementalFromCompilerTicket(
oldCompiler: NgCompiler, newProgram: ts.Program,
incrementalBuildStrategy: IncrementalBuildStrategy,
typeCheckingProgramStrategy: TypeCheckingProgramStrategy, modifiedResourceFiles: Set<string>,
incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver,
modifiedResourceFiles: Set<AbsoluteFsPath>,
perfRecorder: ActivePerfRecorder|null): CompilationTicket {
const oldProgram = oldCompiler.getNextProgram();
const oldDriver = oldCompiler.incrementalStrategy.getIncrementalDriver(oldProgram);
if (oldDriver === null) {
const oldProgram = oldCompiler.getCurrentProgram();
const oldState = oldCompiler.incrementalStrategy.getIncrementalState(oldProgram);
if (oldState === null) {
// No incremental step is possible here, since no IncrementalDriver was found for the old
// program.
return freshCompilationTicket(
newProgram, oldCompiler.options, incrementalBuildStrategy, typeCheckingProgramStrategy,
perfRecorder, oldCompiler.enableTemplateTypeChecker, oldCompiler.usePoisonedData);
newProgram, oldCompiler.options, incrementalBuildStrategy, programDriver, perfRecorder,
oldCompiler.enableTemplateTypeChecker, oldCompiler.usePoisonedData);
}
if (perfRecorder === null) {
perfRecorder = ActivePerfRecorder.zeroedToNow();
}
const newDriver = IncrementalDriver.reconcile(
oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder);
const incrementalCompilation = IncrementalCompilation.incremental(
newProgram, versionMapFromProgram(newProgram, programDriver), oldProgram, oldState,
modifiedResourceFiles, perfRecorder);
return {
kind: CompilationTicketKind.IncrementalTypeScript,
@@ -170,9 +166,8 @@ export function incrementalFromCompilerTicket(
usePoisonedData: oldCompiler.usePoisonedData,
options: oldCompiler.options,
incrementalBuildStrategy,
typeCheckingProgramStrategy,
newDriver,
oldProgram,
incrementalCompilation,
programDriver,
newProgram,
perfRecorder,
};
@@ -182,26 +177,25 @@ export function incrementalFromCompilerTicket(
* Create a `CompilationTicket` directly from an old `ts.Program` and associated Angular compilation
* state, along with a new `ts.Program`.
*/
export function incrementalFromDriverTicket(
oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program,
export function incrementalFromStateTicket(
oldProgram: ts.Program, oldState: IncrementalState, newProgram: ts.Program,
options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy,
typeCheckingProgramStrategy: TypeCheckingProgramStrategy, modifiedResourceFiles: Set<string>,
programDriver: ProgramDriver, modifiedResourceFiles: Set<AbsoluteFsPath>,
perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean,
usePoisonedData: boolean): CompilationTicket {
if (perfRecorder === null) {
perfRecorder = ActivePerfRecorder.zeroedToNow();
}
const newDriver = IncrementalDriver.reconcile(
oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder);
const incrementalCompilation = IncrementalCompilation.incremental(
newProgram, versionMapFromProgram(newProgram, programDriver), oldProgram, oldState,
modifiedResourceFiles, perfRecorder);
return {
kind: CompilationTicketKind.IncrementalTypeScript,
oldProgram,
newProgram,
options,
incrementalBuildStrategy,
newDriver,
typeCheckingProgramStrategy,
incrementalCompilation,
programDriver,
enableTemplateTypeChecker,
usePoisonedData,
perfRecorder,
@@ -255,7 +249,7 @@ export class NgCompiler {
private nonTemplateDiagnostics: ts.Diagnostic[]|null = null;
private closureCompilerEnabled: boolean;
private nextProgram: ts.Program;
private currentProgram: ts.Program;
private entryPoint: ts.SourceFile|null;
private moduleResolver: ModuleResolver;
private resourceManager: AdapterResourceLoader;
@@ -286,9 +280,10 @@ export class NgCompiler {
adapter,
ticket.options,
ticket.tsProgram,
ticket.typeCheckingProgramStrategy,
ticket.programDriver,
ticket.incrementalBuildStrategy,
IncrementalDriver.fresh(ticket.tsProgram),
IncrementalCompilation.fresh(
ticket.tsProgram, versionMapFromProgram(ticket.tsProgram, ticket.programDriver)),
ticket.enableTemplateTypeChecker,
ticket.usePoisonedData,
ticket.perfRecorder,
@@ -298,9 +293,9 @@ export class NgCompiler {
adapter,
ticket.options,
ticket.newProgram,
ticket.typeCheckingProgramStrategy,
ticket.programDriver,
ticket.incrementalBuildStrategy,
ticket.newDriver,
ticket.incrementalCompilation,
ticket.enableTemplateTypeChecker,
ticket.usePoisonedData,
ticket.perfRecorder,
@@ -315,10 +310,10 @@ export class NgCompiler {
private constructor(
private adapter: NgCompilerAdapter,
readonly options: NgCompilerOptions,
private tsProgram: ts.Program,
readonly typeCheckingProgramStrategy: TypeCheckingProgramStrategy,
private inputProgram: ts.Program,
readonly programDriver: ProgramDriver,
readonly incrementalStrategy: IncrementalBuildStrategy,
readonly incrementalDriver: IncrementalDriver,
readonly incrementalCompilation: IncrementalCompilation,
readonly enableTemplateTypeChecker: boolean,
readonly usePoisonedData: boolean,
private livePerfRecorder: ActivePerfRecorder,
@@ -329,11 +324,11 @@ export class NgCompiler {
this.constructionDiagnostics.push(incompatibleTypeCheckOptionsDiagnostic);
}
this.nextProgram = tsProgram;
this.currentProgram = inputProgram;
this.closureCompilerEnabled = !!this.options.annotateForClosureCompiler;
this.entryPoint =
adapter.entryPoint !== null ? getSourceFileOrNull(tsProgram, adapter.entryPoint) : null;
adapter.entryPoint !== null ? getSourceFileOrNull(inputProgram, adapter.entryPoint) : null;
const moduleResolutionCache = ts.createModuleResolutionCache(
this.adapter.getCurrentDirectory(),
@@ -343,19 +338,19 @@ export class NgCompiler {
// way into all kinds of places inside TS internal objects.
this.adapter.getCanonicalFileName.bind(this.adapter));
this.moduleResolver =
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
new ModuleResolver(inputProgram, this.options, this.adapter, moduleResolutionCache);
this.resourceManager = new AdapterResourceLoader(adapter, this.options);
this.cycleAnalyzer =
new CycleAnalyzer(new ImportGraph(tsProgram.getTypeChecker(), this.delegatingPerfRecorder));
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, tsProgram);
this.cycleAnalyzer = new CycleAnalyzer(
new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder));
this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, inputProgram);
this.ignoreForDiagnostics =
new Set(tsProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf)));
new Set(inputProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf)));
this.ignoreForEmit = this.adapter.ignoreForEmit;
let dtsFileCount = 0;
let nonDtsFileCount = 0;
for (const sf of tsProgram.getSourceFiles()) {
for (const sf of inputProgram.getSourceFiles()) {
if (sf.isDeclarationFile) {
dtsFileCount++;
} else {
@@ -371,6 +366,16 @@ export class NgCompiler {
return this.livePerfRecorder;
}
/**
* Exposes the `IncrementalCompilation` under an old property name that the CLI uses, avoiding a
* chicken-and-egg problem with the rename to `incrementalCompilation`.
*
* TODO(alxhub): remove when the CLI uses the new name.
*/
get incrementalDriver(): IncrementalCompilation {
return this.incrementalCompilation;
}
private updateWithChangedResources(
changedResources: Set<string>, perfRecorder: ActivePerfRecorder): void {
this.livePerfRecorder = perfRecorder;
@@ -415,7 +420,7 @@ export class NgCompiler {
getResourceDependencies(file: ts.SourceFile): string[] {
this.ensureAnalyzed();
return this.incrementalDriver.depGraph.getResourceDependencies(file);
return this.incrementalCompilation.depGraph.getResourceDependencies(file);
}
/**
@@ -462,16 +467,22 @@ export class NgCompiler {
}
/**
* Get the `ts.Program` to use as a starting point when spawning a subsequent incremental
* compilation.
* Get the current `ts.Program` known to this `NgCompiler`.
*
* The `NgCompiler` spawns an internal incremental TypeScript compilation (inheriting the
* consumer's `ts.Program` into a new one for the purposes of template type-checking). After this
* operation, the consumer's `ts.Program` is no longer usable for starting a new incremental
* compilation. `getNextProgram` retrieves the `ts.Program` which can be used instead.
* Compilation begins with an input `ts.Program`, and during template type-checking operations new
* `ts.Program`s may be produced using the `ProgramDriver`. The most recent such `ts.Program` to
* be produced is available here.
*
* This `ts.Program` serves two key purposes:
*
* * As an incremental starting point for creating the next `ts.Program` based on files that the
* user has changed (for clients using the TS compiler program APIs).
*
* * As the "before" point for an incremental compilation invocation, to determine what's changed
* between the old and new programs (for all compilations).
*/
getNextProgram(): ts.Program {
return this.nextProgram;
getCurrentProgram(): ts.Program {
return this.currentProgram;
}
getTemplateTypeChecker(): TemplateTypeChecker {
@@ -515,6 +526,19 @@ export class NgCompiler {
return {styles, template};
}
getMeta(classDecl: DeclarationNode): PipeMeta|DirectiveMeta|null {
if (!isNamedClassDeclaration(classDecl)) {
return null;
}
const ref = new Reference(classDecl);
const {metaReader} = this.ensureAnalyzed();
const meta = metaReader.getPipeMetadata(ref) ?? metaReader.getDirectiveMetadata(ref);
if (meta === null) {
return null;
}
return meta;
}
/**
* Perform Angular's analysis step (as a precursor to `getDiagnostics` or `prepareEmit`)
* asynchronously.
@@ -533,13 +557,12 @@ export class NgCompiler {
this.compilation = this.makeCompilation();
const promises: Promise<void>[] = [];
for (const sf of this.tsProgram.getSourceFiles()) {
for (const sf of this.inputProgram.getSourceFiles()) {
if (sf.isDeclarationFile) {
continue;
}
let analysisPromise = this.compilation.traitCompiler.analyzeAsync(sf);
this.scanForMwp(sf);
if (analysisPromise !== undefined) {
promises.push(analysisPromise);
}
@@ -579,7 +602,7 @@ export class NgCompiler {
//
// In all cases above, the `containingFile` argument is ignored, so we can just take the first
// of the root files.
const containingFile = this.tsProgram.getRootFileNames()[0];
const containingFile = this.inputProgram.getRootFileNames()[0];
const [entryPath, moduleName] = entryRoute.split('#');
const resolvedModule =
resolveModuleName(entryPath, containingFile, this.options, this.adapter, null);
@@ -602,7 +625,7 @@ export class NgCompiler {
} {
const compilation = this.ensureAnalyzed();
const coreImportsFrom = compilation.isCore ? getR3SymbolsFile(this.tsProgram) : null;
const coreImportsFrom = compilation.isCore ? getR3SymbolsFile(this.inputProgram) : null;
let importRewriter: ImportRewriter;
if (coreImportsFrom !== null) {
importRewriter = new R3SymbolsImportRewriter(coreImportsFrom.fileName);
@@ -610,13 +633,14 @@ export class NgCompiler {
importRewriter = new NoopImportRewriter();
}
const defaultImportTracker = new DefaultImportTracker();
const before = [
ivyTransformFactory(
compilation.traitCompiler, compilation.reflector, importRewriter,
compilation.defaultImportTracker, this.delegatingPerfRecorder, compilation.isCore,
this.closureCompilerEnabled),
compilation.traitCompiler, compilation.reflector, importRewriter, defaultImportTracker,
this.delegatingPerfRecorder, compilation.isCore, this.closureCompilerEnabled),
aliasTransformFactory(compilation.traitCompiler.exportStatements),
compilation.defaultImportTracker.importPreservingTransformer(),
defaultImportTracker.importPreservingTransformer(),
];
const afterDeclarations: ts.TransformerFactory<ts.SourceFile>[] = [];
@@ -651,6 +675,16 @@ export class NgCompiler {
return generateAnalysis(context);
}
/**
* Collect i18n messages into the `Xi18nContext`.
*/
xi18n(ctx: Xi18nContext): void {
// Note that the 'resolve' phase is not strictly necessary for xi18n, but this is not currently
// optimized.
const compilation = this.ensureAnalyzed();
compilation.traitCompiler.xi18n(ctx);
}
private ensureAnalyzed(this: NgCompiler): LazyCompilationState {
if (this.compilation === null) {
this.analyzeSync();
@@ -661,12 +695,11 @@ export class NgCompiler {
private analyzeSync(): void {
this.perfRecorder.inPhase(PerfPhase.Analysis, () => {
this.compilation = this.makeCompilation();
for (const sf of this.tsProgram.getSourceFiles()) {
for (const sf of this.inputProgram.getSourceFiles()) {
if (sf.isDeclarationFile) {
continue;
}
this.compilation.traitCompiler.analyzeSync(sf);
this.scanForMwp(sf);
}
this.perfRecorder.memory(PerfCheckpoint.Analysis);
@@ -681,7 +714,7 @@ export class NgCompiler {
// At this point, analysis is complete and the compiler can now calculate which files need to
// be emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler);
this.incrementalCompilation.recordSuccessfulAnalysis(traitCompiler);
this.perfRecorder.memory(PerfCheckpoint.Resolve);
});
@@ -703,7 +736,7 @@ export class NgCompiler {
// is not disabled when `strictTemplates` is enabled.
const strictTemplates = !!this.options.strictTemplates;
const useInlineTypeConstructors = this.typeCheckingProgramStrategy.supportsInlineOperations;
const useInlineTypeConstructors = this.programDriver.supportsInlineOperations;
// First select a type-checking configuration, based on whether full template type-checking is
// requested.
@@ -816,7 +849,7 @@ export class NgCompiler {
// Get the diagnostics.
const diagnostics: ts.Diagnostic[] = [];
for (const sf of this.tsProgram.getSourceFiles()) {
for (const sf of this.inputProgram.getSourceFiles()) {
if (sf.isDeclarationFile || this.adapter.isShim(sf)) {
continue;
}
@@ -825,9 +858,9 @@ export class NgCompiler {
...compilation.templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram));
}
const program = this.typeCheckingProgramStrategy.getProgram();
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program);
this.nextProgram = program;
const program = this.programDriver.getProgram();
this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program;
return diagnostics;
}
@@ -842,9 +875,9 @@ export class NgCompiler {
diagnostics.push(...compilation.templateTypeChecker.getDiagnosticsForFile(sf, optimizeFor));
}
const program = this.typeCheckingProgramStrategy.getProgram();
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program);
this.nextProgram = program;
const program = this.programDriver.getProgram();
this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program;
return diagnostics;
}
@@ -855,24 +888,14 @@ export class NgCompiler {
this.nonTemplateDiagnostics = [...compilation.traitCompiler.diagnostics];
if (this.entryPoint !== null && compilation.exportReferenceGraph !== null) {
this.nonTemplateDiagnostics.push(...checkForPrivateExports(
this.entryPoint, this.tsProgram.getTypeChecker(), compilation.exportReferenceGraph));
this.entryPoint, this.inputProgram.getTypeChecker(), compilation.exportReferenceGraph));
}
}
return this.nonTemplateDiagnostics;
}
private scanForMwp(sf: ts.SourceFile): void {
this.compilation!.mwpScanner.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
// Only obtain the return type transform for the source file once there's a type to replace,
// so that no transform is allocated when there's nothing to do.
this.compilation!.dtsTransforms!.getReturnTypeTransform(sf).addTypeReplacement(node, type);
}
});
}
private makeCompilation(): LazyCompilationState {
const checker = this.tsProgram.getTypeChecker();
const checker = this.inputProgram.getTypeChecker();
const reflector = new TypeScriptReflectionHost(checker);
@@ -904,7 +927,7 @@ export class NgCompiler {
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Next, attempt to use an absolute import.
new AbsoluteModuleStrategy(this.tsProgram, checker, this.moduleResolver, reflector),
new AbsoluteModuleStrategy(this.inputProgram, checker, this.moduleResolver, reflector),
// Finally, check if the reference is being written into a file within the project's .ts
// sources, and use a relative import if so. If this fails, ReferenceEmitter will throw
// an error.
@@ -932,7 +955,8 @@ export class NgCompiler {
aliasingHost = new UnifiedModulesAliasingHost(this.adapter.unifiedModulesHost);
}
const evaluator = new PartialEvaluator(reflector, checker, this.incrementalDriver.depGraph);
const evaluator =
new PartialEvaluator(reflector, checker, this.incrementalCompilation.depGraph);
const dtsReader = new DtsMetadataReader(checker, reflector);
const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader: MetadataReader = localMetaRegistry;
@@ -940,7 +964,7 @@ export class NgCompiler {
const scopeRegistry =
new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry;
const semanticDepGraphUpdater = this.incrementalDriver.getSemanticDepGraphUpdater();
const semanticDepGraphUpdater = this.incrementalCompilation.semanticDepGraphUpdater;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(reflector);
@@ -964,11 +988,8 @@ export class NgCompiler {
const dtsTransforms = new DtsTransformRegistry();
const mwpScanner = new ModuleWithProvidersScanner(reflector, evaluator, refEmitter);
const isCore = isAngularCorePackage(this.inputProgram);
const isCore = isAngularCorePackage(this.tsProgram);
const defaultImportTracker = new DefaultImportTracker();
const resourceRegistry = new ResourceRegistry();
const compilationMode =
@@ -990,7 +1011,7 @@ export class NgCompiler {
this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
cycleHandlingStrategy, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph,
cycleHandlingStrategy, refEmitter, this.incrementalCompilation.depGraph,
injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled,
this.delegatingPerfRecorder),
@@ -999,7 +1020,7 @@ export class NgCompiler {
// clang-format off
new DirectiveDecoratorHandler(
reflector, evaluator, metaRegistry, scopeRegistry, metaReader,
defaultImportTracker, injectableRegistry, isCore, semanticDepGraphUpdater,
injectableRegistry, isCore, semanticDepGraphUpdater,
this.closureCompilerEnabled, compileUndecoratedClassesWithAngularFeatures,
this.delegatingPerfRecorder,
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null,unknown>>,
@@ -1007,27 +1028,34 @@ export class NgCompiler {
// Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them)
new PipeDecoratorHandler(
reflector, evaluator, metaRegistry, scopeRegistry, defaultImportTracker,
injectableRegistry, isCore, this.delegatingPerfRecorder),
reflector, evaluator, metaRegistry, scopeRegistry, injectableRegistry, isCore,
this.delegatingPerfRecorder),
new InjectableDecoratorHandler(
reflector, defaultImportTracker, isCore, this.options.strictInjectionParameters || false,
injectableRegistry, this.delegatingPerfRecorder),
reflector, isCore, this.options.strictInjectionParameters || false, injectableRegistry,
this.delegatingPerfRecorder),
new NgModuleDecoratorHandler(
reflector, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, isCore,
routeAnalyzer, refEmitter, this.adapter.factoryTracker, defaultImportTracker,
this.closureCompilerEnabled, injectableRegistry, this.delegatingPerfRecorder,
this.options.i18nInLocale),
routeAnalyzer, refEmitter, this.adapter.factoryTracker, this.closureCompilerEnabled,
injectableRegistry, this.delegatingPerfRecorder, this.options.i18nInLocale),
];
const traitCompiler = new TraitCompiler(
handlers, reflector, this.delegatingPerfRecorder, this.incrementalDriver,
handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation,
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
semanticDepGraphUpdater);
// Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this
// happens, they need to be tracked by the `NgCompiler`.
const notifyingDriver =
new NotifyingProgramDriverWrapper(this.programDriver, (program: ts.Program) => {
this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program;
});
const templateTypeChecker = new TemplateTypeCheckerImpl(
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver,
scopeRegistry, typeCheckScopeRegistry, this.delegatingPerfRecorder);
this.inputProgram, notifyingDriver, traitCompiler, this.getTypeCheckingConfig(), refEmitter,
reflector, this.adapter, this.incrementalCompilation, scopeRegistry, typeCheckScopeRegistry,
this.delegatingPerfRecorder);
return {
isCore,
@@ -1037,10 +1065,8 @@ export class NgCompiler {
dtsTransforms,
exportReferenceGraph,
routeAnalyzer,
mwpScanner,
metaReader,
typeCheckScopeRegistry,
defaultImportTracker,
aliasingHost,
refEmitter,
templateTypeChecker,
@@ -1141,3 +1167,37 @@ class ReferenceGraphAdapter implements ReferencesRegistry {
}
}
}
class NotifyingProgramDriverWrapper implements ProgramDriver {
constructor(
private delegate: ProgramDriver, private notifyNewProgram: (program: ts.Program) => void) {}
get supportsInlineOperations() {
return this.delegate.supportsInlineOperations;
}
getProgram(): ts.Program {
return this.delegate.getProgram();
}
updateFiles(contents: Map<AbsoluteFsPath, FileUpdate>, updateMode: UpdateMode): void {
this.delegate.updateFiles(contents, updateMode);
this.notifyNewProgram(this.delegate.getProgram());
}
getSourceFileVersion = this.delegate.getSourceFileVersion?.bind(this);
}
function versionMapFromProgram(
program: ts.Program, driver: ProgramDriver): Map<AbsoluteFsPath, string>|null {
if (driver.getSourceFileVersion === undefined) {
return null;
}
const versions = new Map<AbsoluteFsPath, string>();
for (const possiblyRedirectedSourceFile of program.getSourceFiles()) {
const sf = toUnredirectedSourceFile(possiblyRedirectedSourceFile);
versions.set(absoluteFromSourceFile(sf), driver.getSourceFileVersion(sf));
}
return versions;
}
@@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/program_driver",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
@@ -11,9 +11,9 @@ import * as ts from 'typescript';
import {absoluteFrom as _, FileSystem, getFileSystem, getSourceFileOrError, NgtscCompilerHost, setFileSystem} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {IncrementalBuildStrategy, NoopIncrementalBuildStrategy} from '../../incremental';
import {ProgramDriver, TsCreateProgramDriver} from '../../program_driver';
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
import {ReusedProgramStrategy} from '../../typecheck';
import {OptimizeFor, TypeCheckingProgramStrategy} from '../../typecheck/api';
import {OptimizeFor} from '../../typecheck/api';
import {NgCompilerOptions} from '../api';
@@ -22,7 +22,7 @@ import {NgCompilerHost} from '../src/host';
function makeFreshCompiler(
host: NgCompilerHost, options: NgCompilerOptions, program: ts.Program,
programStrategy: TypeCheckingProgramStrategy, incrementalStrategy: IncrementalBuildStrategy,
programStrategy: ProgramDriver, incrementalStrategy: IncrementalBuildStrategy,
enableTemplateTypeChecker: boolean, usePoisonedData: boolean): NgCompiler {
const ticket = freshCompilationTicket(
program, options, incrementalStrategy, programStrategy, /* perfRecorder */ null,
@@ -61,7 +61,7 @@ runInEachFileSystem(() => {
const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null);
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
@@ -113,7 +113,7 @@ runInEachFileSystem(() => {
const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC');
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
const components = compiler.getComponentsWithTemplateFile(templateFile);
@@ -165,7 +165,7 @@ runInEachFileSystem(() => {
const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA');
const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC');
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
const components = compiler.getComponentsWithStyleFile(styleFile);
@@ -199,7 +199,7 @@ runInEachFileSystem(() => {
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA');
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
const resources = compiler.getComponentResources(CmpA);
@@ -235,7 +235,7 @@ runInEachFileSystem(() => {
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA');
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
const resources = compiler.getComponentResources(CmpA);
@@ -267,7 +267,7 @@ runInEachFileSystem(() => {
const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null);
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const compiler = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
@@ -301,7 +301,7 @@ runInEachFileSystem(() => {
const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null);
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const compilerA = makeFreshCompiler(
host, options, program, new ReusedProgramStrategy(program, host, options, []),
host, options, program, new TsCreateProgramDriver(program, host, options, []),
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
/* usePoisonedData */ false);
@@ -14,6 +14,14 @@ import {ImportGraph} from './imports';
* Analyzes a `ts.Program` for cycles.
*/
export class CycleAnalyzer {
/**
* Cycle detection is requested with the same `from` source file for all used directives and pipes
* within a component, which makes it beneficial to cache the results as long as the `from` source
* file has not changed. This avoids visiting the import graph that is reachable from multiple
* directives/pipes more than once.
*/
private cachedResults: CycleResults|null = null;
constructor(private importGraph: ImportGraph) {}
/**
@@ -24,10 +32,13 @@ export class CycleAnalyzer {
* otherwise.
*/
wouldCreateCycle(from: ts.SourceFile, to: ts.SourceFile): Cycle|null {
// Try to reuse the cached results as long as the `from` source file is the same.
if (this.cachedResults === null || this.cachedResults.from !== from) {
this.cachedResults = new CycleResults(from, this.importGraph);
}
// Import of 'from' -> 'to' is illegal if an edge 'to' -> 'from' already exists.
return this.importGraph.transitiveImportsOf(to).has(from) ?
new Cycle(this.importGraph, from, to) :
null;
return this.cachedResults.wouldBeCyclic(to) ? new Cycle(this.importGraph, from, to) : null;
}
/**
@@ -37,10 +48,83 @@ export class CycleAnalyzer {
* import graph for cycle creation.
*/
recordSyntheticImport(from: ts.SourceFile, to: ts.SourceFile): void {
this.cachedResults = null;
this.importGraph.addSyntheticImport(from, to);
}
}
const NgCyclicResult = Symbol('NgCyclicResult');
type CyclicResultMarker = {
__brand: 'CyclicResultMarker';
};
type CyclicSourceFile = ts.SourceFile&{[NgCyclicResult]?: CyclicResultMarker};
/**
* Stores the results of cycle detection in a memory efficient manner. A symbol is attached to
* source files that indicate what the cyclic analysis result is, as indicated by two markers that
* are unique to this instance. This alleviates memory pressure in large import graphs, as each
* execution is able to store its results in the same memory location (i.e. in the symbol
* on the source file) as earlier executions.
*/
class CycleResults {
private readonly cyclic = {} as CyclicResultMarker;
private readonly acyclic = {} as CyclicResultMarker;
constructor(readonly from: ts.SourceFile, private importGraph: ImportGraph) {}
wouldBeCyclic(sf: ts.SourceFile): boolean {
const cached = this.getCachedResult(sf);
if (cached !== null) {
// The result for this source file has already been computed, so return its result.
return cached;
}
if (sf === this.from) {
// We have reached the source file that we want to create an import from, which means that
// doing so would create a cycle.
return true;
}
// Assume for now that the file will be acyclic; this prevents infinite recursion in the case
// that `sf` is visited again as part of an existing cycle in the graph.
this.markAcyclic(sf);
const imports = this.importGraph.importsOf(sf);
for (const imported of imports) {
if (this.wouldBeCyclic(imported)) {
this.markCyclic(sf);
return true;
}
}
return false;
}
/**
* Returns whether the source file is already known to be cyclic, or `null` if the result is not
* yet known.
*/
private getCachedResult(sf: CyclicSourceFile): boolean|null {
const result = sf[NgCyclicResult];
if (result === this.cyclic) {
return true;
} else if (result === this.acyclic) {
return false;
} else {
// Either the symbol is missing or its value does not correspond with one of the current
// result markers. As such, the result is unknown.
return null;
}
}
private markCyclic(sf: CyclicSourceFile): void {
sf[NgCyclicResult] = this.cyclic;
}
private markAcyclic(sf: CyclicSourceFile): void {
sf[NgCyclicResult] = this.acyclic;
}
}
/**
* Represents an import cycle between `from` and `to` in the program.
*
@@ -17,7 +17,7 @@ import {PerfPhase, PerfRecorder} from '../../perf';
* dependencies within the same program are tracked; imports into packages on NPM are not.
*/
export class ImportGraph {
private map = new Map<ts.SourceFile, Set<ts.SourceFile>>();
private imports = new Map<ts.SourceFile, Set<ts.SourceFile>>();
constructor(private checker: ts.TypeChecker, private perf: PerfRecorder) {}
@@ -27,29 +27,10 @@ export class ImportGraph {
* This operation is cached.
*/
importsOf(sf: ts.SourceFile): Set<ts.SourceFile> {
if (!this.map.has(sf)) {
this.map.set(sf, this.scanImports(sf));
if (!this.imports.has(sf)) {
this.imports.set(sf, this.scanImports(sf));
}
return this.map.get(sf)!;
}
/**
* Lists the transitive imports of a given `ts.SourceFile`.
*/
transitiveImportsOf(sf: ts.SourceFile): Set<ts.SourceFile> {
const imports = new Set<ts.SourceFile>();
this.transitiveImportsOfHelper(sf, imports);
return imports;
}
private transitiveImportsOfHelper(sf: ts.SourceFile, results: Set<ts.SourceFile>): void {
if (results.has(sf)) {
return;
}
results.add(sf);
this.importsOf(sf).forEach(imported => {
this.transitiveImportsOfHelper(imported, results);
});
return this.imports.get(sf)!;
}
/**
@@ -110,6 +91,13 @@ export class ImportGraph {
continue;
}
if (ts.isImportDeclaration(stmt) && stmt.importClause !== undefined &&
stmt.importClause.isTypeOnly) {
// Exclude type-only imports as they are always elided, so they don't contribute to
// cycles.
continue;
}
const symbol = this.checker.getSymbolAtLocation(stmt.moduleSpecifier);
if (symbol === undefined || symbol.valueDeclaration === undefined) {
// No symbol could be found to skip over this import/export.
@@ -36,6 +36,22 @@ runInEachFileSystem(() => {
expect(importPath(cycle!.getPath())).toEqual('b,a,b');
});
it('should deal with cycles', () => {
// a -> b -> c -> d
// ^---------/
const {program, analyzer} = makeAnalyzer('a:b;b:c;c:d;d:b');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
const d = getSourceFileOrError(program, (_('/d.ts')));
expect(analyzer.wouldCreateCycle(a, b)).toBe(null);
expect(analyzer.wouldCreateCycle(a, c)).toBe(null);
expect(analyzer.wouldCreateCycle(a, d)).toBe(null);
expect(analyzer.wouldCreateCycle(b, a)).not.toBe(null);
expect(analyzer.wouldCreateCycle(b, c)).not.toBe(null);
expect(analyzer.wouldCreateCycle(b, d)).not.toBe(null);
});
it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
@@ -70,6 +86,17 @@ runInEachFileSystem(() => {
expect(cycle).toBeInstanceOf(Cycle);
expect(importPath(cycle!.getPath())).toEqual('b,c,b');
});
it('should not consider type-only imports', () => {
const {program, analyzer} = makeAnalyzer('a:b,c!;b;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(c, a)).toBe(null);
const cycle = analyzer.wouldCreateCycle(b, a);
expect(cycle).toBeInstanceOf(Cycle);
expect(importPath(cycle!.getPath())).toEqual('b,a,b');
});
});
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
@@ -28,34 +28,6 @@ runInEachFileSystem(() => {
});
});
describe('transitiveImportsOf()', () => {
it('should calculate transitive imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
});
it('should calculate transitive imports in a more complex program (with a cycle)', () => {
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
});
it('should reflect the addition of a synthetic import', () => {
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
const d = getSourceFileOrError(program, (_('/d.ts')));
expect(importsToString(graph.importsOf(b))).toEqual('');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
graph.addSyntheticImport(b, c);
expect(importsToString(graph.importsOf(b))).toEqual('c');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
});
});
describe('findPath()', () => {
it('should be able to compute the path between two source files if there is a cycle', () => {
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
@@ -30,6 +30,8 @@ import {makeProgram} from '../../testing';
* "a:*b,c;b;c"
*
* represents a program where a.ts exports from b.ts and imports from c.ts.
*
* An import can be suffixed with ! to make it a type-only import.
*/
export function makeProgramFromGraph(fs: PathManipulation, graph: string): {
program: ts.Program,
@@ -43,6 +45,9 @@ export function makeProgramFromGraph(fs: PathManipulation, graph: string): {
if (i.startsWith('*')) {
const sym = i.substr(1);
return `export {${sym}} from './${sym}';`;
} else if (i.endsWith('!')) {
const sym = i.substr(0, i.length - 1);
return `import type {${sym}} from './${sym}';`;
} else {
return `import {${i}} from './${i}';`;
}
@@ -50,6 +50,12 @@ export enum ErrorCode {
*/
COMPONENT_RESOURCE_NOT_FOUND = 2008,
/**
* Raised when a component uses `ShadowDom` view encapsulation, but its selector
* does not match the shadow DOM tag name requirements.
*/
COMPONENT_INVALID_SHADOW_DOM_SELECTOR = 2009,
SYMBOL_NOT_EXPORTED = 3001,
SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002,
/**
@@ -110,6 +116,12 @@ export enum ErrorCode {
*/
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
/**
* Not actually raised by the compiler, but reserved for documentation of a View Engine error when
* a View Engine build depends on an Ivy-compiled NgModule.
*/
NGMODULE_VE_DEPENDENCY_ON_IVY_LIB = 6999,
/**
* An element name failed validation against the DOM schema.
*/
@@ -154,6 +166,22 @@ export enum ErrorCode {
*/
DUPLICATE_VARIABLE_DECLARATION = 8006,
/**
* A template has a two way binding (two bindings created by a single syntactial element)
* in which the input and output are going to different places.
*/
SPLIT_TWO_WAY_BINDING = 8007,
/**
* A two way binding in a template has an incorrect syntax,
* parentheses outside brackets. For example:
*
* ```
* <div ([foo])="bar" />
* ```
*/
INVALID_BANANA_IN_BOX = 8101,
/**
* The template type-checking engine would need to generate an inline type check block for a
* component, but the current type-checking environment doesn't support it.
@@ -209,6 +237,7 @@ export const COMPILER_ERRORS_WITH_GUIDES = new Set([
ErrorCode.SCHEMA_INVALID_ELEMENT,
ErrorCode.SCHEMA_INVALID_ATTRIBUTE,
ErrorCode.MISSING_REFERENCE_TARGET,
ErrorCode.COMPONENT_INVALID_SHADOW_DOM_SELECTOR,
]);
/**
@@ -8,9 +8,7 @@ ts_library(
"src/**/*.ts",
]),
deps = [
"@npm//@types/fs-extra",
"@npm//@types/node",
"@npm//fs-extra",
"@npm//typescript",
],
)
@@ -29,11 +29,21 @@ export function absoluteFrom(path: string): AbsoluteFsPath {
return fs.resolve(path);
}
const ABSOLUTE_PATH = Symbol('AbsolutePath');
/**
* Extract an `AbsoluteFsPath` from a `ts.SourceFile`.
* Extract an `AbsoluteFsPath` from a `ts.SourceFile`-like object.
*/
export function absoluteFromSourceFile(sf: ts.SourceFile): AbsoluteFsPath {
return fs.resolve(sf.fileName);
export function absoluteFromSourceFile(sf: {fileName: string}): AbsoluteFsPath {
const sfWithPatch = sf as {fileName: string, [ABSOLUTE_PATH]?: AbsoluteFsPath};
if (sfWithPatch[ABSOLUTE_PATH] === undefined) {
sfWithPatch[ABSOLUTE_PATH] = fs.resolve(sfWithPatch.fileName);
}
// Non-null assertion needed since TS doesn't narrow the type of fields that use a symbol as a key
// apparently.
return sfWithPatch[ABSOLUTE_PATH]!;
}
/**
@@ -7,7 +7,6 @@
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
import * as p from 'path';
import {AbsoluteFsPath, FileStats, FileSystem, PathManipulation, PathSegment, PathString, ReadonlyFileSystem} from './types';
@@ -121,7 +120,7 @@ export class NodeJSFileSystem extends NodeJSReadonlyFileSystem implements FileSy
}
}
removeDeep(path: AbsoluteFsPath): void {
fsExtra.removeSync(path);
fs.rmdirSync(path, {recursive: true});
}
private safeMkdir(path: AbsoluteFsPath): void {
@@ -11,7 +11,6 @@ ts_library(
deps = [
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//@types/fs-extra",
"@npm//typescript",
],
)
@@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as realFs from 'fs';
import * as fsExtra from 'fs-extra';
import * as os from 'os';
import {NodeJSFileSystem, NodeJSPathManipulation, NodeJSReadonlyFileSystem} from '../src/node_js_file_system';
import {AbsoluteFsPath, PathSegment} from '../src/types';
@@ -269,10 +268,10 @@ describe('NodeJSFileSystem', () => {
});
describe('removeDeep()', () => {
it('should delegate to fsExtra.remove()', () => {
const spy = spyOn(fsExtra, 'removeSync');
it('should delegate to rmdirSync()', () => {
const spy = spyOn(realFs, 'rmdirSync');
fs.removeDeep(abcPath);
expect(spy).toHaveBeenCalledWith(abcPath);
expect(spy).toHaveBeenCalledWith(abcPath, {recursive: true});
});
});
});
@@ -21,36 +21,36 @@ export class MockFileSystemNative extends MockFileSystem {
// Delegate to the real NodeJSFileSystem for these path related methods
resolve(...paths: string[]): AbsoluteFsPath {
override resolve(...paths: string[]): AbsoluteFsPath {
return NodeJSFileSystem.prototype.resolve.call(this, this.pwd(), ...paths);
}
dirname<T extends string>(file: T): T {
override dirname<T extends string>(file: T): T {
return NodeJSFileSystem.prototype.dirname.call(this, file) as T;
}
join<T extends string>(basePath: T, ...paths: string[]): T {
override join<T extends string>(basePath: T, ...paths: string[]): T {
return NodeJSFileSystem.prototype.join.call(this, basePath, ...paths) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
override relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
return NodeJSFileSystem.prototype.relative.call(this, from, to);
}
basename(filePath: string, extension?: string): PathSegment {
override basename(filePath: string, extension?: string): PathSegment {
return NodeJSFileSystem.prototype.basename.call(this, filePath, extension);
}
isCaseSensitive() {
override isCaseSensitive() {
return NodeJSFileSystem.prototype.isCaseSensitive.call(this);
}
isRooted(path: string): boolean {
override isRooted(path: string): boolean {
return NodeJSFileSystem.prototype.isRooted.call(this, path);
}
isRoot(path: AbsoluteFsPath): boolean {
override isRoot(path: AbsoluteFsPath): boolean {
return NodeJSFileSystem.prototype.isRoot.call(this, path);
}
normalize<T extends PathString>(path: T): T {
override normalize<T extends PathString>(path: T): T {
// When running in Windows, absolute paths are normalized to always include a drive letter. This
// ensures that rooted posix paths used in tests will be normalized to real Windows paths, i.e.
// including a drive letter. Note that the same normalization is done in emulated Windows mode
@@ -63,7 +63,7 @@ export class MockFileSystemNative extends MockFileSystem {
return NodeJSFileSystem.prototype.normalize.call(this, path) as T;
}
protected splitPath<T>(path: string): string[] {
protected override splitPath<T>(path: string): string[] {
return path.split(/[\\\/]/);
}
}
@@ -12,36 +12,36 @@ import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemPosix extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
override resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.posix.resolve(this.pwd(), ...paths);
return this.normalize(resolved) as AbsoluteFsPath;
}
dirname<T extends string>(file: T): T {
override dirname<T extends string>(file: T): T {
return this.normalize(p.posix.dirname(file)) as T;
}
join<T extends string>(basePath: T, ...paths: string[]): T {
override join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.posix.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
override relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
return this.normalize(p.posix.relative(from, to)) as PathSegment | AbsoluteFsPath;
}
basename(filePath: string, extension?: string): PathSegment {
override basename(filePath: string, extension?: string): PathSegment {
return p.posix.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean {
override isRooted(path: string): boolean {
return path.startsWith('/');
}
protected splitPath<T extends PathString>(path: T): string[] {
protected override splitPath<T extends PathString>(path: T): string[] {
return path.split('/');
}
normalize<T extends PathString>(path: T): T {
override normalize<T extends PathString>(path: T): T {
return path.replace(/^[a-z]:\//i, '/').replace(/\\/g, '/') as T;
}
}
@@ -12,36 +12,36 @@ import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemWindows extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
override resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.win32.resolve(this.pwd(), ...paths);
return this.normalize(resolved as AbsoluteFsPath);
}
dirname<T extends string>(path: T): T {
override dirname<T extends string>(path: T): T {
return this.normalize(p.win32.dirname(path) as T);
}
join<T extends string>(basePath: T, ...paths: string[]): T {
override join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.win32.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
override relative<T extends PathString>(from: T, to: T): PathSegment|AbsoluteFsPath {
return this.normalize(p.win32.relative(from, to)) as PathSegment | AbsoluteFsPath;
}
basename(filePath: string, extension?: string): PathSegment {
override basename(filePath: string, extension?: string): PathSegment {
return p.win32.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean {
override isRooted(path: string): boolean {
return /^([A-Z]:)?([\\\/]|$)/i.test(path);
}
protected splitPath<T extends PathString>(path: T): string[] {
protected override splitPath<T extends PathString>(path: T): string[] {
return path.split(/[\\\/]/);
}
normalize<T extends PathString>(path: T): T {
override normalize<T extends PathString>(path: T): T {
return path.replace(/^[\/\\]/i, 'C:/').replace(/\\/g, '/') as T;
}
}
@@ -167,8 +167,6 @@ It consists of two mechanisms:
1. A `DefaultImportTracker`, which records information about both default imports encountered in the program as well as usages of those imports added during compilation.
A `DefaultImportRecorder` interface is used to allow for a noop implementation in cases (like ngcc) where this tracking isn't necessary.
2. A TypeScript transformer which processes default import statements and can preserve those which are actually used.
This is accessed via `DefaultImportTracker.importPreservingTransformer`.
@@ -8,7 +8,7 @@
export {AliasingHost, AliasStrategy, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default';
export {DefaultImportTracker} from './src/default';
export {AbsoluteModuleStrategy, EmittedReference, ImportedFile, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter';
export {Reexport} from './src/reexport';
export {OwningModule, Reference} from './src/references';
@@ -61,7 +61,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
['ɵɵinject', 'ɵɵinject'],
['ɵɵFactoryDeclaration', 'ɵɵFactoryDeclaration'],
['ɵsetClassMetadata', 'setClassMetadata'],
['ɵɵInjectableDef', 'ɵɵInjectableDef'],
['ɵɵInjectableDeclaration', 'ɵɵInjectableDeclaration'],
['ɵɵInjectorDeclaration', 'ɵɵInjectorDeclaration'],
['ɵɵNgModuleDeclaration', 'ɵɵNgModuleDeclaration'],
['ɵNgModuleFactory', 'NgModuleFactory'],

Some files were not shown because too many files have changed in this diff Show More