提交代码进行测试

This commit is contained in:
YuCheng Hu 2021-04-05 12:11:12 -04:00
parent 03cab937c1
commit ea5de990f7
851 changed files with 90585 additions and 36 deletions

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn -s ng-dev commit-message pre-commit-validate --file $1;

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn -s ng-dev format staged;

View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn -s ng-dev commit-message restore-commit-message-draft $1 $2;

22590
aio/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,13 @@
# CliElementsUniversal
This project tests the integration of Angular Elements (`@angular/elements`) with SSR (via `@angular/platform-server`).
The project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.1.4.
Support for Angular Elements was added with `ng add @angular/elements` and for SSR with `ng generate app-shell`.
What this project tests is that an app can be successfully SSR'd even when it uses `@angular/elements`, which relies on certain DOM built-ins being available as soon as it is imported.
This is tested by generating the [app-shell](https://angular.io/guide/app-shell) (using `ng run cli-elements-universal:app-shell:production`) and then verifying that the `index.html` file was generated correctly.
(See, the `test-ssr` script in [package.json](./package.json).)
NOTE:
Currently, `domino` (the server-side DOM implementation used by `@angular/platform-server`) does not support [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components), so the Custom Elements functionality does not work on the server.

View File

@ -0,0 +1,168 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"cli-elements-universal": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/cli-elements-universal/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"progress": false
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "cli-elements-universal:build"
},
"configurations": {
"production": {
"browserTarget": "cli-elements-universal:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "cli-elements-universal:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"watch": false
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json",
"tsconfig.server.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "cli-elements-universal:serve",
"webdriverUpdate": false
},
"configurations": {
"production": {
"devServerTarget": "cli-elements-universal:serve:production"
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/cli-elements-universal/server",
"main": "src/main.server.ts",
"tsConfig": "tsconfig.server.json",
"progress": false
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"optimization": true
}
}
},
"app-shell": {
"builder": "@angular-devkit/build-angular:app-shell",
"options": {
"browserTarget": "cli-elements-universal:build",
"serverTarget": "cli-elements-universal:server",
"route": ""
},
"configurations": {
"production": {
"browserTarget": "cli-elements-universal:build:production",
"serverTarget": "cli-elements-universal:server:production"
}
}
}
}
}
},
"defaultProject": "cli-elements-universal"
}

View File

@ -0,0 +1,42 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome',
chromeOptions: {
binary: require('puppeteer').executablePath(),
// See /integration/README.md#browser-tests for more info on these args
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
},
},
directConnect: true,
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,22 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser.
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
const errorLogs = logs.filter(({level}) => level === logging.Level.SEVERE);
expect(errorLogs).toEqual([]);
});
it('should display welcome message', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toBe('cli-elements-universal app is running!');
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
async navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl);
}
async getTitleText(): Promise<string> {
return element(by.css('h1')).getText();
}
}

View File

@ -0,0 +1,13 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"node"
]
}
}

View File

@ -0,0 +1,47 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
// Env var CHROME_BIN is later picked up by karma-chrome-launcher that is triggered by
// `browsers: ['ChromeHeadlessNoSandbox']` below.
// See https://github.com/karma-runner/karma-chrome-launcher#usage for more info.
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/cli-elements-universal'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
}
},
browsers: ['ChromeHeadlessNoSandbox'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,55 @@
{
"name": "cli-elements-universal",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"pretest": "ng version",
"test": "ng test && yarn e2e --prod && yarn test-ssr",
"lint": "ng lint",
"e2e": "ng e2e --port 0",
"pretest-ssr": "yarn ng run cli-elements-universal:app-shell:production",
"test-ssr": "node --eval \"assert(fs.readFileSync('dist/cli-elements-universal/browser/index.html', 'utf8').includes('<app-title-ce>'), 'Expected \\'index.html\\' to contain \\'<app-title-ce>\\'.');\""
},
"private": true,
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/elements": "file:../../dist/packages-dist/elements",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/router": "file:../../dist/packages-dist/router",
"document-register-element": "^1.7.2",
"rxjs": "file:../../node_modules/rxjs",
"tslib": "file:../../node_modules/tslib",
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
},
"devDependencies": {
"@angular-devkit/build-angular": "file:../../node_modules/@angular-devkit/build-angular",
"@angular/cli": "file:../../node_modules/@angular/cli",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"@types/node": "file:../../node_modules/@types/node",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.2.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "file:../../node_modules/protractor",
"puppeteer": "file:../../node_modules/puppeteer",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "file:../../node_modules/typescript"
},
"//resolutions-comment": "Ensure a single version of webdriver-manager which comes from root node_modules that has already run webdriver-manager update",
"resolutions": {
"**/webdriver-manager": "file:../../node_modules/webdriver-manager"
}
}

