Merge remote-tracking branch 'origin/angular.ossez.com'
# Conflicts: # .husky/commit-msg # .husky/pre-commit # .husky/prepare-commit-msg # package.json
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
## PR Checklist
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Build related changes
|
||||
- [ ] CI related changes
|
||||
- [ ] Documentation content changes
|
||||
- [ ] angular.io application / infrastructure changes
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
||||
## Other information
|
||||
Generated
+22590
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -104,6 +104,7 @@
|
||||
"@angular/service-worker": "11.2.3",
|
||||
"@bazel/bazelisk": "^1.7.5",
|
||||
"@webcomponents/custom-elements": "1.2.1",
|
||||
"husky": "^6.0.0",
|
||||
"rxjs": "^6.5.3",
|
||||
"tslib": "^2.1.0",
|
||||
"zone.js": "~0.11.4"
|
||||
@@ -127,7 +128,7 @@
|
||||
"codelyzer": "^6.0.0",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-selector-parser": "^1.3.0",
|
||||
"dgeni": "^0.4.13",
|
||||
"dgeni": "^0.4.14",
|
||||
"dgeni-packages": "^0.28.4",
|
||||
"entities": "^1.1.1",
|
||||
"esbuild": "^0.9.0",
|
||||
@@ -154,9 +155,9 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"light-server": "^2.6.2",
|
||||
"lighthouse": "^7.2.0",
|
||||
"lighthouse": "^7.3.0",
|
||||
"lighthouse-logger": "^1.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lunr": "^2.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"protractor": "~7.0.0",
|
||||
|
||||
@@ -102,10 +102,10 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'."
|
||||
|
||||
|
||||
# Did it generate a base factory call for synthesized constructors correctly?
|
||||
grep "/\*@__PURE__\*/ function () { let ɵMatTable_BaseFactory; return function MatTable_Factory(t) { return (ɵMatTable_BaseFactory || (ɵMatTable_BaseFactory = ɵngcc0.ɵɵgetInheritedFactory(MatTable)))(t || MatTable); }; }();" node_modules/@angular/material/esm2015/table/table.js
|
||||
grep "const ɵMatTable_BaseFactory = /\*@__PURE__\*/ ɵngcc0.ɵɵgetInheritedFactory(MatTable);" node_modules/@angular/material/esm2015/table/table.js
|
||||
assertSucceeded "Expected 'ngcc' to generate a base factory for 'MatTable' in '@angular/material' (esm2015)."
|
||||
|
||||
grep "/\*@__PURE__\*/ function () { var ɵMatTable_BaseFactory; return function MatTable_Factory(t) { return (ɵMatTable_BaseFactory || (ɵMatTable_BaseFactory = ɵngcc0.ɵɵgetInheritedFactory(MatTable)))(t || MatTable); }; }();" node_modules/@angular/material/esm5/table/table.js
|
||||
grep "var ɵMatTable_BaseFactory = /\*@__PURE__\*/ ɵngcc0.ɵɵgetInheritedFactory(MatTable);" node_modules/@angular/material/esm5/table/table.js
|
||||
assertSucceeded "Expected 'ngcc' to generate a base factory for 'MatTable' in '@angular/material' (esm5)."
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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 {fakeAsync} from '@angular/core/testing';
|
||||
import {NoopAnimationPlayer} from '../src/animations';
|
||||
import {AnimationGroupPlayer} from '../src/players/animation_group_player';
|
||||
|
||||
|
||||
describe('AnimationGroupPlayer', () => {
|
||||
it('should getPosition of an empty group', fakeAsync(() => {
|
||||
const players: NoopAnimationPlayer[] = [];
|
||||
const groupPlayer = new AnimationGroupPlayer(players);
|
||||
expect(groupPlayer.getPosition()).toBe(0);
|
||||
}));
|
||||
|
||||
it('should getPosition of a single player in a group', fakeAsync(() => {
|
||||
const player = new NoopAnimationPlayer(5, 5);
|
||||
player.setPosition(0.2);
|
||||
const players = [player];
|
||||
const groupPlayer = new AnimationGroupPlayer(players);
|
||||
expect(groupPlayer.getPosition()).toBe(0.2);
|
||||
}));
|
||||
|
||||
it('should getPosition based on the longest player in the group', fakeAsync(() => {
|
||||
const longestPlayer = new NoopAnimationPlayer(5, 5);
|
||||
longestPlayer.setPosition(0.2);
|
||||
const players = [
|
||||
new NoopAnimationPlayer(1, 4),
|
||||
new NoopAnimationPlayer(4, 1),
|
||||
new NoopAnimationPlayer(7, 0),
|
||||
longestPlayer,
|
||||
new NoopAnimationPlayer(1, 1),
|
||||
];
|
||||
const groupPlayer = new AnimationGroupPlayer(players);
|
||||
expect(groupPlayer.getPosition()).toBe(0.2);
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
|
||||
load(":ng_module.bzl", "NgPerfInfo")
|
||||
|
||||
def _ng_perf_flag_impl(ctx):
|
||||
return NgPerfInfo(enable_perf_logging = ctx.build_setting_value)
|
||||
|
||||
# `ng_perf_flag` is a special `build_setting` rule which ultimately enables a command-line boolean
|
||||
# flag to control whether the `ng_module` rule produces performance tracing JSON files (in Ivy mode)
|
||||
# as declared outputs.
|
||||
#
|
||||
# It does this via the `NgPerfInfo` provider and the `perf_flag` attriubute on `ng_module`. For more
|
||||
# details, see: https://docs.bazel.build/versions/master/skylark/config.html
|
||||
ng_perf_flag = rule(
|
||||
implementation = _ng_perf_flag_impl,
|
||||
build_setting = config.bool(flag = True),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "ng_module_ivy_test_lib",
|
||||
testonly = True,
|
||||
srcs = ["ng_module_ivy_test.ts"],
|
||||
tags = ["ivy-only"],
|
||||
)
|
||||
|
||||
# `ng_module` with `compilation_mode` explicitly set to `partial`.
|
||||
ng_module(
|
||||
name = "test_module_partial_compilation",
|
||||
srcs = ["test_module_partial_compilation.ts"],
|
||||
compilation_mode = "partial",
|
||||
tags = ["ivy-only"],
|
||||
deps = ["//packages/core"],
|
||||
)
|
||||
|
||||
# `ng_module` with `compilation_mode` explicitly set to `full`.
|
||||
ng_module(
|
||||
name = "test_module_full_compilation",
|
||||
srcs = ["test_module_full_compilation.ts"],
|
||||
compilation_mode = "full",
|
||||
tags = ["ivy-only"],
|
||||
deps = ["//packages/core"],
|
||||
)
|
||||
|
||||
# `ng_module` with no specific `compilation_mode` attribute specified.
|
||||
ng_module(
|
||||
name = "test_module_default_compilation",
|
||||
srcs = ["test_module_default_compilation.ts"],
|
||||
tags = ["ivy-only"],
|
||||
deps = ["//packages/core"],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "ng_module_ivy_test",
|
||||
srcs = [":ng_module_ivy_test_lib"],
|
||||
data = [
|
||||
":test_module_default_compilation",
|
||||
":test_module_full_compilation",
|
||||
":test_module_partial_compilation",
|
||||
],
|
||||
tags = ["ivy-only"],
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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 {readFileSync} from 'fs';
|
||||
|
||||
/** Runfiles helper from bazel to resolve file name paths. */
|
||||
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']!);
|
||||
|
||||
describe('ng_module with ivy enabled', () => {
|
||||
describe('default compilation mode', () => {
|
||||
it('should generate definitions', () => {
|
||||
const outputFile = runfiles.resolveWorkspaceRelative(
|
||||
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_default_compilation.js');
|
||||
const fileContent = readFileSync(outputFile, 'utf8');
|
||||
expect(fileContent).toContain(`TestComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent(`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('full compilation mode', () => {
|
||||
it('should generate definitions', () => {
|
||||
const outputFile = runfiles.resolveWorkspaceRelative(
|
||||
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_full_compilation.js');
|
||||
const fileContent = readFileSync(outputFile, 'utf8');
|
||||
expect(fileContent).toContain(`TestComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent(`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('partial compilation mode', () => {
|
||||
it('should generate declarations', () => {
|
||||
const outputFile = runfiles.resolveWorkspaceRelative(
|
||||
'packages/bazel/test/ngc-wrapped/ivy_enabled/test_module_partial_compilation.js');
|
||||
const fileContent = readFileSync(outputFile, 'utf8');
|
||||
expect(fileContent).toContain(`TestComponent.ɵcmp = i0.ɵɵngDeclareComponent(`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: 'Hello',
|
||||
})
|
||||
export class TestComponent {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: 'Hello',
|
||||
})
|
||||
export class TestComponent {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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 {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: 'Hello',
|
||||
})
|
||||
export class TestComponent {
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* A token used to manipulate and access values stored in `HttpContext`.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class HttpContextToken<T> {
|
||||
constructor(public readonly defaultValue: () => T) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Http context stores arbitrary user defined values and ensures type safety without
|
||||
* actually knowing the types. It is backed by a `Map` and guarantees that keys do not clash.
|
||||
*
|
||||
* This context is mutable and is shared between cloned requests unless explicitly specified.
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* ### Usage Example
|
||||
*
|
||||
* ```typescript
|
||||
* // inside cache.interceptors.ts
|
||||
* export const IS_CACHE_ENABLED = new HttpContextToken<boolean>(() => false);
|
||||
*
|
||||
* export class CacheInterceptor implements HttpInterceptor {
|
||||
*
|
||||
* intercept(req: HttpRequest<any>, delegate: HttpHandler): Observable<HttpEvent<any>> {
|
||||
* if (req.context.get(IS_CACHE_ENABLED) === true) {
|
||||
* return ...;
|
||||
* }
|
||||
* return delegate.handle(req);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // inside a service
|
||||
*
|
||||
* this.httpClient.get('/api/weather', {
|
||||
* context: new HttpContext().set(IS_CACHE_ENABLED, true)
|
||||
* }).subscribe(...);
|
||||
* ```
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class HttpContext {
|
||||
private readonly map = new Map<HttpContextToken<unknown>, unknown>();
|
||||
|
||||
/**
|
||||
* Store a value in the context. If a value is already present it will be overwritten.
|
||||
*
|
||||
* @param token The reference to an instance of `HttpContextToken`.
|
||||
* @param value The value to store.
|
||||
*
|
||||
* @returns A reference to itself for easy chaining.
|
||||
*/
|
||||
set<T>(token: HttpContextToken<T>, value: T): HttpContext {
|
||||
this.map.set(token, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value associated with the given token.
|
||||
*
|
||||
* @param token The reference to an instance of `HttpContextToken`.
|
||||
*
|
||||
* @returns The stored value or default if one is defined.
|
||||
*/
|
||||
get<T>(token: HttpContextToken<T>): T {
|
||||
if (!this.map.has(token)) {
|
||||
this.map.set(token, token.defaultValue());
|
||||
}
|
||||
return this.map.get(token) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the value associated with the given token.
|
||||
*
|
||||
* @param token The reference to an instance of `HttpContextToken`.
|
||||
*
|
||||
* @returns A reference to itself for easy chaining.
|
||||
*/
|
||||
delete(token: HttpContextToken<unknown>): HttpContext {
|
||||
this.map.delete(token);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a list of tokens currently stored in the context.
|
||||
*/
|
||||
keys(): IterableIterator<HttpContextToken<unknown>> {
|
||||
return this.map.keys();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @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 {HttpContext, HttpContextToken} from '../src/context';
|
||||
|
||||
const IS_ENABLED = new HttpContextToken<boolean>(() => false);
|
||||
const CACHE_OPTION =
|
||||
new HttpContextToken<{cache: boolean, expiresIn?: number}>(() => ({cache: false}));
|
||||
|
||||
describe('HttpContext', () => {
|
||||
let context: HttpContext;
|
||||
|
||||
beforeEach(() => {
|
||||
context = new HttpContext();
|
||||
});
|
||||
|
||||
describe('with basic value', () => {
|
||||
it('should test public api', () => {
|
||||
expect(context.get(IS_ENABLED)).toBe(false);
|
||||
expect([...context.keys()]).toEqual([
|
||||
IS_ENABLED
|
||||
]); // value from factory function is stored in the map upon access
|
||||
|
||||
context.set(IS_ENABLED, true);
|
||||
expect(context.get(IS_ENABLED)).toBe(true);
|
||||
expect([...context.keys()]).toEqual([IS_ENABLED]);
|
||||
|
||||
context.delete(IS_ENABLED);
|
||||
expect([...context.keys()]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with complex value', () => {
|
||||
it('should test public api', () => {
|
||||
expect(context.get(CACHE_OPTION)).toEqual({cache: false});
|
||||
expect([...context.keys()]).toEqual([CACHE_OPTION]);
|
||||
|
||||
const value = {cache: true, expiresIn: 30};
|
||||
context.set(CACHE_OPTION, value);
|
||||
expect(context.get(CACHE_OPTION)).toBe(value);
|
||||
expect([...context.keys()]).toEqual([CACHE_OPTION]);
|
||||
|
||||
context.delete(CACHE_OPTION);
|
||||
expect([...context.keys()]).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ensure that same reference is returned for default value between multiple accesses',
|
||||
() => {
|
||||
const value = context.get(CACHE_OPTION); // will get default value
|
||||
expect(value).toEqual({cache: false});
|
||||
expect(context.get(CACHE_OPTION)).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper around the `XMLHttpRequest` constructor.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export abstract class XhrFactory {
|
||||
abstract build(): XMLHttpRequest;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @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 {LinkerOptions} from '../..';
|
||||
import {ReadonlyFileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../../../src/ngtsc/logging';
|
||||
|
||||
export interface LinkerPluginOptions extends Partial<LinkerOptions> {
|
||||
/**
|
||||
* File-system, used to load up the input source-map and content.
|
||||
*/
|
||||
fileSystem: ReadonlyFileSystem;
|
||||
|
||||
/**
|
||||
* Logger used by the linker.
|
||||
*/
|
||||
logger: Logger;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @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 {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {SourceFile, SourceFileLoader} from '../../../src/ngtsc/sourcemaps';
|
||||
|
||||
/**
|
||||
* A function that will return a `SourceFile` object (or null) for the current file being linked.
|
||||
*/
|
||||
export type GetSourceFileFn = () => SourceFile|null;
|
||||
|
||||
/**
|
||||
* Create a `GetSourceFileFn` that will return the `SourceFile` being linked or `null`, if not
|
||||
* available.
|
||||
*/
|
||||
export function createGetSourceFile(
|
||||
sourceUrl: AbsoluteFsPath, code: string, loader: SourceFileLoader|null): GetSourceFileFn {
|
||||
if (loader === null) {
|
||||
// No source-mapping so just return a function that always returns `null`.
|
||||
return () => null;
|
||||
} else {
|
||||
// Source-mapping is available so return a function that will load (and cache) the `SourceFile`.
|
||||
let sourceFile: SourceFile|null|undefined = undefined;
|
||||
return () => {
|
||||
if (sourceFile === undefined) {
|
||||
sourceFile = loader.loadSourceFile(sourceUrl, code);
|
||||
}
|
||||
return sourceFile;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @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 {declarationFunctions} from './partial_linkers/partial_linker_selector';
|
||||
|
||||
/**
|
||||
* Determines if the provided source file may need to be processed by the linker, i.e. whether it
|
||||
* potentially contains any declarations. If true is returned, then the source file should be
|
||||
* processed by the linker as it may contain declarations that need to be fully compiled. If false
|
||||
* is returned, parsing and processing of the source file can safely be skipped to improve
|
||||
* performance.
|
||||
*
|
||||
* This function may return true even for source files that don't actually contain any declarations
|
||||
* that need to be compiled.
|
||||
*
|
||||
* @param path the absolute path of the source file for which to determine whether linking may be
|
||||
* needed.
|
||||
* @param source the source file content as a string.
|
||||
* @returns whether the source file may contain declarations that need to be linked.
|
||||
*/
|
||||
export function needsLinking(path: string, source: string): boolean {
|
||||
return declarationFunctions.some(fn => source.includes(fn));
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @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 {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareDependencyMetadata, 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';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `ɵɵngDeclareFactory()` call expressions.
|
||||
*/
|
||||
export class PartialFactoryLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
|
||||
linkPartialDeclaration(
|
||||
constantPool: ConstantPool,
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3FactoryMeta(metaObj);
|
||||
const def = compileFactoryFunction(meta);
|
||||
return def.expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the `R3FactoryMetadata` structure from the AST object.
|
||||
*/
|
||||
export function toR3FactoryMeta<TExpression>(
|
||||
metaObj: AstObject<R3DeclareFactoryMetadata, TExpression>): R3FactoryMetadata {
|
||||
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');
|
||||
}
|
||||
|
||||
return {
|
||||
name: typeName,
|
||||
type: wrapReference(typeExpr.getOpaque()),
|
||||
internalType: metaObj.getOpaque('type'),
|
||||
typeArgumentCount: 0,
|
||||
target: parseEnum(metaObj.getValue('target'), FactoryTarget),
|
||||
deps: getDeps(metaObj, 'deps'),
|
||||
};
|
||||
}
|
||||
|
||||
function getDeps<TExpression>(
|
||||
metaObj: AstObject<R3DeclareFactoryMetadata, TExpression>,
|
||||
propName: keyof R3DeclareFactoryMetadata): R3DependencyMetadata[]|null|'invalid' {
|
||||
if (!metaObj.has(propName)) {
|
||||
return null;
|
||||
}
|
||||
const deps = metaObj.getValue(propName);
|
||||
if (deps.isArray()) {
|
||||
return deps.getArray().map(dep => getDep(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;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @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 {compileInjector, ConstantPool, R3DeclareInjectorMetadata, R3InjectorMetadata, 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 {wrapReference} from './util';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `ɵɵngDeclareInjector()` call expressions.
|
||||
*/
|
||||
export class PartialInjectorLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
|
||||
linkPartialDeclaration(
|
||||
constantPool: ConstantPool,
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3InjectorMeta(metaObj);
|
||||
const def = compileInjector(meta);
|
||||
return def.expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the `R3InjectorMetadata` structure from the AST object.
|
||||
*/
|
||||
export function toR3InjectorMeta<TExpression>(
|
||||
metaObj: AstObject<R3DeclareInjectorMetadata, TExpression>): R3InjectorMetadata {
|
||||
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');
|
||||
}
|
||||
|
||||
return {
|
||||
name: typeName,
|
||||
type: wrapReference(typeExpr.getOpaque()),
|
||||
internalType: metaObj.getOpaque('type'),
|
||||
providers: metaObj.has('providers') ? metaObj.getOpaque('providers') : null,
|
||||
imports: metaObj.has('imports') ? metaObj.getArray('imports').map(i => i.getOpaque()) : [],
|
||||
};
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @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 {compileNgModule, ConstantPool, R3DeclareNgModuleMetadata, R3NgModuleMetadata, R3PartialDeclaration, R3Reference} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
|
||||
import {AstObject, AstValue} from '../../ast/ast_value';
|
||||
|
||||
import {PartialLinker} from './partial_linker';
|
||||
import {wrapReference} from './util';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `ɵɵngDeclareNgModule()` call expressions.
|
||||
*/
|
||||
export class PartialNgModuleLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
|
||||
constructor(
|
||||
/**
|
||||
* If true then emit the additional declarations, imports, exports, etc in the NgModule
|
||||
* definition. These are only used by JIT compilation.
|
||||
*/
|
||||
private emitInline: boolean) {}
|
||||
|
||||
linkPartialDeclaration(
|
||||
constantPool: ConstantPool,
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3NgModuleMeta(metaObj, this.emitInline);
|
||||
const def = compileNgModule(meta);
|
||||
return def.expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the `R3NgModuleMetadata` structure from the AST object.
|
||||
*/
|
||||
export function toR3NgModuleMeta<TExpression>(
|
||||
metaObj: AstObject<R3DeclareNgModuleMetadata, TExpression>,
|
||||
emitInline: boolean): R3NgModuleMetadata {
|
||||
const wrappedType = metaObj.getOpaque('type');
|
||||
|
||||
const meta: R3NgModuleMetadata = {
|
||||
type: wrapReference(wrappedType),
|
||||
internalType: wrappedType,
|
||||
adjacentType: wrappedType,
|
||||
bootstrap: [],
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
emitInline,
|
||||
containsForwardDecls: false,
|
||||
schemas: [],
|
||||
id: metaObj.has('id') ? metaObj.getOpaque('id') : null,
|
||||
};
|
||||
|
||||
// Each of `bootstrap`, `declarations`, `imports` and `exports` are normally an array. But if any
|
||||
// of the references are not yet declared, then the arrays must be wrapped in a function to
|
||||
// prevent errors at runtime when accessing the values.
|
||||
|
||||
// The following blocks of code will unwrap the arrays from such functions, because
|
||||
// `R3NgModuleMetadata` expects arrays of `R3Reference` objects.
|
||||
|
||||
// Further, since the `ɵɵdefineNgModule()` will also suffer from the forward declaration problem,
|
||||
// we must update the `containsForwardDecls` property if a function wrapper was found.
|
||||
|
||||
if (metaObj.has('bootstrap')) {
|
||||
const bootstrap: AstValue<unknown, TExpression> = metaObj.getValue('bootstrap');
|
||||
if (bootstrap.isFunction()) {
|
||||
meta.containsForwardDecls = true;
|
||||
meta.bootstrap = wrapReferences(unwrapForwardRefs(bootstrap));
|
||||
} else
|
||||
meta.bootstrap = wrapReferences(bootstrap);
|
||||
}
|
||||
|
||||
if (metaObj.has('declarations')) {
|
||||
const declarations: AstValue<unknown, TExpression> = metaObj.getValue('declarations');
|
||||
if (declarations.isFunction()) {
|
||||
meta.containsForwardDecls = true;
|
||||
meta.declarations = wrapReferences(unwrapForwardRefs(declarations));
|
||||
} else
|
||||
meta.declarations = wrapReferences(declarations);
|
||||
}
|
||||
|
||||
if (metaObj.has('imports')) {
|
||||
const imports: AstValue<unknown, TExpression> = metaObj.getValue('imports');
|
||||
if (imports.isFunction()) {
|
||||
meta.containsForwardDecls = true;
|
||||
meta.imports = wrapReferences(unwrapForwardRefs(imports));
|
||||
} else
|
||||
meta.imports = wrapReferences(imports);
|
||||
}
|
||||
|
||||
if (metaObj.has('exports')) {
|
||||
const exports: AstValue<unknown, TExpression> = metaObj.getValue('exports');
|
||||
if (exports.isFunction()) {
|
||||
meta.containsForwardDecls = true;
|
||||
meta.exports = wrapReferences(unwrapForwardRefs(exports));
|
||||
} else
|
||||
meta.exports = wrapReferences(exports);
|
||||
}
|
||||
|
||||
if (metaObj.has('schemas')) {
|
||||
const schemas: AstValue<unknown, TExpression> = metaObj.getValue('schemas');
|
||||
meta.schemas = wrapReferences(schemas);
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an array from the body of the function.
|
||||
*
|
||||
* If `field` is `function() { return [exp1, exp2, exp3]; }` then we return `[exp1, exp2, exp3]`.
|
||||
*
|
||||
*/
|
||||
function unwrapForwardRefs<TExpression>(field: AstValue<unknown, TExpression>):
|
||||
AstValue<TExpression[], TExpression> {
|
||||
return (field as AstValue<Function, TExpression>).getFunctionReturnValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the array of expressions into an array of R3 references.
|
||||
*/
|
||||
function wrapReferences<TExpression>(values: AstValue<TExpression[], TExpression>): R3Reference[] {
|
||||
return values.getArray().map(i => wrapReference(i.getOpaque()));
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @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 {compilePipeFromMetadata, ConstantPool, R3DeclarePipeMetadata, R3PartialDeclaration, R3PipeMetadata} 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 {wrapReference} from './util';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `ɵɵngDeclarePipe()` call expressions.
|
||||
*/
|
||||
export class PartialPipeLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
|
||||
constructor() {}
|
||||
|
||||
linkPartialDeclaration(
|
||||
constantPool: ConstantPool,
|
||||
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
|
||||
const meta = toR3PipeMeta(metaObj);
|
||||
const def = compilePipeFromMetadata(meta);
|
||||
return def.expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the `R3PipeMetadata` structure from the AST object.
|
||||
*/
|
||||
export function toR3PipeMeta<TExpression>(metaObj: AstObject<R3DeclarePipeMetadata, TExpression>):
|
||||
R3PipeMetadata {
|
||||
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 pure = metaObj.has('pure') ? metaObj.getBoolean('pure') : true;
|
||||
|
||||
return {
|
||||
name: typeName,
|
||||
type: wrapReference(typeExpr.getOpaque()),
|
||||
internalType: metaObj.getOpaque('type'),
|
||||
typeArgumentCount: 0,
|
||||
deps: null,
|
||||
pipeName: metaObj.getString('name'),
|
||||
pure,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @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 {R3Reference} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import {AstValue} from '../../ast/ast_value';
|
||||
import {FatalLinkerError} from '../../fatal_linker_error';
|
||||
|
||||
export function wrapReference<TExpression>(wrapped: o.WrappedNodeExpr<TExpression>): R3Reference {
|
||||
return {value: wrapped, type: wrapped};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the value of an enum from the AST value's symbol name.
|
||||
*/
|
||||
export function parseEnum<TExpression, TEnum>(
|
||||
value: AstValue<unknown, TExpression>, Enum: TEnum): TEnum[keyof TEnum] {
|
||||
const symbolName = value.getSymbolName();
|
||||
if (symbolName === null) {
|
||||
throw new FatalLinkerError(value.expression, 'Expected value to have a symbol name');
|
||||
}
|
||||
const enumValue = Enum[symbolName as keyof typeof Enum];
|
||||
if (enumValue === undefined) {
|
||||
throw new FatalLinkerError(value.expression, `Unsupported enum value for ${Enum}`);
|
||||
}
|
||||
return enumValue;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @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 {needsLinking} from '../../src/file_linker/needs_linking';
|
||||
|
||||
describe('needsLinking', () => {
|
||||
it('should return true for directive declarations', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
export class Dir {
|
||||
ɵdir = ɵɵngDeclareDirective({type: Dir});
|
||||
}
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return true for namespaced directive declarations', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
export class Dir {
|
||||
ɵdir = ng.ɵɵngDeclareDirective({type: Dir});
|
||||
}
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return true for unrelated usages of ɵɵngDeclareDirective', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
const fnName = 'ɵɵngDeclareDirective';
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when the file does not contain ɵɵngDeclareDirective', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
const foo = ngDeclareDirective;
|
||||
`)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should return true for component declarations', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
export class Cmp {
|
||||
ɵdir = ɵɵngDeclareComponent({type: Cmp});
|
||||
}
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return true for namespaced component declarations', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
export class Cmp {
|
||||
ɵdir = ng.ɵɵngDeclareComponent({type: Cmp});
|
||||
}
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return true for unrelated usages of ɵɵngDeclareComponent', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
const fnName = 'ɵɵngDeclareComponent';
|
||||
`)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when the file does not contain ɵɵngDeclareComponent', () => {
|
||||
expect(needsLinking('file.js', `
|
||||
const foo = ngDeclareComponent;
|
||||
`)).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This file exists as a target for g3 patches which change the Angular compiler's behavior.
|
||||
// Separating the patched code in a separate file eliminates the possibility of conflicts with the
|
||||
// patch diffs when making changes to the rest of the compiler codebase.
|
||||
|
||||
// In ngtsc we no longer want to compile undecorated classes with Angular features.
|
||||
// Migrations for these patterns ran as part of `ng update` and we want to ensure
|
||||
// that projects do not regress. See https://hackmd.io/@alx/ryfYYuvzH for more details.
|
||||
export const compileUndecoratedClassesWithAngularFeatures = false;
|
||||
@@ -0,0 +1,16 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_library(
|
||||
name = "semantic_graph",
|
||||
srcs = ["index.ts"] + glob([
|
||||
"src/**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export {SemanticReference, SemanticSymbol} from './src/api';
|
||||
export {SemanticDepGraph, SemanticDepGraphUpdater} from './src/graph';
|
||||
export {areTypeParametersEqual, extractSemanticTypeParameters, SemanticTypeParameter} from './src/type_parameters';
|
||||
export {isArrayEqual, isReferenceEqual, isSetEqual, isSymbolEqual} from './src/util';
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../file_system';
|
||||
import {ClassDeclaration} from '../../../reflection';
|
||||
|
||||
/**
|
||||
* Represents a symbol that is recognizable across incremental rebuilds, which enables the captured
|
||||
* metadata to be compared to the prior compilation. This allows for semantic understanding of
|
||||
* the changes that have been made in a rebuild, which potentially enables more reuse of work
|
||||
* from the prior compilation.
|
||||
*/
|
||||
export abstract class SemanticSymbol {
|
||||
/**
|
||||
* The path of the file that declares this symbol.
|
||||
*/
|
||||
public readonly path: AbsoluteFsPath;
|
||||
|
||||
/**
|
||||
* The identifier of this symbol, or null if no identifier could be determined. It should
|
||||
* uniquely identify the symbol relative to `file`. This is typically just the name of a
|
||||
* top-level class declaration, as that uniquely identifies the class within the file.
|
||||
*
|
||||
* If the identifier is null, then this symbol cannot be recognized across rebuilds. In that
|
||||
* case, the symbol is always assumed to have semantically changed to guarantee a proper
|
||||
* rebuild.
|
||||
*/
|
||||
public readonly identifier: string|null;
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The declaration for this symbol.
|
||||
*/
|
||||
public readonly decl: ClassDeclaration,
|
||||
) {
|
||||
this.path = absoluteFromSourceFile(decl.getSourceFile());
|
||||
this.identifier = getSymbolIdentifier(decl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the symbol to be compared to the equivalent symbol in the previous compilation. The
|
||||
* return value indicates whether the symbol has been changed in a way such that its public API
|
||||
* is affected.
|
||||
*
|
||||
* This method determines whether a change to _this_ symbol require the symbols that
|
||||
* use to this symbol to be re-emitted.
|
||||
*
|
||||
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
|
||||
* failed compilations are never provided.
|
||||
*
|
||||
* @param previousSymbol The symbol from a prior compilation.
|
||||
*/
|
||||
abstract isPublicApiAffected(previousSymbol: SemanticSymbol): boolean;
|
||||
|
||||
/**
|
||||
* Allows the symbol to determine whether its emit is affected. The equivalent symbol from a prior
|
||||
* build is given, in addition to the set of symbols of which the public API has changed.
|
||||
*
|
||||
* This method determines whether a change to _other_ symbols, i.e. those present in
|
||||
* `publicApiAffected`, should cause _this_ symbol to be re-emitted.
|
||||
*
|
||||
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
|
||||
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
|
||||
* @param publicApiAffected The set of symbols of which the public API has changed.
|
||||
*/
|
||||
isEmitAffected?(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>): boolean;
|
||||
|
||||
/**
|
||||
* Similar to `isPublicApiAffected`, but here equivalent symbol from a prior compilation needs
|
||||
* to be compared to see if the type-check block of components that use this symbol is affected.
|
||||
*
|
||||
* This method determines whether a change to _this_ symbol require the symbols that
|
||||
* use to this symbol to have their type-check block regenerated.
|
||||
*
|
||||
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
|
||||
* failed compilations are never provided.
|
||||
*
|
||||
* @param previousSymbol The symbol from a prior compilation.
|
||||
*/
|
||||
abstract isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean;
|
||||
|
||||
/**
|
||||
* Similar to `isEmitAffected`, but focused on the type-check block of this symbol. This method
|
||||
* determines whether a change to _other_ symbols, i.e. those present in `typeCheckApiAffected`,
|
||||
* should cause _this_ symbol's type-check block to be regenerated.
|
||||
*
|
||||
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
|
||||
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
|
||||
* @param typeCheckApiAffected The set of symbols of which the type-check API has changed.
|
||||
*/
|
||||
isTypeCheckBlockAffected?
|
||||
(previousSymbol: SemanticSymbol, typeCheckApiAffected: Set<SemanticSymbol>): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a reference to a semantic symbol that has been emitted into a source file. The
|
||||
* reference may refer to the symbol using a different name than the semantic symbol's declared
|
||||
* name, e.g. in case a re-export under a different name was chosen by a reference emitter.
|
||||
* Consequently, to know that an emitted reference is still valid not only requires that the
|
||||
* semantic symbol is still valid, but also that the path by which the symbol is imported has not
|
||||
* changed.
|
||||
*/
|
||||
export interface SemanticReference {
|
||||
symbol: SemanticSymbol;
|
||||
|
||||
/**
|
||||
* The path by which the symbol has been referenced.
|
||||
*/
|
||||
importPath: string|null;
|
||||
}
|
||||
|
||||
function getSymbolIdentifier(decl: ClassDeclaration): string|null {
|
||||
if (!ts.isSourceFile(decl.parent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If this is a top-level class declaration, the class name is used as unique identifier.
|
||||
// Other scenarios are currently not supported and causes the symbol not to be identified
|
||||
// across rebuilds, unless the declaration node has not changed.
|
||||
return decl.name.text;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* @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 {Expression, ExternalExpr} from '@angular/compiler';
|
||||
import {AbsoluteFsPath} from '../../../file_system';
|
||||
import {ClassDeclaration} from '../../../reflection';
|
||||
import {SemanticReference, SemanticSymbol} from './api';
|
||||
|
||||
export interface SemanticDependencyResult {
|
||||
/**
|
||||
* The files that need to be re-emitted.
|
||||
*/
|
||||
needsEmit: Set<AbsoluteFsPath>;
|
||||
|
||||
/**
|
||||
* The files for which the type-check block should be regenerated.
|
||||
*/
|
||||
needsTypeCheckEmit: Set<AbsoluteFsPath>;
|
||||
|
||||
/**
|
||||
* The newly built graph that represents the current compilation.
|
||||
*/
|
||||
newGraph: SemanticDepGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a declaration for which no semantic symbol has been registered. For example,
|
||||
* declarations from external dependencies have not been explicitly registered and are represented
|
||||
* by this symbol. This allows the unresolved symbol to still be compared to a symbol from a prior
|
||||
* compilation.
|
||||
*/
|
||||
class OpaqueSymbol extends SemanticSymbol {
|
||||
isPublicApiAffected(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
isTypeCheckApiAffected(): false {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The semantic dependency graph of a single compilation.
|
||||
*/
|
||||
export class SemanticDepGraph {
|
||||
readonly files = new Map<AbsoluteFsPath, Map<string, SemanticSymbol>>();
|
||||
readonly symbolByDecl = new Map<ClassDeclaration, SemanticSymbol>();
|
||||
|
||||
/**
|
||||
* Registers a symbol in the graph. The symbol is given a unique identifier if possible, such that
|
||||
* its equivalent symbol can be obtained from a prior graph even if its declaration node has
|
||||
* changed across rebuilds. Symbols without an identifier are only able to find themselves in a
|
||||
* prior graph if their declaration node is identical.
|
||||
*/
|
||||
registerSymbol(symbol: SemanticSymbol): void {
|
||||
this.symbolByDecl.set(symbol.decl, symbol);
|
||||
|
||||
if (symbol.identifier !== null) {
|
||||
// If the symbol has a unique identifier, record it in the file that declares it. This enables
|
||||
// the symbol to be requested by its unique name.
|
||||
if (!this.files.has(symbol.path)) {
|
||||
this.files.set(symbol.path, new Map<string, SemanticSymbol>());
|
||||
}
|
||||
this.files.get(symbol.path)!.set(symbol.identifier, symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resolve a symbol in this graph that represents the given symbol from another graph.
|
||||
* If no matching symbol could be found, null is returned.
|
||||
*
|
||||
* @param symbol The symbol from another graph for which its equivalent in this graph should be
|
||||
* found.
|
||||
*/
|
||||
getEquivalentSymbol(symbol: SemanticSymbol): SemanticSymbol|null {
|
||||
// First lookup the symbol by its declaration. It is typical for the declaration to not have
|
||||
// changed across rebuilds, so this is likely to find the symbol. Using the declaration also
|
||||
// allows to diff symbols for which no unique identifier could be determined.
|
||||
let previousSymbol = this.getSymbolByDecl(symbol.decl);
|
||||
if (previousSymbol === null && symbol.identifier !== null) {
|
||||
// The declaration could not be resolved to a symbol in a prior compilation, which may
|
||||
// happen because the file containing the declaration has changed. In that case we want to
|
||||
// lookup the symbol based on its unique identifier, as that allows us to still compare the
|
||||
// changed declaration to the prior compilation.
|
||||
previousSymbol = this.getSymbolByName(symbol.path, symbol.identifier);
|
||||
}
|
||||
|
||||
return previousSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find the symbol by its identifier.
|
||||
*/
|
||||
private getSymbolByName(path: AbsoluteFsPath, identifier: string): SemanticSymbol|null {
|
||||
if (!this.files.has(path)) {
|
||||
return null;
|
||||
}
|
||||
const file = this.files.get(path)!;
|
||||
if (!file.has(identifier)) {
|
||||
return null;
|
||||
}
|
||||
return file.get(identifier)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resolve the declaration to its semantic symbol.
|
||||
*/
|
||||
getSymbolByDecl(decl: ClassDeclaration): SemanticSymbol|null {
|
||||
if (!this.symbolByDecl.has(decl)) {
|
||||
return null;
|
||||
}
|
||||
return this.symbolByDecl.get(decl)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic to go from a previous dependency graph to a new one, along with information
|
||||
* on which files have been affected.
|
||||
*/
|
||||
export class SemanticDepGraphUpdater {
|
||||
private readonly newGraph = new SemanticDepGraph();
|
||||
|
||||
/**
|
||||
* Contains opaque symbols that were created for declarations for which there was no symbol
|
||||
* registered, which happens for e.g. external declarations.
|
||||
*/
|
||||
private readonly opaqueSymbols = new Map<ClassDeclaration, OpaqueSymbol>();
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The semantic dependency graph of the most recently succeeded compilation, or null if this
|
||||
* is the initial build.
|
||||
*/
|
||||
private priorGraph: SemanticDepGraph|null) {}
|
||||
|
||||
/**
|
||||
* Registers the symbol in the new graph that is being created.
|
||||
*/
|
||||
registerSymbol(symbol: SemanticSymbol): void {
|
||||
this.newGraph.registerSymbol(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes all facts that have been gathered to create a new semantic dependency graph. In this
|
||||
* process, the semantic impact of the changes is determined which results in a set of files that
|
||||
* need to be emitted and/or type-checked.
|
||||
*/
|
||||
finalize(): SemanticDependencyResult {
|
||||
if (this.priorGraph === null) {
|
||||
// If no prior dependency graph is available then this was the initial build, in which case
|
||||
// we don't need to determine the semantic impact as everything is already considered
|
||||
// logically changed.
|
||||
return {
|
||||
needsEmit: new Set<AbsoluteFsPath>(),
|
||||
needsTypeCheckEmit: new Set<AbsoluteFsPath>(),
|
||||
newGraph: this.newGraph,
|
||||
};
|
||||
}
|
||||
|
||||
const needsEmit = this.determineInvalidatedFiles(this.priorGraph);
|
||||
const needsTypeCheckEmit = this.determineInvalidatedTypeCheckFiles(this.priorGraph);
|
||||
return {
|
||||
needsEmit,
|
||||
needsTypeCheckEmit,
|
||||
newGraph: this.newGraph,
|
||||
};
|
||||
}
|
||||
|
||||
private determineInvalidatedFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
|
||||
const isPublicApiAffected = new Set<SemanticSymbol>();
|
||||
|
||||
// The first phase is to collect all symbols which have their public API affected. Any symbols
|
||||
// that cannot be matched up with a symbol from the prior graph are considered affected.
|
||||
for (const symbol of this.newGraph.symbolByDecl.values()) {
|
||||
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
|
||||
if (previousSymbol === null || symbol.isPublicApiAffected(previousSymbol)) {
|
||||
isPublicApiAffected.add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// The second phase is to find all symbols for which the emit result is affected, either because
|
||||
// their used declarations have changed or any of those used declarations has had its public API
|
||||
// affected as determined in the first phase.
|
||||
const needsEmit = new Set<AbsoluteFsPath>();
|
||||
for (const symbol of this.newGraph.symbolByDecl.values()) {
|
||||
if (symbol.isEmitAffected === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
|
||||
if (previousSymbol === null || symbol.isEmitAffected(previousSymbol, isPublicApiAffected)) {
|
||||
needsEmit.add(symbol.path);
|
||||
}
|
||||
}
|
||||
|
||||
return needsEmit;
|
||||
}
|
||||
|
||||
private determineInvalidatedTypeCheckFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
|
||||
const isTypeCheckApiAffected = new Set<SemanticSymbol>();
|
||||
|
||||
// The first phase is to collect all symbols which have their public API affected. Any symbols
|
||||
// that cannot be matched up with a symbol from the prior graph are considered affected.
|
||||
for (const symbol of this.newGraph.symbolByDecl.values()) {
|
||||
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
|
||||
if (previousSymbol === null || symbol.isTypeCheckApiAffected(previousSymbol)) {
|
||||
isTypeCheckApiAffected.add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// The second phase is to find all symbols for which the emit result is affected, either because
|
||||
// their used declarations have changed or any of those used declarations has had its public API
|
||||
// affected as determined in the first phase.
|
||||
const needsTypeCheckEmit = new Set<AbsoluteFsPath>();
|
||||
for (const symbol of this.newGraph.symbolByDecl.values()) {
|
||||
if (symbol.isTypeCheckBlockAffected === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
|
||||
if (previousSymbol === null ||
|
||||
symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected)) {
|
||||
needsTypeCheckEmit.add(symbol.path);
|
||||
}
|
||||
}
|
||||
|
||||
return needsTypeCheckEmit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `SemanticReference` for the reference to `decl` using the expression `expr`. See
|
||||
* the documentation of `SemanticReference` for details.
|
||||
*/
|
||||
getSemanticReference(decl: ClassDeclaration, expr: Expression): SemanticReference {
|
||||
return {
|
||||
symbol: this.getSymbol(decl),
|
||||
importPath: getImportPath(expr),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `SemanticSymbol` that was registered for `decl` during the current compilation, or
|
||||
* returns an opaque symbol that represents `decl`.
|
||||
*/
|
||||
getSymbol(decl: ClassDeclaration): SemanticSymbol {
|
||||
const symbol = this.newGraph.getSymbolByDecl(decl);
|
||||
if (symbol === null) {
|
||||
// No symbol has been recorded for the provided declaration, which would be the case if the
|
||||
// declaration is external. Return an opaque symbol in that case, to allow the external
|
||||
// declaration to be compared to a prior compilation.
|
||||
return this.getOpaqueSymbol(decl);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates an `OpaqueSymbol` for the provided class declaration.
|
||||
*/
|
||||
private getOpaqueSymbol(decl: ClassDeclaration): OpaqueSymbol {
|
||||
if (this.opaqueSymbols.has(decl)) {
|
||||
return this.opaqueSymbols.get(decl)!;
|
||||
}
|
||||
|
||||
const symbol = new OpaqueSymbol(decl);
|
||||
this.opaqueSymbols.set(decl, symbol);
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
function getImportPath(expr: Expression): string|null {
|
||||
if (expr instanceof ExternalExpr) {
|
||||
return `${expr.value.moduleName}\$${expr.value.name}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration} from '../../../reflection';
|
||||
import {isArrayEqual} from './util';
|
||||
|
||||
/**
|
||||
* Describes a generic type parameter of a semantic symbol. A class declaration with type parameters
|
||||
* needs special consideration in certain contexts. For example, template type-check blocks may
|
||||
* contain type constructors of used directives which include the type parameters of the directive.
|
||||
* As a consequence, if a change is made that affects the type parameters of said directive, any
|
||||
* template type-check blocks that use the directive need to be regenerated.
|
||||
*
|
||||
* This type represents a single generic type parameter. It currently only tracks whether the
|
||||
* type parameter has a constraint, i.e. has an `extends` clause. When a constraint is present, we
|
||||
* currently assume that the type parameter is affected in each incremental rebuild; proving that
|
||||
* a type parameter with constraint is not affected is non-trivial as it requires full semantic
|
||||
* understanding of the type constraint.
|
||||
*/
|
||||
export interface SemanticTypeParameter {
|
||||
/**
|
||||
* Whether a type constraint, i.e. an `extends` clause is present on the type parameter.
|
||||
*/
|
||||
hasGenericTypeBound: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the type parameters of the given class into their semantic representation. If the class
|
||||
* does not have any type parameters, then `null` is returned.
|
||||
*/
|
||||
export function extractSemanticTypeParameters(node: ClassDeclaration): SemanticTypeParameter[]|
|
||||
null {
|
||||
if (!ts.isClassDeclaration(node) || node.typeParameters === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node.typeParameters.map(
|
||||
typeParam => ({hasGenericTypeBound: typeParam.constraint !== undefined}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the list of type parameters to determine if they can be considered equal.
|
||||
*/
|
||||
export function areTypeParametersEqual(
|
||||
current: SemanticTypeParameter[]|null, previous: SemanticTypeParameter[]|null): boolean {
|
||||
// First compare all type parameters one-to-one; any differences mean that the list of type
|
||||
// parameters has changed.
|
||||
if (!isArrayEqual(current, previous, isTypeParameterEqual)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is a current list of type parameters and if any of them has a generic type constraint,
|
||||
// then the meaning of that type parameter may have changed without us being aware; as such we
|
||||
// have to assume that the type parameters have in fact changed.
|
||||
if (current !== null && current.some(typeParam => typeParam.hasGenericTypeBound)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTypeParameterEqual(a: SemanticTypeParameter, b: SemanticTypeParameter): boolean {
|
||||
return a.hasGenericTypeBound === b.hasGenericTypeBound;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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 {SemanticReference, SemanticSymbol} from './api';
|
||||
|
||||
/**
|
||||
* Determines whether the provided symbols represent the same declaration.
|
||||
*/
|
||||
export function isSymbolEqual(a: SemanticSymbol, b: SemanticSymbol): boolean {
|
||||
if (a.decl === b.decl) {
|
||||
// If the declaration is identical then it must represent the same symbol.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.identifier === null || b.identifier === null) {
|
||||
// Unidentifiable symbols are assumed to be different.
|
||||
return false;
|
||||
}
|
||||
|
||||
return a.path === b.path && a.identifier === b.identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the provided references to a semantic symbol are still equal, i.e. represent
|
||||
* the same symbol and are imported by the same path.
|
||||
*/
|
||||
export function isReferenceEqual(a: SemanticReference, b: SemanticReference): boolean {
|
||||
if (!isSymbolEqual(a.symbol, b.symbol)) {
|
||||
// If the reference's target symbols are different, the reference itself is different.
|
||||
return false;
|
||||
}
|
||||
|
||||
// The reference still corresponds with the same symbol, now check that the path by which it is
|
||||
// imported has not changed.
|
||||
return a.importPath === b.importPath;
|
||||
}
|
||||
|
||||
export function referenceEquality<T>(a: T, b: T): boolean {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided arrays are equal to each other, using the provided equality tester
|
||||
* that is called for all entries in the array.
|
||||
*/
|
||||
export function isArrayEqual<T>(
|
||||
a: readonly T[]|null, b: readonly T[]|null,
|
||||
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
|
||||
if (a === null || b === null) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !a.some((item, index) => !equalityTester(item, b[index]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided sets are equal to each other, using the provided equality tester.
|
||||
* Sets that only differ in ordering are considered equal.
|
||||
*/
|
||||
export function isSetEqual<T>(
|
||||
a: ReadonlySet<T>|null, b: ReadonlySet<T>|null,
|
||||
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
|
||||
if (a === null || b === null) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const itemA of a) {
|
||||
let found = false;
|
||||
for (const itemB of b) {
|
||||
if (equalityTester(itemA, itemB)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user