View File

@ -0,0 +1,48 @@
import { Component, Input } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
TestTitleComponent,
],
imports: [
RouterTestingModule,
],
});
await TestBed.compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const appComp = fixture.componentInstance;
expect(appComp).toBeTruthy();
expect(appComp.title).toBe('cli-elements-universal');
});
it('should pass the app title to the `TitleComponent`', () => {
const fixture = TestBed.createComponent(AppComponent);
const titleDebugElement = fixture.debugElement.query(By.directive(TestTitleComponent));
const titleComp: TestTitleComponent = titleDebugElement.componentInstance;
fixture.detectChanges();
expect(titleComp).toBeTruthy();
expect(titleComp.appName).toBe('cli-elements-universal');
});
// Helpers
@Component({
selector: 'app-title-ce',
template: '',
})
class TestTitleComponent {
@Input() appName = '';
}
});

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<app-title-ce [appName]="title"></app-title-ce>',
})
export class AppComponent {
title = 'cli-elements-universal';
}

View File

@ -0,0 +1,32 @@
import { isPlatformBrowser } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, Inject, Injector, NgModule, PLATFORM_ID } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { TitleComponent } from './title.component';
@NgModule({
bootstrap: [
AppComponent,
],
declarations: [
AppComponent,
TitleComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
RouterModule.forRoot([]),
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,
],
})
export class AppModule {
constructor(injector: Injector, @Inject(PLATFORM_ID) platformId: object) {
if (isPlatformBrowser(platformId)) {
customElements.define('app-title-ce', createCustomElement(TitleComponent, {injector}));
}
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { RouterModule } from '@angular/router';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
bootstrap: [
AppComponent,
],
imports: [
AppModule,
RouterModule.forRoot([]),
ServerModule,
],
})
export class AppServerModule {}

View File

@ -0,0 +1,27 @@
import { TestBed } from '@angular/core/testing';
import { TitleComponent } from './title.component';
describe('TitleComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({declarations: [TitleComponent]});
await TestBed.compileComponents();
});
it('should create the component', () => {
const fixture = TestBed.createComponent(TitleComponent);
const titleComp = fixture.componentInstance;
expect(titleComp).toBeTruthy();
});
it('should render the title using the specified app name', () => {
const fixture = TestBed.createComponent(TitleComponent);
const titleComp = fixture.componentInstance;
const titleElem = fixture.nativeElement;
titleComp.appName = 'Test';
fixture.detectChanges();
expect(titleElem.querySelector('h1').textContent).toBe('Test app is running!');
});
});

View File

@ -0,0 +1,9 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-title',
template: '<h1>{{ appName }} app is running!</h1>',
})
export class TitleComponent {
@Input() appName = 'Unknown';
}

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CliElementsUniversal</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,11 @@
import '@angular/platform-server/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '@angular/platform-server';

View File

@ -0,0 +1,14 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
});

View File

@ -0,0 +1,64 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
import 'document-register-element';

View File

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View File

@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,29 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"target": "es2016",
"types": [
"node"
]
},
"files": [
"src/main.server.ts"
],
"angularCompilerOptions": {
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}

View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,152 @@
{
"extends": "tslint:recommended",
"rulesDirectory": [
"codelyzer"
],
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
integration/forms/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,27 @@
# Forms test app
This project was generated with [Angular CLI](https://github.com/angular/angular-cli).
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](https://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -0,0 +1,144 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"forms": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"progress": false
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "forms:build"
},
"configurations": {
"dev": {
"browserTarget": "forms:build:dev"
},
"production": {
"browserTarget": "forms:build:production"
},
"ci": {
"progress": false
},
"ci-production": {
"browserTarget": "forms:build:production",
"progress": false
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "forms:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"progress": false,
"watch": false
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "forms:serve",
"webdriverUpdate": false
},
"configurations": {
"production": {
"devServerTarget": "forms:serve:production"
},
"ci": {
"devServerTarget": "forms:serve:ci"
},
"ci-production": {
"devServerTarget": "forms:serve:ci-production"
}
}
}
}
}},
"defaultProject": "forms"
}

View File

@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 11 # For IE 11 support, remove 'not'.

View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome',
chromeOptions: {
binary: require('puppeteer').executablePath(),
// See /integration/README.md#browser-tests for more info on these args
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
},
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@ -0,0 +1,24 @@
import {browser, logging} from 'protractor';
import {AppPage} from './app.po';
describe('forms app', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display title', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Forms app');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@ -0,0 +1,11 @@
import {browser, by, element} from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root h1')).getText() as Promise<string>;
}
}

View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
// Env var CHROME_BIN is later picked up by karma-chrome-launcher that is triggered by
// `browsers: ['ChromeHeadlessNoSandbox']` below.
// See https://github.com/karma-runner/karma-chrome-launcher#usage for more info.
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/latest-app'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio']
}
},
browsers: ['ChromeHeadlessNoSandbox'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,54 @@
{
"name": "forms",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"pretest": "ng version",
"test": "ng test && yarn e2e --configuration=ci && yarn e2e --configuration=ci-production",
"lint": "ng lint",
"e2e": "ng e2e --port 0",
"postinstall": "ngcc --properties es2015 --create-ivy-entry-points"
},
"private": true,
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/forms": "file:../../dist/packages-dist/forms",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/router": "file:../../dist/packages-dist/router",
"rxjs": "file:../../node_modules/rxjs",
"tslib": "file:../../node_modules/tslib",
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
},
"devDependencies": {
"@angular-devkit/build-angular": "file:../../node_modules/@angular-devkit/build-angular",
"@angular/cli": "file:../../node_modules/@angular/cli",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/language-service": "file:../../dist/packages-dist/language-service",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"@types/jasminewd2": "file:../../node_modules/@types/jasminewd2",
"@types/node": "file:../../node_modules/@types/node",
"codelyzer": "5.2.0",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"karma": "4.4.0",
"karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "2.1.0",
"karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "1.4.2",
"protractor": "file:../../node_modules/protractor",
"puppeteer": "file:../../node_modules/puppeteer",
"ts-node": "8.3.0",
"tslint": "5.18.0",
"typescript": "file:../../node_modules/typescript"
},
"//resolutions-comment": "Ensure a single version of webdriver-manager which comes from root node_modules that has already run webdriver-manager update",
"resolutions": {
"**/webdriver-manager": "file:../../node_modules/webdriver-manager"
}
}

View File

@ -0,0 +1,3 @@
<h1>Forms app</h1>
<app-template-forms></app-template-forms>
<app-reactive-forms></app-reactive-forms>

View File

@ -0,0 +1,18 @@
import {TestBed, waitForAsync} from '@angular/core/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
TestBed
.configureTestingModule({
declarations: [AppComponent],
})
.compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@ -0,0 +1,114 @@
import {Component} from '@angular/core';
import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'app-template-forms',
template: `
<form novalidate>
<div ngModelGroup="profileForm">
<div>
First Name:
<input name="first" ngModel required />
</div>
<div>
Last Name:
<input name="last" ngModel />
</div>
<div>
Subscribe:
<input name="subscribed" type="checkbox" ngModel />
</div>
<div>Disabled: <input name="foo" ngModel disabled /></div>
<div *ngFor="let city of addresses; let i = index">
City <input [(ngModel)]="addresses[i].city" name="name" />
</div>
<button (click)="addCity()">Add City</button>
</div>
</form>`,
})
export class TemplateFormsComponent {
name = {first: 'Nancy', last: 'Drew', subscribed: true};
addresses = [{city: 'Toronto'}];
constructor() {
// We use this reference in our test
(window as any).templateFormsComponent = this;
}
addCity() {
this.addresses.push(({city: ''}));
}
}
@Component({
selector: 'app-reactive-forms',
template: `
<form [formGroup]="profileForm">
<div>
First Name:
<input type="text" formControlName="firstName" />
</div>
<div>
Last Name:
<input type="text" formControlName="lastName" />
</div>
<div>
Subscribe:
<input type="checkbox" formControlName="subscribed" />
</div>
<div>Disabled: <input formControlName="disabledInput" /></div>
<div formArrayName="addresses">
<div *ngFor="let item of itemControls; let i = index" [formGroupName]="i">
<div>City: <input formControlName="city" /></div>
</div>
</div>
<button (click)="addCity()">Add City</button>
</form>`,
})
export class ReactiveFormsComponent {
profileForm!: FormGroup;
addresses!: FormArray;
get itemControls() {
return (this.profileForm.get('addresses') as FormArray).controls;
}
constructor(private formBuilder: FormBuilder) {
// We use this reference in our test
(window as any).reactiveFormsComponent = this;
}
ngOnInit() {
this.profileForm = new FormGroup({
firstName: new FormControl('', Validators.required),
lastName: new FormControl(''),
addresses: new FormArray([]),
subscribed: new FormControl(),
disabledInput: new FormControl({value: '', disabled: true}),
});
this.addCity();
}
createItem(): FormGroup {
return this.formBuilder.group({
city: '',
});
}
addCity(): void {
this.addresses = this.profileForm.get('addresses') as FormArray;
this.addresses.push(this.createItem());
}
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
}

View File

@ -0,0 +1,23 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent, ReactiveFormsComponent, TemplateFormsComponent} from './app.component';
@NgModule({
declarations: [
AppComponent,
TemplateFormsComponent,
ReactiveFormsComponent,
],
imports: [
BrowserModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Forms</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags.ts';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View File

@ -0,0 +1,20 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,26 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

View File

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,91 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}

10009
integration/forms/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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 * as animations from '@angular/animations';
import * as animationsBrowser from '@angular/animations/browser';
import * as animationsBrowserTesting from '@angular/animations/browser/testing';
import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as commonTesting from '@angular/common/testing';
import * as commonHttpTesting from '@angular/common/testing';
import * as compiler from '@angular/compiler';
import * as compilerTesting from '@angular/compiler/testing';
import * as core from '@angular/core';
import * as coreTesting from '@angular/core/testing';
import * as elements from '@angular/elements';
import * as forms from '@angular/forms';
import * as platformBrowser from '@angular/platform-browser';
import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
import * as platformBrowserDynamicTesting from '@angular/platform-browser-dynamic/testing';
import * as platformBrowserAnimations from '@angular/platform-browser/animations';
import * as platformBrowserTesting from '@angular/platform-browser/testing';
import * as platformServer from '@angular/platform-server';
import * as platformServerInit from '@angular/platform-server/init';
import * as platformServerTesting from '@angular/platform-server/testing';
import * as router from '@angular/router';
import * as routerTesting from '@angular/router/testing';
import * as routerUpgrade from '@angular/router/upgrade';
import * as serviceWorker from '@angular/service-worker';
import * as upgrade from '@angular/upgrade';
import * as upgradeStatic from '@angular/upgrade/static';
import * as upgradeTesting from '@angular/upgrade/static/testing';
export default {
animations,
animationsBrowser,
animationsBrowserTesting,
common,
commonTesting,
commonHttp,
commonHttpTesting,
compiler,
compilerTesting,
core,
coreTesting,
elements,
forms,
platformBrowser,
platformBrowserTesting,
platformBrowserDynamic,
platformBrowserDynamicTesting,
platformBrowserAnimations,
platformServer,
platformServerInit,
platformServerTesting,
router,
routerTesting,
routerUpgrade,
serviceWorker,
upgrade,
upgradeStatic,
upgradeTesting,
};

View File

@ -0,0 +1,28 @@
{
"name": "angular-integration",
"description": "Assert that users with TypeScript 4.2 can type-check an Angular application",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/elements": "file:../../dist/packages-dist/elements",
"@angular/forms": "file:../../dist/packages-dist/forms",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/router": "file:../../dist/packages-dist/router",
"@angular/service-worker": "file:../../dist/packages-dist/service-worker",
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "4.2.3",
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
},
"scripts": {
"test": "tsc"
}
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist/out-tsc",
"rootDir": ".",
"target": "es5",
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.iterable",
"es2015.promise"
],
"types": [],
},
"files": [
"include-all.ts",
"node_modules/@types/jasmine/index.d.ts"
]
}

View File

@ -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);
}));
});

View File

@ -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),
)

View File

@ -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"],
)

View File

@ -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(`);
});
});
});

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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();
}
}

View File

@ -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);
});
});
});

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
};
}
}

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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()) : [],
};
}

View File

@ -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()));
}

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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",
],
)

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,35 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps =
[
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps =
[
":test_lib",
],
)

View File

@ -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 {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {Reference} from '../../imports';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {loadFakeCore, makeProgram} from '../../testing';
import {DtsMetadataReader} from '../src/dts';
runInEachFileSystem(() => {
beforeEach(() => {
loadFakeCore(getFileSystem());
});
describe('DtsMetadataReader', () => {
it('should not assume directives are structural', () => {
const mainPath = absoluteFrom('/main.d.ts');
const {program} = makeProgram(
[{
name: mainPath,
contents: `
import {ViewContainerRef} from '@angular/core';
import * as i0 from '@angular/core';
export declare class TestDir {
constructor(p0: ViewContainerRef);
static ɵdir: i0.ɵɵDirectiveDeclaration<TestDir, "[test]", never, {}, {}, never>
}
`
}],
{
skipLibCheck: true,
lib: ['es6', 'dom'],
});
const sf = getSourceFileOrError(program, mainPath);
const clazz = sf.statements[2];
if (!isNamedClassDeclaration(clazz)) {
return fail('Expected class declaration');
}
const typeChecker = program.getTypeChecker();
const dtsReader =
new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker));
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
expect(meta.isStructural).toBeFalse();
});
it('should identify a structural directive by its constructor', () => {
const mainPath = absoluteFrom('/main.d.ts');
const {program} = makeProgram(
[{
name: mainPath,
contents: `
import {TemplateRef, ViewContainerRef} from '@angular/core';
import * as i0 from '@angular/core';
export declare class TestDir {
constructor(p0: ViewContainerRef, p1: TemplateRef);
static ɵdir: i0.ɵɵDirectiveDeclaration<TestDir, "[test]", never, {}, {}, never>
}
`
}],
{
skipLibCheck: true,
lib: ['es6', 'dom'],
});
const sf = getSourceFileOrError(program, mainPath);
const clazz = sf.statements[2];
if (!isNamedClassDeclaration(clazz)) {
return fail('Expected class declaration');
}
const typeChecker = program.getTypeChecker();
const dtsReader =
new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker));
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
expect(meta.isStructural).toBeTrue();
});
});
});

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