2017-06-09 17:50:57 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2017-06-09 17:50:57 -04:00
|
|
|
*
|
|
|
|
* 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 fs from 'fs';
|
|
|
|
import * as path from 'path';
|
2017-07-07 19:29:39 -04:00
|
|
|
import * as ts from 'typescript';
|
2017-06-09 17:50:57 -04:00
|
|
|
|
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
2020-06-05 10:26:23 -04:00
|
|
|
import {main, mainDiagnosticsForTest, readCommandLineAndConfiguration, watchMode} from '../src/main';
|
2019-12-04 13:44:32 -05:00
|
|
|
import {setup, stripAnsi} from './test_support';
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-09 16:45:45 -04:00
|
|
|
describe('ngc transformer command-line', () => {
|
2017-06-09 17:50:57 -04:00
|
|
|
let basePath: string;
|
|
|
|
let outDir: string;
|
|
|
|
let write: (fileName: string, content: string) => void;
|
2017-08-09 16:45:45 -04:00
|
|
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
function shouldExist(fileName: string) {
|
|
|
|
if (!fs.existsSync(path.resolve(outDir, fileName))) {
|
|
|
|
throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function shouldNotExist(fileName: string) {
|
|
|
|
if (fs.existsSync(path.resolve(outDir, fileName))) {
|
|
|
|
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-09 17:50:57 -04:00
|
|
|
function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') {
|
|
|
|
write('tsconfig.json', tsconfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2017-08-23 16:57:37 -04:00
|
|
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
2019-01-25 13:44:49 -05:00
|
|
|
const support = setup();
|
|
|
|
basePath = support.basePath;
|
|
|
|
outDir = path.join(basePath, 'built');
|
|
|
|
process.chdir(basePath);
|
2020-03-30 11:22:25 -04:00
|
|
|
write = (fileName: string, content: string) => {
|
|
|
|
support.write(fileName, content);
|
|
|
|
};
|
2019-01-25 13:44:49 -05:00
|
|
|
|
2017-06-09 17:50:57 -04:00
|
|
|
write('tsconfig-base.json', `{
|
|
|
|
"compilerOptions": {
|
|
|
|
"experimentalDecorators": true,
|
2017-07-13 16:56:12 -04:00
|
|
|
"skipLibCheck": true,
|
2017-08-23 16:57:37 -04:00
|
|
|
"noImplicitAny": true,
|
2017-06-09 17:50:57 -04:00
|
|
|
"types": [],
|
|
|
|
"outDir": "built",
|
2017-08-23 16:57:37 -04:00
|
|
|
"rootDir": ".",
|
|
|
|
"baseUrl": ".",
|
2017-06-09 17:50:57 -04:00
|
|
|
"declaration": true,
|
2017-08-23 16:57:37 -04:00
|
|
|
"target": "es5",
|
2019-01-25 13:50:08 -05:00
|
|
|
"newLine": "lf",
|
2017-06-09 17:50:57 -04:00
|
|
|
"module": "es2015",
|
|
|
|
"moduleResolution": "node",
|
2017-08-23 16:57:37 -04:00
|
|
|
"lib": ["es6", "dom"],
|
|
|
|
"typeRoots": ["node_modules/@types"]
|
2019-08-20 13:52:31 -04:00
|
|
|
},
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"enableIvy": false
|
2017-06-09 17:50:57 -04:00
|
|
|
}
|
|
|
|
}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should compile without errors', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('test.ts', 'export const A = 1;');
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-08-09 16:45:45 -04:00
|
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
|
|
expect(exitCode).toBe(0);
|
2017-07-07 19:29:39 -04:00
|
|
|
});
|
|
|
|
|
2019-01-25 12:40:51 -05:00
|
|
|
it('should respect the "newLine" compiler option when printing diagnostics', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"newLine": "CRLF",
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('test.ts', 'export NOT_VALID = true;');
|
|
|
|
|
|
|
|
// Stub the error spy because we don't want to call through and print the
|
|
|
|
// expected error diagnostic.
|
|
|
|
errorSpy.and.stub();
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
|
|
|
`test.ts:1:1 - error TS1128: Declaration or statement expected.\r\n`);
|
2019-01-25 12:40:51 -05:00
|
|
|
expect(exitCode).toBe(1);
|
|
|
|
});
|
|
|
|
|
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
2020-06-05 10:26:23 -04:00
|
|
|
describe('decorator metadata', () => {
|
|
|
|
it('should add metadata as decorators if "annotationsAs" is set to "decorators"', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"emitDecoratorMetadata": true
|
|
|
|
},
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"annotationsAs": "decorators"
|
|
|
|
},
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('aclass.ts', `export class AClass {}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {AClass} from './aclass';
|
|
|
|
|
|
|
|
@NgModule({declarations: []})
|
|
|
|
export class MyModule {
|
|
|
|
constructor(importedClass: AClass) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('MyModule = __decorate([');
|
|
|
|
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
|
|
|
|
expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`);
|
|
|
|
expect(mymoduleSource).not.toContain('MyModule.ctorParameters');
|
|
|
|
expect(mymoduleSource).not.toContain('MyModule.decorators');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add metadata for Angular-decorated classes as static fields', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('aclass.ts', `export class AClass {}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {AClass} from './aclass';
|
|
|
|
|
|
|
|
@NgModule({declarations: []})
|
|
|
|
export class MyModule {
|
|
|
|
constructor(importedClass: AClass) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).not.toContain('__decorate');
|
|
|
|
expect(mymoduleSource).toContain('args: [{ declarations: [] },] }');
|
|
|
|
expect(mymoduleSource).not.toContain(`__metadata`);
|
|
|
|
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
|
|
|
|
expect(mymoduleSource).toContain(`{ type: AClass }`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not downlevel decorators for classes with custom decorators', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('aclass.ts', `export class AClass {}`);
|
|
|
|
write('decorator.ts', `
|
|
|
|
export function CustomDecorator(metadata: any) {
|
|
|
|
return (...args: any[]) => {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {AClass} from './aclass';
|
|
|
|
import {CustomDecorator} from './decorator';
|
|
|
|
|
|
|
|
@CustomDecorator({declarations: []})
|
|
|
|
export class MyModule {
|
|
|
|
constructor(importedClass: AClass) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('__decorate');
|
|
|
|
expect(mymoduleSource).toContain('({ declarations: [] })');
|
|
|
|
expect(mymoduleSource).not.toContain('AClass');
|
|
|
|
expect(mymoduleSource).not.toContain('.ctorParameters =');
|
|
|
|
expect(mymoduleSource).not.toContain('.decorators = ');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
describe('errors', () => {
|
2020-03-30 11:22:25 -04:00
|
|
|
beforeEach(() => {
|
|
|
|
errorSpy.and.stub();
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
it('should not print the stack trace if user input file does not exist', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["test.ts"]
|
|
|
|
}`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
2019-01-25 13:50:08 -05:00
|
|
|
`error TS6053: File '` + path.posix.join(basePath, 'test.ts') + `' not found.` +
|
2017-08-23 16:57:37 -04:00
|
|
|
'\n');
|
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
it('should not print the stack trace if user input file is malformed', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('test.ts', 'foo;');
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
|
|
|
`test.ts:1:1 - error TS2304: Cannot find name 'foo'.` +
|
2017-08-23 16:57:37 -04:00
|
|
|
'\n');
|
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
it('should not print the stack trace if cannot find the imported module', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('test.ts', `import {MyClass} from './not-exist-deps';`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
2020-05-12 03:19:59 -04:00
|
|
|
`test.ts:1:23 - error TS2307: Cannot find module './not-exist-deps' or its corresponding type declarations.` +
|
2017-08-23 16:57:37 -04:00
|
|
|
'\n');
|
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
it('should not print the stack trace if cannot import', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('empty-deps.ts', 'export const A = 1;');
|
|
|
|
write('test.ts', `import {MyClass} from './empty-deps';`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
|
|
|
`test.ts:1:9 - error TS2305: Module '"./empty-deps"' has no exported member 'MyClass'.\n`);
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
it('should not print the stack trace if type mismatches', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('empty-deps.ts', 'export const A = "abc";');
|
|
|
|
write('test.ts', `
|
|
|
|
import {A} from './empty-deps';
|
|
|
|
A();
|
|
|
|
`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2019-12-04 13:44:32 -05:00
|
|
|
const errorText = stripAnsi(errorSpy.calls.mostRecent().args[0]);
|
|
|
|
expect(errorText).toContain(
|
|
|
|
'test.ts:3:9 - error TS2349: This expression is not callable.\n' +
|
2019-10-01 19:44:50 -04:00
|
|
|
' Type \'String\' has no call signatures.\n');
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should print the stack trace on compiler internal errors', () => {
|
|
|
|
write('test.ts', 'export const A = 1;');
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', 'not-exist'], errorSpy);
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
|
|
expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory');
|
2018-09-05 15:38:53 -04:00
|
|
|
expect(errorSpy.calls.mostRecent().args[0]).toMatch(/at Object\.(fs\.)?lstatSync/);
|
2017-09-12 18:53:17 -04:00
|
|
|
expect(exitCode).toEqual(2);
|
2017-08-23 16:57:37 -04:00
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
|
|
|
|
it('should report errors for ngfactory files that are not referenced by root files', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({template: '{{unknownProp}}'})
|
|
|
|
export class MyComp {}
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
2017-11-14 20:49:47 -05:00
|
|
|
expect(errorSpy.calls.mostRecent().args[0]).toContain('mymodule.ts.MyComp.html');
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
|
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
|
|
|
|
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report errors as coming from the html file, not the factory', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('my.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({templateUrl: './my.component.html'})
|
|
|
|
export class MyComp {}
|
|
|
|
`);
|
|
|
|
write('my.component.html', `<h1>
|
|
|
|
{{unknownProp}}
|
|
|
|
</h1>`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyComp} from './my.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
2017-11-14 20:49:47 -05:00
|
|
|
expect(errorSpy.calls.mostRecent().args[0]).toContain('my.component.html(1,5):');
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
|
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
|
|
|
|
|
|
|
expect(exitCode).toEqual(1);
|
|
|
|
});
|
2017-08-23 16:57:37 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('compile ngfactory files', () => {
|
2017-06-09 17:50:57 -04:00
|
|
|
it('should compile ngfactory files that are not referenced by root files', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
2019-01-25 13:44:49 -05:00
|
|
|
expect(fs.existsSync(
|
|
|
|
path.resolve(outDir, 'node_modules', '@angular', 'core', 'core.ngfactory.js')))
|
|
|
|
.toBe(true);
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|
|
|
|
|
2017-12-07 11:52:16 -05:00
|
|
|
describe('comments', () => {
|
|
|
|
function compileAndRead(contents: string) {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"],
|
|
|
|
"angularCompilerOptions": {"allowEmptyCodegenFiles": true}
|
|
|
|
}`);
|
|
|
|
write('mymodule.ts', contents);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const modPath = path.resolve(outDir, 'mymodule.ngfactory.js');
|
|
|
|
expect(fs.existsSync(modPath)).toBe(true);
|
|
|
|
return fs.readFileSync(modPath, {encoding: 'UTF-8'});
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should be added', () => {
|
|
|
|
const contents = compileAndRead(`
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(contents).toContain('@fileoverview');
|
|
|
|
expect(contents).toContain('generated by the Angular template compiler');
|
|
|
|
expect(contents).toContain('@suppress {suspiciousCode');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be merged with existing fileoverview comments', () => {
|
|
|
|
const contents = compileAndRead(`/** Hello world. */
|
|
|
|
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(contents).toContain('Hello world.');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should only pick file comments', () => {
|
|
|
|
const contents = compileAndRead(`
|
|
|
|
/** Comment on class. */
|
|
|
|
class MyClass {
|
|
|
|
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
expect(contents).toContain('@fileoverview');
|
|
|
|
expect(contents).not.toContain('Comment on class.');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not be merged with @license comments', () => {
|
|
|
|
const contents = compileAndRead(`/** @license Some license. */
|
|
|
|
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(contents).toContain('@fileoverview');
|
|
|
|
expect(contents).not.toContain('@license');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be included in empty files', () => {
|
|
|
|
const contents = compileAndRead(`/** My comment. */
|
|
|
|
|
|
|
|
import {Inject, Injectable, Optional} from '@angular/core';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class NotAnAngularComponent {}
|
|
|
|
`);
|
|
|
|
expect(contents).toContain('My comment');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-08-02 14:20:07 -04:00
|
|
|
it('should compile with an explicit tsconfig reference', () => {
|
2017-06-09 17:50:57 -04:00
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
2019-01-25 13:44:49 -05:00
|
|
|
expect(fs.existsSync(
|
|
|
|
path.resolve(outDir, 'node_modules', '@angular', 'core', 'core.ngfactory.js')))
|
|
|
|
.toBe(true);
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
describe(`emit generated files depending on the source file`, () => {
|
|
|
|
const modules = ['comp', 'directive', 'module'];
|
|
|
|
beforeEach(() => {
|
|
|
|
write('src/comp.ts', `
|
|
|
|
import {Component, ViewEncapsulation} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'comp-a',
|
|
|
|
template: 'A',
|
|
|
|
styleUrls: ['plain.css'],
|
|
|
|
encapsulation: ViewEncapsulation.None
|
|
|
|
})
|
|
|
|
export class CompA {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'comp-b',
|
|
|
|
template: 'B',
|
|
|
|
styleUrls: ['emulated.css']
|
|
|
|
})
|
|
|
|
export class CompB {
|
|
|
|
}`);
|
|
|
|
write('src/plain.css', 'div {}');
|
|
|
|
write('src/emulated.css', 'div {}');
|
|
|
|
write('src/directive.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[someDir]',
|
|
|
|
host: {'[title]': 'someProp'},
|
|
|
|
})
|
|
|
|
export class SomeDirective {
|
|
|
|
@Input() someProp: string;
|
|
|
|
}`);
|
|
|
|
write('src/module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
import {CompA, CompB} from './comp';
|
|
|
|
import {SomeDirective} from './directive';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [
|
|
|
|
CompA, CompB,
|
|
|
|
SomeDirective,
|
|
|
|
],
|
|
|
|
exports: [
|
|
|
|
CompA, CompB,
|
|
|
|
SomeDirective,
|
|
|
|
],
|
|
|
|
})
|
|
|
|
export class SomeModule {
|
|
|
|
}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
function expectJsDtsMetadataJsonToExist() {
|
|
|
|
modules.forEach(moduleName => {
|
|
|
|
shouldExist(moduleName + '.js');
|
|
|
|
shouldExist(moduleName + '.d.ts');
|
|
|
|
shouldExist(moduleName + '.metadata.json');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-29 17:55:44 -04:00
|
|
|
function expectAllGeneratedFilesToExist(enableSummariesForJit = true) {
|
2017-08-23 16:57:37 -04:00
|
|
|
modules.forEach(moduleName => {
|
|
|
|
if (/module|comp/.test(moduleName)) {
|
|
|
|
shouldExist(moduleName + '.ngfactory.js');
|
|
|
|
shouldExist(moduleName + '.ngfactory.d.ts');
|
|
|
|
} else {
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.js');
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
2017-09-29 17:55:44 -04:00
|
|
|
}
|
|
|
|
if (enableSummariesForJit) {
|
2017-08-23 16:57:37 -04:00
|
|
|
shouldExist(moduleName + '.ngsummary.js');
|
|
|
|
shouldExist(moduleName + '.ngsummary.d.ts');
|
2017-09-29 17:55:44 -04:00
|
|
|
} else {
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.js');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.d.ts');
|
2017-08-23 16:57:37 -04:00
|
|
|
}
|
|
|
|
shouldExist(moduleName + '.ngsummary.json');
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.metadata.json');
|
|
|
|
});
|
|
|
|
shouldExist('plain.css.ngstyle.js');
|
|
|
|
shouldExist('plain.css.ngstyle.d.ts');
|
|
|
|
shouldExist('emulated.css.shim.ngstyle.js');
|
|
|
|
shouldExist('emulated.css.shim.ngstyle.d.ts');
|
|
|
|
}
|
|
|
|
|
2017-09-29 17:55:44 -04:00
|
|
|
it('should emit generated files from sources with summariesForJit', () => {
|
2017-08-23 16:57:37 -04:00
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"enableSummariesForJit": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"include": ["src/**/*.ts"]
|
|
|
|
}`);
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
outDir = path.resolve(basePath, 'built', 'src');
|
|
|
|
expectJsDtsMetadataJsonToExist();
|
2017-09-29 17:55:44 -04:00
|
|
|
expectAllGeneratedFilesToExist(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not emit generated files from sources without summariesForJit', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"enableSummariesForJit": false
|
|
|
|
},
|
|
|
|
"include": ["src/**/*.ts"]
|
|
|
|
}`);
|
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
outDir = path.resolve(basePath, 'built', 'src');
|
|
|
|
expectJsDtsMetadataJsonToExist();
|
|
|
|
expectAllGeneratedFilesToExist(false);
|
2017-08-23 16:57:37 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit generated files from libraries', () => {
|
|
|
|
// first only generate .d.ts / .js / .metadata.json files
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"skipTemplateCodegen": true
|
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "lib"
|
|
|
|
},
|
|
|
|
"include": ["src/**/*.ts"]
|
|
|
|
}`);
|
2017-09-13 19:55:42 -04:00
|
|
|
let exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
outDir = path.resolve(basePath, 'lib', 'src');
|
|
|
|
modules.forEach(moduleName => {
|
|
|
|
shouldExist(moduleName + '.js');
|
|
|
|
shouldExist(moduleName + '.d.ts');
|
|
|
|
shouldExist(moduleName + '.metadata.json');
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.js');
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.js');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.d.ts');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.json');
|
|
|
|
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
|
|
|
shouldNotExist(moduleName + '.ngsummary.metadata.json');
|
|
|
|
});
|
|
|
|
shouldNotExist('src/plain.css.ngstyle.js');
|
|
|
|
shouldNotExist('src/plain.css.ngstyle.d.ts');
|
|
|
|
shouldNotExist('src/emulated.css.shim.ngstyle.js');
|
|
|
|
shouldNotExist('src/emulated.css.shim.ngstyle.d.ts');
|
|
|
|
// Then compile again, using the previous .metadata.json as input.
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"skipTemplateCodegen": false,
|
|
|
|
"enableSummariesForJit": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "built"
|
|
|
|
},
|
|
|
|
"include": ["lib/**/*.d.ts"]
|
|
|
|
}`);
|
|
|
|
write('lib/src/plain.css', 'div {}');
|
|
|
|
write('lib/src/emulated.css', 'div {}');
|
2017-09-13 19:55:42 -04:00
|
|
|
exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-08-23 16:57:37 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
outDir = path.resolve(basePath, 'built', 'lib', 'src');
|
|
|
|
expectAllGeneratedFilesToExist();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-08-02 14:20:07 -04:00
|
|
|
describe('closure', () => {
|
2018-08-27 14:04:48 -04:00
|
|
|
it('should not run tsickle by default', () => {
|
2017-08-02 14:20:07 -04:00
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
2018-08-27 14:04:48 -04:00
|
|
|
"files": ["mymodule.ts"],
|
2017-08-02 14:20:07 -04:00
|
|
|
}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({template: ''})
|
|
|
|
export class MyComp {}
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-08-02 14:20:07 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).not.toContain('@fileoverview added by tsickle');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add closure annotations', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"annotateForClosureCompiler": true
|
|
|
|
},
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('mymodule.ts', `
|
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
2020-06-05 10:26:23 -04:00
|
|
|
import {NgModule, Component, Injectable} from '@angular/core';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class InjectedClass {}
|
2017-08-02 14:20:07 -04:00
|
|
|
|
|
|
|
@Component({template: ''})
|
|
|
|
export class MyComp {
|
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
2020-06-05 10:26:23 -04:00
|
|
|
constructor(injected: InjectedClass) {}
|
2017-08-02 14:20:07 -04:00
|
|
|
fn(p: any) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
2017-08-02 14:20:07 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('@fileoverview added by tsickle');
|
|
|
|
expect(mymoduleSource).toContain('@param {?} p');
|
fix(compiler-cli): downlevel angular decorators to static properties (#37382)
In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.
TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:
```
@Directive()
export class MyDirective {
constructor(button: MyButton) {}
}
export class MyButton {}
```
TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:
```
let MyDirective = class MyDir {}
MyDirective = __decorate([
Directive(),
__metadata("design:paramtypes", [MyButton]),
], MyDirective);
let MyButton = class MyButton {}
```
Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.
The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:
```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```
As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).
Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:
```
MyDirective.ctorParameters = () => [MyButton];
```
With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.
In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).
This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.
```
@Input() panelElement: HTMLElement;
```
Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.
More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.
Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.
PR Close #37382
2020-06-05 10:26:23 -04:00
|
|
|
expect(mymoduleSource).toMatch(/\/\*\* @nocollapse \*\/\s+MyComp\.ctorParameters = /);
|
2017-08-02 14:20:07 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-09-28 12:31:28 -04:00
|
|
|
it('should not rewrite imports when annotating with closure', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"paths": {
|
|
|
|
"submodule": ["./src/submodule/public_api.ts"]
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"annotateForClosureCompiler": true
|
|
|
|
},
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/test.txt', ' ');
|
|
|
|
write('src/submodule/public_api.ts', `
|
|
|
|
export const A = 1;
|
|
|
|
`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
import {A} from 'submodule';
|
|
|
|
|
|
|
|
@Component({template: ''})
|
|
|
|
export class MyComp {
|
|
|
|
fn(p: any) { return A; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
2017-12-22 12:36:47 -05:00
|
|
|
expect(mymoduleSource).toContain(`import { A } from 'submodule'`);
|
2017-09-28 12:31:28 -04:00
|
|
|
});
|
|
|
|
|
2017-08-08 15:40:08 -04:00
|
|
|
describe('expression lowering', () => {
|
2017-07-13 17:25:17 -04:00
|
|
|
beforeEach(() => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"]
|
|
|
|
}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
function compile(): number {
|
2017-09-13 19:55:42 -04:00
|
|
|
errorSpy.calls.reset();
|
|
|
|
const result = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-08-09 16:45:45 -04:00
|
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
2017-07-13 17:25:17 -04:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should be able to lower a lambda expression in a provider', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
class Foo {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule],
|
|
|
|
providers: [{provide: 'someToken', useFactory: () => new Foo()}]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('export { ɵ0');
|
|
|
|
|
|
|
|
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
|
|
|
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
|
|
|
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to lower a function expression in a provider', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
class Foo {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule],
|
|
|
|
providers: [{provide: 'someToken', useFactory: function() {return new Foo();}}]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('export { ɵ0');
|
|
|
|
|
|
|
|
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
|
|
|
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
|
|
|
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should able to lower multiple expressions', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
class Foo {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule],
|
|
|
|
providers: [
|
|
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
|
|
{provide: 'someToken', useFactory: () => new Foo()}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('ɵ0 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('ɵ1 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('ɵ2 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('ɵ3 = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('export { ɵ0, ɵ1, ɵ2, ɵ3');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to lower an indirect expression', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
class Foo {}
|
|
|
|
|
|
|
|
const factory = () => new Foo();
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule],
|
|
|
|
providers: [{provide: 'someToken', useFactory: factory}]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
2017-08-08 15:40:08 -04:00
|
|
|
expect(compile()).toEqual(0, 'Compile failed');
|
2017-07-13 17:25:17 -04:00
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
2017-09-01 19:27:35 -04:00
|
|
|
expect(mymoduleSource).toContain('var factory = function () { return new Foo(); }');
|
|
|
|
expect(mymoduleSource).toContain('var ɵ0 = factory;');
|
|
|
|
expect(mymoduleSource).toContain('export { ɵ0 };');
|
2017-07-13 17:25:17 -04:00
|
|
|
});
|
2017-08-08 15:40:08 -04:00
|
|
|
|
|
|
|
it('should not lower a lambda that is already exported', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
2017-08-02 14:20:07 -04:00
|
|
|
export class Foo {}
|
2017-08-08 15:40:08 -04:00
|
|
|
|
|
|
|
export const factory = () => new Foo();
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [CommonModule],
|
|
|
|
providers: [{provide: 'someToken', useFactory: factory}]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).not.toContain('ɵ0');
|
|
|
|
});
|
2017-09-01 19:27:35 -04:00
|
|
|
|
2018-03-27 14:56:23 -04:00
|
|
|
it('should lower an NgModule id', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
id: (() => 'test')(),
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('id: ɵ0');
|
|
|
|
expect(mymoduleSource).toMatch(/ɵ0 = .*'test'/);
|
|
|
|
});
|
|
|
|
|
2018-03-30 11:02:30 -04:00
|
|
|
it('should lower loadChildren', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import {RouterModule} from '@angular/router';
|
2018-12-14 16:40:01 -05:00
|
|
|
|
2018-03-30 11:02:30 -04:00
|
|
|
export function foo(): string {
|
|
|
|
console.log('side-effect');
|
|
|
|
return 'test';
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'route',
|
|
|
|
template: 'route',
|
|
|
|
})
|
|
|
|
export class Route {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Route],
|
|
|
|
imports: [
|
|
|
|
RouterModule.forRoot([
|
|
|
|
{path: '', pathMatch: 'full', component: Route, loadChildren: foo()}
|
|
|
|
]),
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('loadChildren: ɵ0');
|
|
|
|
expect(mymoduleSource).toMatch(/ɵ0 = .*foo\(\)/);
|
|
|
|
});
|
|
|
|
|
feat(compiler-cli): lower some exported expressions (#30038)
The compiler uses metadata to represent what it statically knows about
various expressions in a program. Occasionally, expressions in the program
for which metadata is extracted may contain sub-expressions which are not
representable in metadata. One such construct is an arrow function.
The compiler does not always need to understand such expressions completely.
For example, for a provider defined with `useValue`, the compiler does not
need to understand the value at all, only the outer provider definition. In
this case, the compiler employs a technique known as "expression lowering",
where it rewrites the provider expression into one that can be represented
in metadata. Chiefly, this involves extracting out the dynamic part (the
`useValue` expression) into an exported constant.
Lowering is applied through a heuristic, which considers the containing
statement as well as the field name of the expression.
Previously, this heuristic was not completely accurate in the case of
route definitions and the `loadChildren` field, which is lowered. If the
route definition using `loadChildren` existed inside a decorator invocation,
lowering was performed correctly. However, if it existed inside a standalone
variable declaration with an export keyword, the heuristic would conclude
that lowering was unnecessary. For ordinary providers this is true; however
the compiler attempts to fully understand the ROUTES token and thus even if
an array of routes is declared in an exported variable, any `loadChildren`
expressions within still need to be lowered.
This commit enables lowering of already exported variables under a limited
set of conditions (where the initializer expression is of a specific form).
This should enable the use of `loadChildren` in route definitions.
PR Close #30038
2019-04-22 18:22:52 -04:00
|
|
|
it('should lower loadChildren in an exported variable expression', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
|
|
|
|
export function foo(): string {
|
|
|
|
console.log('side-effect');
|
|
|
|
return 'test';
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'route',
|
|
|
|
template: 'route',
|
|
|
|
})
|
|
|
|
export class Route {}
|
|
|
|
|
|
|
|
export const routes = [
|
|
|
|
{path: '', pathMatch: 'full', component: Route, loadChildren: foo()}
|
|
|
|
];
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Route],
|
|
|
|
imports: [
|
|
|
|
RouterModule.forRoot(routes),
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('loadChildren: ɵ0');
|
|
|
|
expect(mymoduleSource).toMatch(/ɵ0 = .*foo\(\)/);
|
|
|
|
});
|
|
|
|
|
2017-09-01 19:27:35 -04:00
|
|
|
it('should be able to lower supported expressions', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["module.ts"]
|
|
|
|
}`);
|
|
|
|
write('module.ts', `
|
|
|
|
import {NgModule, InjectionToken} from '@angular/core';
|
|
|
|
import {AppComponent} from './app';
|
|
|
|
|
|
|
|
export interface Info {
|
|
|
|
route: string;
|
|
|
|
data: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const T1 = new InjectionToken<string>('t1');
|
|
|
|
export const T2 = new InjectionToken<string>('t2');
|
|
|
|
export const T3 = new InjectionToken<number>('t3');
|
|
|
|
export const T4 = new InjectionToken<Info[]>('t4');
|
|
|
|
|
|
|
|
enum SomeEnum {
|
|
|
|
OK,
|
|
|
|
Cancel
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateString() {
|
|
|
|
return 'someValue';
|
|
|
|
}
|
|
|
|
|
|
|
|
const routeLikeData = [{
|
|
|
|
route: '/home',
|
|
|
|
data: calculateString()
|
|
|
|
}];
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [AppComponent],
|
|
|
|
providers: [
|
|
|
|
{ provide: T1, useValue: calculateString() },
|
|
|
|
{ provide: T2, useFactory: () => 'someValue' },
|
|
|
|
{ provide: T3, useValue: SomeEnum.OK },
|
|
|
|
{ provide: T4, useValue: routeLikeData }
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
write('app.ts', `
|
|
|
|
import {Component, Inject} from '@angular/core';
|
|
|
|
import * as m from './module';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-app',
|
|
|
|
template: ''
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
constructor(
|
|
|
|
@Inject(m.T1) private t1: string,
|
|
|
|
@Inject(m.T2) private t2: string,
|
|
|
|
@Inject(m.T3) private t3: number,
|
|
|
|
@Inject(m.T4) private t4: m.Info[],
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
expect(main(['-p', basePath], errorSpy)).toBe(0);
|
2017-09-01 19:27:35 -04:00
|
|
|
shouldExist('module.js');
|
|
|
|
});
|
2017-09-24 14:19:01 -04:00
|
|
|
|
|
|
|
it('should allow to use lowering with export *', () => {
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
export * from './util';
|
|
|
|
|
2018-01-19 18:06:44 -05:00
|
|
|
// Note: the lambda will be lowered into an exported expression
|
2017-09-24 14:19:01 -04:00
|
|
|
@NgModule({providers: [{provide: 'aToken', useValue: () => 2}]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
write('util.ts', `
|
2018-01-19 18:06:44 -05:00
|
|
|
// Note: The lambda will be lowered into an exported expression
|
2017-09-24 14:19:01 -04:00
|
|
|
const x = () => 2;
|
|
|
|
|
|
|
|
export const y = x;
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(compile()).toEqual(0);
|
|
|
|
|
|
|
|
const mymoduleSource = fs.readFileSync(path.resolve(outDir, 'mymodule.js'), 'utf8');
|
|
|
|
expect(mymoduleSource).toContain('ɵ0');
|
|
|
|
|
|
|
|
const utilSource = fs.readFileSync(path.resolve(outDir, 'util.js'), 'utf8');
|
|
|
|
expect(utilSource).toContain('ɵ0');
|
|
|
|
|
|
|
|
const mymoduleNgFactoryJs =
|
|
|
|
fs.readFileSync(path.resolve(outDir, 'mymodule.ngfactory.js'), 'utf8');
|
|
|
|
// check that the generated code refers to ɵ0 from mymodule, and not from util!
|
|
|
|
expect(mymoduleNgFactoryJs).toContain(`import * as i1 from "./mymodule"`);
|
|
|
|
expect(mymoduleNgFactoryJs).toContain(`"aToken", i1.ɵ0`);
|
|
|
|
});
|
2017-07-13 17:25:17 -04:00
|
|
|
});
|
|
|
|
|
2017-09-21 21:05:07 -04:00
|
|
|
function writeFlatModule(outFile: string) {
|
2017-06-09 17:50:57 -04:00
|
|
|
writeConfig(`
|
2017-09-21 21:05:07 -04:00
|
|
|
{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"flatModuleId": "flat_module",
|
|
|
|
"flatModuleOutFile": "${outFile}",
|
2018-04-02 16:05:08 -04:00
|
|
|
"skipTemplateCodegen": true,
|
|
|
|
"enableResourceInlining": true
|
2017-09-21 21:05:07 -04:00
|
|
|
},
|
|
|
|
"files": ["public-api.ts"]
|
|
|
|
}
|
2017-06-09 17:50:57 -04:00
|
|
|
`);
|
|
|
|
write('public-api.ts', `
|
|
|
|
export * from './src/flat.component';
|
|
|
|
export * from './src/flat.module';`);
|
|
|
|
write('src/flat.component.html', '<div>flat module component</div>');
|
|
|
|
write('src/flat.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'flat-comp',
|
|
|
|
templateUrl: 'flat.component.html',
|
|
|
|
})
|
|
|
|
export class FlatComponent {
|
|
|
|
}`);
|
|
|
|
write('src/flat.module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
import {FlatComponent} from './flat.component';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [
|
|
|
|
FlatComponent,
|
|
|
|
],
|
|
|
|
exports: [
|
|
|
|
FlatComponent,
|
2018-04-02 16:05:08 -04:00
|
|
|
],
|
2017-06-09 17:50:57 -04:00
|
|
|
})
|
|
|
|
export class FlatModule {
|
|
|
|
}`);
|
2017-09-21 21:05:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
it('should be able to generate a flat module library', () => {
|
|
|
|
writeFlatModule('index.js');
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
2017-06-09 17:50:57 -04:00
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
shouldExist('index.js');
|
|
|
|
shouldExist('index.metadata.json');
|
|
|
|
});
|
|
|
|
|
2018-04-06 11:23:40 -04:00
|
|
|
it('should downlevel templates in flat module metadata', () => {
|
2018-04-02 16:05:08 -04:00
|
|
|
writeFlatModule('index.js');
|
|
|
|
|
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
shouldExist('index.js');
|
|
|
|
shouldExist('index.metadata.json');
|
|
|
|
|
|
|
|
const metadataPath = path.resolve(outDir, 'index.metadata.json');
|
|
|
|
const metadataSource = fs.readFileSync(metadataPath, 'utf8');
|
|
|
|
expect(metadataSource).not.toContain('templateUrl');
|
2018-04-06 11:23:40 -04:00
|
|
|
expect(metadataSource).toContain('<div>flat module component</div>');
|
2018-04-02 16:05:08 -04:00
|
|
|
});
|
|
|
|
|
2017-06-09 17:50:57 -04:00
|
|
|
describe('with tree example', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
writeConfig();
|
|
|
|
write('index_aot.ts', `
|
|
|
|
import {enableProdMode} from '@angular/core';
|
|
|
|
import {platformBrowser} from '@angular/platform-browser';
|
|
|
|
|
|
|
|
import {AppModuleNgFactory} from './tree.ngfactory';
|
|
|
|
|
|
|
|
enableProdMode();
|
|
|
|
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);`);
|
|
|
|
write('tree.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'tree',
|
|
|
|
inputs: ['data'],
|
|
|
|
template:
|
|
|
|
\`<span [style.backgroundColor]="bgColor"> {{data.value}} </span><tree *ngIf='data.right != null' [data]='data.right'></tree><tree *ngIf='data.left != null' [data]='data.left'></tree>\`
|
|
|
|
})
|
|
|
|
export class TreeComponent {
|
|
|
|
data: any;
|
|
|
|
bgColor = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({imports: [CommonModule], bootstrap: [TreeComponent], declarations: [TreeComponent]})
|
|
|
|
export class AppModule {}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should compile without error', () => {
|
2017-09-13 19:55:42 -04:00
|
|
|
expect(main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0);
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
describe('with external symbol re-exports enabled', () => {
|
|
|
|
it('should be able to compile multiple libraries with summaries', () => {
|
|
|
|
// Note: we need to emit the generated code for the libraries
|
|
|
|
// into the node_modules, as that is the only way that we
|
|
|
|
// currently support when using summaries.
|
|
|
|
// TODO(tbosch): add support for `paths` to our CompilerHost.fileNameToModuleName
|
|
|
|
// and then use `paths` here instead of writing to node_modules.
|
|
|
|
|
|
|
|
// Angular
|
|
|
|
write('tsconfig-ng.json', `{
|
2017-08-23 16:57:37 -04:00
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"generateCodeForLibraries": true,
|
|
|
|
"enableSummariesForJit": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "."
|
|
|
|
},
|
|
|
|
"include": ["node_modules/@angular/core/**/*"],
|
|
|
|
"exclude": [
|
|
|
|
"node_modules/@angular/core/test/**",
|
|
|
|
"node_modules/@angular/core/testing/**"
|
|
|
|
]
|
|
|
|
}`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
// Lib 1
|
|
|
|
write('lib1/tsconfig-lib1.json', `{
|
2017-08-23 16:57:37 -04:00
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"generateCodeForLibraries": false,
|
2019-02-12 17:29:28 -05:00
|
|
|
"enableSummariesForJit": true,
|
|
|
|
"createExternalSymbolFactoryReexports": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../node_modules/lib1_built"
|
|
|
|
}
|
|
|
|
}`);
|
2019-02-12 17:29:28 -05:00
|
|
|
write('lib1/module.ts', `
|
2017-06-09 17:50:57 -04:00
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
export function someFactory(): any { return null; }
|
|
|
|
@NgModule({
|
|
|
|
providers: [{provide: 'foo', useFactory: someFactory}]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
2019-02-12 17:29:28 -05:00
|
|
|
write('lib1/class1.ts', `export class Class1 {}`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
// Lib 2
|
|
|
|
write('lib2/tsconfig-lib2.json', `{
|
2017-08-23 16:57:37 -04:00
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"generateCodeForLibraries": false,
|
2019-02-12 17:29:28 -05:00
|
|
|
"enableSummariesForJit": true,
|
|
|
|
"createExternalSymbolFactoryReexports": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../node_modules/lib2_built"
|
|
|
|
}
|
|
|
|
}`);
|
2019-02-12 17:29:28 -05:00
|
|
|
write('lib2/module.ts', `
|
2017-08-23 16:57:37 -04:00
|
|
|
export {Module} from 'lib1_built/module';
|
2017-06-09 17:50:57 -04:00
|
|
|
`);
|
2019-02-12 17:29:28 -05:00
|
|
|
write('lib2/class2.ts', `
|
|
|
|
import {Class1} from 'lib1_built/class1';
|
|
|
|
export class Class2 {
|
|
|
|
constructor(class1: Class1) {}
|
|
|
|
}
|
|
|
|
`);
|
2017-06-09 17:50:57 -04:00
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
// Application
|
|
|
|
write('app/tsconfig-app.json', `{
|
2017-08-23 16:57:37 -04:00
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
2017-09-29 17:55:44 -04:00
|
|
|
"generateCodeForLibraries": false,
|
2019-02-12 17:29:28 -05:00
|
|
|
"enableSummariesForJit": true,
|
|
|
|
"createExternalSymbolFactoryReexports": true
|
2017-08-23 16:57:37 -04:00
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../built/app"
|
|
|
|
}
|
|
|
|
}`);
|
2019-02-12 17:29:28 -05:00
|
|
|
write('app/main.ts', `
|
2017-06-09 17:50:57 -04:00
|
|
|
import {NgModule, Inject} from '@angular/core';
|
2017-08-23 16:57:37 -04:00
|
|
|
import {Module} from 'lib2_built/module';
|
2017-06-09 17:50:57 -04:00
|
|
|
@NgModule({
|
|
|
|
imports: [Module]
|
|
|
|
})
|
|
|
|
export class AppModule {
|
|
|
|
constructor(@Inject('foo') public foo: any) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
expect(main(['-p', path.join(basePath, 'lib1', 'tsconfig-lib1.json')], errorSpy)).toBe(0);
|
|
|
|
expect(main(['-p', path.join(basePath, 'lib2', 'tsconfig-lib2.json')], errorSpy)).toBe(0);
|
|
|
|
expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0);
|
|
|
|
|
|
|
|
// library 1
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
|
|
shouldExist('lib1_built/module.js');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.json');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.js');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.d.ts');
|
|
|
|
shouldExist('lib1_built/module.ngfactory.js');
|
|
|
|
shouldExist('lib1_built/module.ngfactory.d.ts');
|
|
|
|
|
|
|
|
// library 2
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
|
|
shouldExist('lib2_built/module.js');
|
|
|
|
shouldExist('lib2_built/module.ngsummary.json');
|
|
|
|
shouldExist('lib2_built/module.ngsummary.js');
|
|
|
|
shouldExist('lib2_built/module.ngsummary.d.ts');
|
|
|
|
shouldExist('lib2_built/module.ngfactory.js');
|
|
|
|
shouldExist('lib2_built/module.ngfactory.d.ts');
|
|
|
|
|
|
|
|
shouldExist('lib2_built/class2.ngsummary.json');
|
|
|
|
shouldNotExist('lib2_built/class2.ngsummary.js');
|
|
|
|
shouldNotExist('lib2_built/class2.ngsummary.d.ts');
|
|
|
|
shouldExist('lib2_built/class2.ngfactory.js');
|
|
|
|
shouldExist('lib2_built/class2.ngfactory.d.ts');
|
|
|
|
|
|
|
|
// app
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `built`
|
|
|
|
outDir = path.resolve(basePath, 'built');
|
|
|
|
shouldExist('app/main.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should create external symbol re-exports', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"generateCodeForLibraries": false,
|
|
|
|
"createExternalSymbolFactoryReexports": true
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
|
|
|
|
write('test.ts', `
|
|
|
|
import {Injectable, NgZone} from '@angular/core';
|
2019-04-04 14:41:52 -04:00
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
@Injectable({providedIn: 'root'})
|
|
|
|
export class MyService {
|
|
|
|
constructor(public ngZone: NgZone) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(main(['-p', basePath], errorSpy)).toBe(0);
|
|
|
|
|
|
|
|
shouldExist('test.js');
|
|
|
|
shouldExist('test.metadata.json');
|
|
|
|
shouldExist('test.ngsummary.json');
|
|
|
|
shouldExist('test.ngfactory.js');
|
|
|
|
shouldExist('test.ngfactory.d.ts');
|
|
|
|
|
|
|
|
const summaryJson = require(path.join(outDir, 'test.ngsummary.json'));
|
|
|
|
const factoryOutput = fs.readFileSync(path.join(outDir, 'test.ngfactory.js'), 'utf8');
|
|
|
|
|
|
|
|
expect(summaryJson['symbols'][0].name).toBe('MyService');
|
|
|
|
expect(summaryJson['symbols'][1])
|
|
|
|
.toEqual(jasmine.objectContaining({name: 'NgZone', importAs: 'NgZone_1'}));
|
|
|
|
|
|
|
|
expect(factoryOutput).toContain(`export { NgZone as NgZone_1 } from "@angular/core";`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to compile multiple libraries with summaries', () => {
|
|
|
|
// Lib 1
|
|
|
|
write('lib1/tsconfig-lib1.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"generateCodeForLibraries": false,
|
|
|
|
"enableSummariesForJit": true
|
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../node_modules/lib1_built"
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('lib1/module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
export function someFactory(): any { return null; }
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
providers: [{provide: 'foo', useFactory: someFactory}]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
write('lib1/class1.ts', `export class Class1 {}`);
|
|
|
|
|
|
|
|
// Lib 2
|
|
|
|
write('lib2/tsconfig-lib2.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"generateCodeForLibraries": false,
|
|
|
|
"enableSummariesForJit": true
|
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../node_modules/lib2_built"
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('lib2/module.ts', `export {Module} from 'lib1_built/module';`);
|
|
|
|
write('lib2/class2.ts', `
|
|
|
|
import {Class1} from 'lib1_built/class1';
|
2019-04-04 14:41:52 -04:00
|
|
|
|
2019-02-12 17:29:28 -05:00
|
|
|
export class Class2 {
|
|
|
|
constructor(class1: Class1) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
// Application
|
|
|
|
write('app/tsconfig-app.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"generateCodeForLibraries": false,
|
|
|
|
"enableSummariesForJit": true
|
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"rootDir": ".",
|
|
|
|
"outDir": "../built/app"
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('app/main.ts', `
|
|
|
|
import {NgModule, Inject} from '@angular/core';
|
|
|
|
import {Module} from 'lib2_built/module';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [Module]
|
|
|
|
})
|
|
|
|
export class AppModule {
|
|
|
|
constructor(@Inject('foo') public foo: any) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
expect(main(['-p', path.join(basePath, 'lib1', 'tsconfig-lib1.json')], errorSpy)).toBe(0);
|
|
|
|
expect(main(['-p', path.join(basePath, 'lib2', 'tsconfig-lib2.json')], errorSpy)).toBe(0);
|
|
|
|
expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0);
|
2017-08-23 16:57:37 -04:00
|
|
|
|
|
|
|
// library 1
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
|
|
shouldExist('lib1_built/module.js');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.json');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.js');
|
|
|
|
shouldExist('lib1_built/module.ngsummary.d.ts');
|
|
|
|
shouldExist('lib1_built/module.ngfactory.js');
|
|
|
|
shouldExist('lib1_built/module.ngfactory.d.ts');
|
|
|
|
|
|
|
|
// library 2
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
|
|
shouldExist('lib2_built/module.js');
|
2019-02-12 17:29:28 -05:00
|
|
|
|
|
|
|
// "module.ts" re-exports an external symbol and will therefore
|
|
|
|
// have a summary JSON file and its corresponding JIT summary.
|
2017-08-23 16:57:37 -04:00
|
|
|
shouldExist('lib2_built/module.ngsummary.json');
|
|
|
|
shouldExist('lib2_built/module.ngsummary.js');
|
|
|
|
shouldExist('lib2_built/module.ngsummary.d.ts');
|
2019-02-12 17:29:28 -05:00
|
|
|
// "module.ts" only re-exports an external symbol and the AOT compiler does not
|
|
|
|
// need to generate anything. Therefore there should be no factory files.
|
|
|
|
shouldNotExist('lib2_built/module.ngfactory.js');
|
|
|
|
shouldNotExist('lib2_built/module.ngfactory.d.ts');
|
2017-08-23 16:57:37 -04:00
|
|
|
|
2017-09-20 19:31:02 -04:00
|
|
|
shouldExist('lib2_built/class2.ngsummary.json');
|
|
|
|
shouldNotExist('lib2_built/class2.ngsummary.js');
|
|
|
|
shouldNotExist('lib2_built/class2.ngsummary.d.ts');
|
2019-02-12 17:29:28 -05:00
|
|
|
// We don't expect factories here because the "class2.ts" file
|
|
|
|
// just exports a class that does not produce any AOT code.
|
|
|
|
shouldNotExist('lib2_built/class2.ngfactory.js');
|
|
|
|
shouldNotExist('lib2_built/class2.ngfactory.d.ts');
|
2017-09-20 19:31:02 -04:00
|
|
|
|
2017-08-23 16:57:37 -04:00
|
|
|
// app
|
|
|
|
// make `shouldExist` / `shouldNotExist` relative to `built`
|
|
|
|
outDir = path.resolve(basePath, 'built');
|
|
|
|
shouldExist('app/main.js');
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|
2017-09-28 12:39:16 -04:00
|
|
|
|
2018-03-06 17:53:01 -05:00
|
|
|
describe('enableResourceInlining', () => {
|
|
|
|
it('should inline templateUrl and styleUrl in JS and metadata', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["mymodule.ts"],
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"enableResourceInlining": true
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('my.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
|
|
templateUrl: './my.component.html',
|
|
|
|
styleUrls: ['./my.component.css'],
|
|
|
|
})
|
|
|
|
export class MyComp {}
|
|
|
|
`);
|
|
|
|
write('my.component.html', `<h1>Some template content</h1>`);
|
|
|
|
write('my.component.css', `h1 {color: blue}`);
|
|
|
|
write('mymodule.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyComp} from './my.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', basePath]);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
outDir = path.resolve(basePath, 'built');
|
|
|
|
const outputJs = fs.readFileSync(path.join(outDir, 'my.component.js'), {encoding: 'utf-8'});
|
|
|
|
expect(outputJs).not.toContain('templateUrl');
|
|
|
|
expect(outputJs).not.toContain('styleUrls');
|
|
|
|
expect(outputJs).toContain('Some template content');
|
|
|
|
expect(outputJs).toContain('color: blue');
|
|
|
|
|
|
|
|
const outputMetadata =
|
|
|
|
fs.readFileSync(path.join(outDir, 'my.component.metadata.json'), {encoding: 'utf-8'});
|
|
|
|
expect(outputMetadata).not.toContain('templateUrl');
|
|
|
|
expect(outputMetadata).not.toContain('styleUrls');
|
|
|
|
expect(outputMetadata).toContain('Some template content');
|
|
|
|
expect(outputMetadata).toContain('color: blue');
|
|
|
|
});
|
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|
2017-09-12 18:53:17 -04:00
|
|
|
|
2018-03-21 17:22:06 -04:00
|
|
|
|
2017-09-13 19:55:42 -04:00
|
|
|
describe('expression lowering', () => {
|
|
|
|
const shouldExist = (fileName: string) => {
|
|
|
|
if (!fs.existsSync(path.resolve(basePath, fileName))) {
|
|
|
|
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
it('should be able to lower supported expressions', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["module.ts"]
|
|
|
|
}`);
|
|
|
|
write('module.ts', `
|
|
|
|
import {NgModule, InjectionToken} from '@angular/core';
|
|
|
|
import {AppComponent} from './app';
|
|
|
|
|
|
|
|
export interface Info {
|
|
|
|
route: string;
|
|
|
|
data: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const T1 = new InjectionToken<string>('t1');
|
|
|
|
export const T2 = new InjectionToken<string>('t2');
|
|
|
|
export const T3 = new InjectionToken<number>('t3');
|
|
|
|
export const T4 = new InjectionToken<Info[]>('t4');
|
|
|
|
|
|
|
|
enum SomeEnum {
|
|
|
|
OK,
|
|
|
|
Cancel
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateString() {
|
|
|
|
return 'someValue';
|
|
|
|
}
|
|
|
|
|
|
|
|
const routeLikeData = [{
|
|
|
|
route: '/home',
|
|
|
|
data: calculateString()
|
|
|
|
}];
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [AppComponent],
|
|
|
|
providers: [
|
|
|
|
{ provide: T1, useValue: calculateString() },
|
|
|
|
{ provide: T2, useFactory: () => 'someValue' },
|
|
|
|
{ provide: T3, useValue: SomeEnum.OK },
|
|
|
|
{ provide: T4, useValue: routeLikeData }
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
write('app.ts', `
|
|
|
|
import {Component, Inject} from '@angular/core';
|
|
|
|
import * as m from './module';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-app',
|
|
|
|
template: ''
|
|
|
|
})
|
|
|
|
export class AppComponent {
|
|
|
|
constructor(
|
|
|
|
@Inject(m.T1) private t1: string,
|
|
|
|
@Inject(m.T2) private t2: string,
|
|
|
|
@Inject(m.T3) private t3: number,
|
|
|
|
@Inject(m.T4) private t4: m.Info[],
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(main(['-p', basePath], s => {})).toBe(0);
|
|
|
|
shouldExist('built/module.js');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-09-12 18:53:17 -04:00
|
|
|
describe('watch mode', () => {
|
|
|
|
let timer: (() => void)|undefined = undefined;
|
|
|
|
let results: ((message: string) => void)|undefined = undefined;
|
|
|
|
let originalTimeout: number;
|
|
|
|
|
|
|
|
function trigger() {
|
|
|
|
const delay = 1000;
|
|
|
|
setTimeout(() => {
|
|
|
|
const t = timer;
|
|
|
|
timer = undefined;
|
|
|
|
if (!t) {
|
|
|
|
fail('Unexpected state. Timer was not set.');
|
|
|
|
} else {
|
|
|
|
t();
|
|
|
|
}
|
|
|
|
}, delay);
|
|
|
|
}
|
|
|
|
|
|
|
|
function whenResults(): Promise<string> {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
results = message => {
|
|
|
|
resolve(message);
|
|
|
|
results = undefined;
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function errorSpy(message: string): void {
|
|
|
|
if (results) results(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
|
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
|
|
|
const timerToken = 100;
|
2020-01-03 00:28:06 -05:00
|
|
|
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
|
2020-03-30 11:22:25 -04:00
|
|
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
|
2020-01-03 00:28:06 -05:00
|
|
|
spyOn(ts.sys as any, 'setTimeout').and.callFake((callback: () => void) => {
|
2017-09-12 18:53:17 -04:00
|
|
|
timer = callback;
|
|
|
|
return timerToken;
|
|
|
|
});
|
2020-01-03 00:28:06 -05:00
|
|
|
// TODO: @JiaLiPassion, need to wait @types/jasmine to handle optional method case
|
2020-03-30 11:22:25 -04:00
|
|
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
|
2020-01-03 00:28:06 -05:00
|
|
|
spyOn(ts.sys as any, 'clearTimeout').and.callFake((token: number) => {
|
2017-09-12 18:53:17 -04:00
|
|
|
if (token == timerToken) {
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
write('greet.html', `<p class="greeting"> Hello {{name}}!</p>`);
|
|
|
|
write('greet.css', `p.greeting { color: #eee }`);
|
|
|
|
write('greet.ts', `
|
|
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'greet',
|
|
|
|
templateUrl: 'greet.html',
|
|
|
|
styleUrls: ['greet.css']
|
|
|
|
})
|
|
|
|
export class Greet {
|
|
|
|
@Input()
|
|
|
|
name: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
write('app.ts', `
|
|
|
|
import {Component} from '@angular/core'
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'my-app',
|
|
|
|
template: \`
|
|
|
|
<div>
|
|
|
|
<greet [name]='name'></greet>
|
|
|
|
</div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class App {
|
|
|
|
name:string;
|
|
|
|
constructor() {
|
|
|
|
this.name = \`Angular!\`
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
|
|
|
|
write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Greet} from './greet';
|
|
|
|
import {App} from './app';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Greet, App]
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
2020-03-30 11:22:25 -04:00
|
|
|
afterEach(() => {
|
|
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
|
|
|
|
});
|
2017-09-12 18:53:17 -04:00
|
|
|
|
|
|
|
function writeAppConfig(location: string) {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "${location}"
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
function expectRecompile(cb: () => void) {
|
|
|
|
return (done: DoneFn) => {
|
|
|
|
writeAppConfig('dist');
|
|
|
|
const config = readCommandLineAndConfiguration(['-p', basePath]);
|
|
|
|
const compile = watchMode(config.project, config.options, errorSpy);
|
|
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
compile.ready(() => {
|
|
|
|
cb();
|
|
|
|
|
|
|
|
// Allow the watch callbacks to occur and trigger the timer.
|
|
|
|
trigger();
|
|
|
|
|
|
|
|
// Expect the file to trigger a result.
|
|
|
|
whenResults().then(message => {
|
|
|
|
expect(message).toMatch(/File change detected/);
|
|
|
|
compile.close();
|
|
|
|
done();
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should recompile when config file changes', expectRecompile(() => writeAppConfig('dist2')));
|
|
|
|
|
|
|
|
it('should recompile when a ts file changes', expectRecompile(() => {
|
|
|
|
write('greet.ts', `
|
|
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'greet',
|
|
|
|
templateUrl: 'greet.html',
|
|
|
|
styleUrls: ['greet.css'],
|
|
|
|
})
|
|
|
|
export class Greet {
|
|
|
|
@Input()
|
|
|
|
name: string;
|
|
|
|
age: number;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
}));
|
|
|
|
|
2020-03-30 11:22:25 -04:00
|
|
|
it('should recompile when the html file changes', expectRecompile(() => {
|
|
|
|
write('greet.html', '<p> Hello {{name}} again!</p>');
|
|
|
|
}));
|
2017-09-12 18:53:17 -04:00
|
|
|
|
2020-03-30 11:22:25 -04:00
|
|
|
it('should recompile when the css file changes', expectRecompile(() => {
|
|
|
|
write('greet.css', `p.greeting { color: blue }`);
|
|
|
|
}));
|
2017-09-12 18:53:17 -04:00
|
|
|
});
|
2017-10-17 19:10:15 -04:00
|
|
|
|
|
|
|
describe('regressions', () => {
|
2017-12-15 20:30:41 -05:00
|
|
|
//#20479
|
|
|
|
it('should not generate an invalid metadata file', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["lib.ts"],
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"skipTemplateCodegen": true
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
write('src/lib.ts', `
|
|
|
|
export namespace A{
|
|
|
|
export class C1 {
|
|
|
|
}
|
|
|
|
export interface I1{
|
|
|
|
}
|
|
|
|
}`);
|
|
|
|
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
|
|
|
|
shouldNotExist('src/lib.metadata.json');
|
|
|
|
});
|
|
|
|
|
2017-11-09 19:52:19 -05:00
|
|
|
//#19544
|
|
|
|
it('should recognize @NgModule() directive with a redundant @Injectable()', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "../dist",
|
|
|
|
"rootDir": ".",
|
|
|
|
"rootDirs": [
|
|
|
|
".",
|
|
|
|
"../dist"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/test.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<p>hello</p>',
|
|
|
|
})
|
|
|
|
export class TestComponent {}
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {Injectable, NgModule} from '@angular/core';
|
|
|
|
import {TestComponent} from './test.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
@Injectable()
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
const exitCode =
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
|
|
|
expect(exitCode).toBe(0, 'Compile failed unexpectedly.\n ' + messages.join('\n '));
|
|
|
|
});
|
|
|
|
|
2017-10-17 19:10:15 -04:00
|
|
|
// #19765
|
|
|
|
it('should not report an error when the resolved .css file is in outside rootDir', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "../dist",
|
|
|
|
"rootDir": ".",
|
|
|
|
"rootDirs": [
|
|
|
|
".",
|
|
|
|
"../dist"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/lib/test.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<p>hello</p>',
|
|
|
|
styleUrls: ['./test.component.css']
|
|
|
|
})
|
|
|
|
export class TestComponent {}
|
|
|
|
`);
|
|
|
|
write('dist/dummy.txt', ''); // Force dist to be created
|
|
|
|
write('dist/lib/test.component.css', `
|
|
|
|
p { color: blue }
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestComponent} from './lib/test.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
const exitCode =
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
|
|
|
expect(exitCode).toBe(0, 'Compile failed unexpectedly.\n ' + messages.join('\n '));
|
|
|
|
});
|
2017-10-23 21:29:06 -04:00
|
|
|
|
|
|
|
it('should emit all structural errors', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/lib/indirect2.ts', `
|
|
|
|
declare var f: any;
|
|
|
|
export const t2 = f\`<p>hello</p>\`;
|
|
|
|
`);
|
|
|
|
write('src/lib/indirect1.ts', `
|
|
|
|
import {t2} from './indirect2';
|
|
|
|
export const t1 = t2 + ' ';
|
|
|
|
`);
|
|
|
|
write('src/lib/test.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {t1} from './indirect1';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: t1
|
|
|
|
})
|
|
|
|
export class TestComponent {}
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestComponent} from './lib/test.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
const exitCode =
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
2017-10-26 18:24:54 -04:00
|
|
|
expect(exitCode).toBe(1, 'Compile was expected to fail');
|
2017-10-24 07:54:08 -04:00
|
|
|
expect(messages[0]).toContain('Tagged template expressions are not supported in metadata');
|
2017-10-23 21:29:06 -04:00
|
|
|
});
|
2017-10-26 12:45:01 -04:00
|
|
|
|
2017-11-09 17:06:02 -05:00
|
|
|
// Regression: #20076
|
|
|
|
it('should report template error messages', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/lib/test.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '{{thing.?stuff}}'
|
|
|
|
})
|
|
|
|
export class TestComponent {
|
|
|
|
thing: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestComponent} from './lib/test.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
const exitCode =
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
|
|
|
expect(exitCode).toBe(1, 'Compile was expected to fail');
|
|
|
|
expect(messages[0]).toContain('Parser Error: Unexpected token');
|
|
|
|
});
|
|
|
|
|
2017-12-15 19:25:04 -05:00
|
|
|
// Regression test for #19979
|
|
|
|
it('should not stack overflow on a recursive module export', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: 'Hello'
|
|
|
|
})
|
|
|
|
export class MyFaultyComponent {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
exports: [MyFaultyModule],
|
|
|
|
declarations: [MyFaultyComponent],
|
|
|
|
providers: [],
|
|
|
|
})
|
|
|
|
export class MyFaultyModule { }
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
expect(
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)))
|
|
|
|
.toBe(1, 'Compile was expected to fail');
|
|
|
|
expect(messages[0]).toContain(`module 'MyFaultyModule' is exported recursively`);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Regression test for #19979
|
|
|
|
it('should not stack overflow on a recursive module import', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {Component, NgModule, forwardRef} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: 'Hello'
|
|
|
|
})
|
|
|
|
export class MyFaultyComponent {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [forwardRef(() => MyFaultyModule)]
|
|
|
|
})
|
|
|
|
export class MyFaultyImport {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
imports: [MyFaultyImport],
|
|
|
|
declarations: [MyFaultyComponent]
|
|
|
|
})
|
|
|
|
export class MyFaultyModule { }
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
expect(
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)))
|
|
|
|
.toBe(1, 'Compile was expected to fail');
|
|
|
|
expect(messages[0]).toContain(`is imported recursively by the module 'MyFaultyImport`);
|
|
|
|
});
|
|
|
|
|
2018-01-22 14:30:58 -05:00
|
|
|
// Regression test for #21273
|
|
|
|
it('should not report errors for unknown property annotations', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
|
|
|
|
write('src/test-decorator.ts', `
|
|
|
|
export function Convert(p: any): any {
|
|
|
|
// Make sur this doesn't look like a macro function
|
|
|
|
var r = p;
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {Component, Input, NgModule} from '@angular/core';
|
|
|
|
import {Convert} from './test-decorator';
|
|
|
|
|
|
|
|
@Component({template: '{{name}}'})
|
|
|
|
export class TestComponent {
|
|
|
|
@Input() @Convert(convert) name: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
function convert(n: any) { return n; }
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
expect(
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)))
|
|
|
|
.toBe(0, `Compile failed:\n ${messages.join('\n ')}`);
|
|
|
|
});
|
|
|
|
|
2017-10-26 12:45:01 -04:00
|
|
|
it('should allow using 2 classes with the same name in declarations with noEmitOnError=true',
|
|
|
|
() => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"noEmitOnError": true
|
|
|
|
},
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
function writeComp(fileName: string) {
|
|
|
|
write(fileName, `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'comp', template: ''})
|
|
|
|
export class TestComponent {}
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
writeComp('src/comp1.ts');
|
|
|
|
writeComp('src/comp2.ts');
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestComponent as Comp1} from './comp1';
|
|
|
|
import {TestComponent as Comp2} from './comp2';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Comp1, Comp2],
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
|
|
|
|
});
|
2017-12-15 17:51:42 -05:00
|
|
|
|
|
|
|
it('should not type check a .js files from node_modules with allowJs', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"noEmitOnError": true,
|
|
|
|
"allowJs": true,
|
|
|
|
"declaration": false
|
|
|
|
},
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import 'my-library';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: 'hello'
|
|
|
|
})
|
|
|
|
export class HelloCmp {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [HelloCmp],
|
|
|
|
})
|
|
|
|
export class MyModule {}
|
|
|
|
`);
|
|
|
|
write('src/node_modules/t.txt', ``);
|
|
|
|
write('src/node_modules/my-library/index.js', `
|
|
|
|
export someVar = 1;
|
|
|
|
export someOtherVar = undefined + 1;
|
|
|
|
`);
|
|
|
|
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
|
|
|
|
});
|
2017-10-17 19:10:15 -04:00
|
|
|
});
|
2017-11-14 20:49:47 -05:00
|
|
|
|
|
|
|
describe('formatted messages', () => {
|
|
|
|
it('should emit a formatted error message for a structural error', () => {
|
|
|
|
write('src/tsconfig.json', `{
|
|
|
|
"extends": "../tsconfig-base.json",
|
|
|
|
"files": ["test-module.ts"]
|
|
|
|
}`);
|
|
|
|
write('src/lib/indirect2.ts', `
|
|
|
|
declare var f: any;
|
|
|
|
|
|
|
|
export const t2 = f\`<p>hello</p>\`;
|
|
|
|
`);
|
|
|
|
write('src/lib/indirect1.ts', `
|
|
|
|
import {t2} from './indirect2';
|
|
|
|
export const t1 = t2 + ' ';
|
|
|
|
`);
|
|
|
|
write('src/lib/test.component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {t1} from './indirect1';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: t1,
|
|
|
|
styleUrls: ['./test.component.css']
|
|
|
|
})
|
|
|
|
export class TestComponent {}
|
|
|
|
`);
|
|
|
|
write('src/test-module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestComponent} from './lib/test.component';
|
|
|
|
|
|
|
|
@NgModule({declarations: [TestComponent]})
|
|
|
|
export class TestModule {}
|
|
|
|
`);
|
|
|
|
const messages: string[] = [];
|
|
|
|
const exitCode =
|
|
|
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
|
|
|
expect(exitCode).toBe(1, 'Compile was expected to fail');
|
2019-06-06 15:22:32 -04:00
|
|
|
const srcPathWithSep = `lib/`;
|
2017-11-14 20:49:47 -05:00
|
|
|
expect(messages[0])
|
2020-03-30 11:22:25 -04:00
|
|
|
.toEqual(`${
|
|
|
|
srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent'
|
2017-11-14 20:49:47 -05:00
|
|
|
Tagged template expressions are not supported in metadata in 't1'
|
2019-04-26 09:29:27 -04:00
|
|
|
't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27)
|
|
|
|
't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27).
|
2017-11-14 20:49:47 -05:00
|
|
|
`);
|
|
|
|
});
|
|
|
|
});
|
2017-11-20 13:21:17 -05:00
|
|
|
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
describe('tree shakeable services', () => {
|
|
|
|
function compileService(source: string): string {
|
|
|
|
write('service.ts', source);
|
|
|
|
|
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
|
|
|
|
const servicePath = path.resolve(outDir, 'service.js');
|
|
|
|
return fs.readFileSync(servicePath, 'utf8');
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["service.ts"]
|
|
|
|
}`);
|
|
|
|
write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe(`doesn't break existing injectables`, () => {
|
|
|
|
it('on simple services', () => {
|
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable, NgModule} from '@angular/core';
|
2018-03-06 17:53:01 -05:00
|
|
|
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
@Injectable()
|
|
|
|
export class Service {
|
|
|
|
constructor(public param: string) {}
|
|
|
|
}
|
2018-03-06 17:53:01 -05:00
|
|
|
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
@NgModule({
|
|
|
|
providers: [{provide: Service, useValue: new Service('test')}],
|
|
|
|
})
|
|
|
|
export class ServiceModule {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).not.toMatch(/ɵprov/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
it('on a service with a base class service', () => {
|
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable, NgModule} from '@angular/core';
|
2018-03-06 17:53:01 -05:00
|
|
|
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
@Injectable()
|
|
|
|
export class Dep {}
|
|
|
|
|
|
|
|
export class Base {
|
|
|
|
constructor(private dep: Dep) {}
|
|
|
|
}
|
|
|
|
@Injectable()
|
|
|
|
export class Service extends Base {}
|
2018-03-06 17:53:01 -05:00
|
|
|
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
@NgModule({
|
|
|
|
providers: [Service],
|
|
|
|
})
|
|
|
|
export class ServiceModule {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).not.toMatch(/ɵprov/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-04-14 02:02:29 -04:00
|
|
|
it('compiles a basic InjectableDef', () => {
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
})
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov = .+\.ɵɵdefineInjectable\(/);
|
|
|
|
expect(source).toMatch(/ɵprov.*token: Service/);
|
|
|
|
expect(source).toMatch(/ɵprov.*providedIn: .+\.Module/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
|
2019-10-15 15:41:30 -04:00
|
|
|
it('ɵprov in es5 mode is annotated @nocollapse when closure options are enabled', () => {
|
|
|
|
writeConfig(`{
|
2018-03-09 13:16:09 -05:00
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"annotateForClosureCompiler": true
|
|
|
|
},
|
|
|
|
"files": ["service.ts"]
|
|
|
|
}`);
|
2019-10-15 15:41:30 -04:00
|
|
|
const source = compileService(`
|
2018-03-09 13:16:09 -05:00
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
2018-03-09 13:16:09 -05:00
|
|
|
})
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/\/\*\* @nocollapse \*\/ Service\.ɵprov =/);
|
|
|
|
});
|
2018-03-09 13:16:09 -05:00
|
|
|
|
2018-04-14 02:02:29 -04:00
|
|
|
it('compiles a useValue InjectableDef', () => {
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
export const CONST_SERVICE: Service = null;
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
useValue: CONST_SERVICE
|
|
|
|
})
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov.*return CONST_SERVICE/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
|
2018-04-14 02:02:29 -04:00
|
|
|
it('compiles a useExisting InjectableDef', () => {
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class Existing {}
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
useExisting: Existing,
|
|
|
|
})
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov.*return ..\.ɵɵinject\(Existing\)/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
|
2018-04-14 02:02:29 -04:00
|
|
|
it('compiles a useFactory InjectableDef with optional dep', () => {
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable, Optional} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class Existing {}
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
useFactory: (existing: Existing|null) => new Service(existing),
|
|
|
|
deps: [[new Optional(), Existing]],
|
|
|
|
})
|
|
|
|
export class Service {
|
|
|
|
constructor(e: Existing|null) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov.*return ..\(..\.ɵɵinject\(Existing, 8\)/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
|
|
|
|
2018-04-14 02:02:29 -04:00
|
|
|
it('compiles a useFactory InjectableDef with skip-self dep', () => {
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
const source = compileService(`
|
|
|
|
import {Injectable, SkipSelf} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class Existing {}
|
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
useFactory: (existing: Existing) => new Service(existing),
|
|
|
|
deps: [[new SkipSelf(), Existing]],
|
|
|
|
})
|
|
|
|
export class Service {
|
|
|
|
constructor(e: Existing) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov.*return ..\(..\.ɵɵinject\(Existing, 4\)/);
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
2018-02-13 15:51:21 -05:00
|
|
|
|
|
|
|
it('compiles a service that depends on a token', () => {
|
|
|
|
const source = compileService(`
|
|
|
|
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
|
|
|
import {Module} from './module';
|
|
|
|
|
2018-03-07 18:10:38 -05:00
|
|
|
export const TOKEN = new InjectionToken('desc', {providedIn: Module, factory: () => true});
|
2018-02-13 15:51:21 -05:00
|
|
|
|
|
|
|
@Injectable({
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: Module,
|
2018-02-13 15:51:21 -05:00
|
|
|
})
|
|
|
|
export class Service {
|
|
|
|
constructor(@Inject(TOKEN) value: boolean) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-10-15 15:41:30 -04:00
|
|
|
expect(source).toMatch(/ɵprov = .+\.ɵɵdefineInjectable\(/);
|
|
|
|
expect(source).toMatch(/ɵprov.*token: Service/);
|
|
|
|
expect(source).toMatch(/ɵprov.*providedIn: .+\.Module/);
|
2018-02-13 15:51:21 -05:00
|
|
|
});
|
2018-03-02 18:02:06 -05:00
|
|
|
|
|
|
|
it('generates exports.* references when outputting commonjs', () => {
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"compilerOptions": {
|
|
|
|
"module": "commonjs"
|
|
|
|
},
|
|
|
|
"files": ["service.ts"]
|
|
|
|
}`);
|
|
|
|
const source = compileService(`
|
2018-03-07 18:10:38 -05:00
|
|
|
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
2018-03-02 18:02:06 -05:00
|
|
|
import {Module} from './module';
|
|
|
|
|
|
|
|
export const TOKEN = new InjectionToken<string>('test token', {
|
2018-03-07 18:10:38 -05:00
|
|
|
providedIn: 'root',
|
2018-03-02 18:02:06 -05:00
|
|
|
factory: () => 'this is a test',
|
|
|
|
});
|
|
|
|
|
2018-03-07 18:10:38 -05:00
|
|
|
@Injectable({providedIn: 'root'})
|
2018-03-02 18:02:06 -05:00
|
|
|
export class Service {
|
|
|
|
constructor(@Inject(TOKEN) token: any) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-05-17 21:49:21 -04:00
|
|
|
expect(source).toMatch(/new Service\(i0\.ɵɵinject\(exports\.TOKEN\)\);/);
|
2018-03-02 18:02:06 -05:00
|
|
|
});
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
});
|
2018-02-16 11:45:21 -05:00
|
|
|
|
2018-04-09 16:52:50 -04:00
|
|
|
it('libraries should not break strictMetadataEmit', () => {
|
|
|
|
// first only generate .d.ts / .js / .metadata.json files
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"angularCompilerOptions": {
|
|
|
|
"skipTemplateCodegen": true,
|
|
|
|
"strictMetadataEmit": true,
|
|
|
|
"fullTemplateTypeCheck": true
|
|
|
|
},
|
|
|
|
"compilerOptions": {
|
|
|
|
"outDir": "lib"
|
|
|
|
},
|
|
|
|
"files": ["main.ts", "test.d.ts"]
|
|
|
|
}`);
|
|
|
|
write('main.ts', `
|
|
|
|
import {Test} from './test';
|
|
|
|
export const bar = Test.bar;
|
|
|
|
`);
|
|
|
|
write('test.d.ts', `
|
|
|
|
declare export class Test {
|
|
|
|
static bar: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
let exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
});
|
feat(compiler): allow selector-less directives as base classes (#31379)
In Angular today, the following pattern works:
```typescript
export class BaseDir {
constructor(@Inject(ViewContainerRef) protected vcr: ViewContainerRef) {}
}
@Directive({
selector: '[child]',
})
export class ChildDir extends BaseDir {
// constructor inherited from BaseDir
}
```
A decorated child class can inherit a constructor from an undecorated base
class, so long as the base class has metadata of its own (for JIT mode).
This pattern works regardless of metadata in AOT.
In Angular Ivy, this pattern does not work: without the @Directive
annotation identifying the base class as a directive, information about its
constructor parameters will not be captured by the Ivy compiler. This is a
result of Ivy's locality principle, which is the basis behind a number of
compilation optimizations.
As a solution, @Directive() without a selector will be interpreted as a
"directive base class" annotation. Such a directive cannot be declared in an
NgModule, but can be inherited from. To implement this, a few changes are
made to the ngc compiler:
* the error for a selector-less directive is now generated when an NgModule
declaring it is processed, not when the directive itself is processed.
* selector-less directives are not tracked along with other directives in
the compiler, preventing other errors (like their absence in an NgModule)
from being generated from them.
PR Close #31379
2019-07-01 19:04:58 -04:00
|
|
|
|
|
|
|
describe('base directives', () => {
|
|
|
|
it('should allow directives with no selector that are not in NgModules', () => {
|
|
|
|
// first only generate .d.ts / .js / .metadata.json files
|
|
|
|
writeConfig(`{
|
|
|
|
"extends": "./tsconfig-base.json",
|
|
|
|
"files": ["main.ts"]
|
|
|
|
}`);
|
|
|
|
write('main.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({})
|
|
|
|
export class BaseDir {}
|
|
|
|
|
|
|
|
@Directive({})
|
|
|
|
export abstract class AbstractBaseDir {}
|
|
|
|
|
|
|
|
@Directive()
|
|
|
|
export abstract class EmptyDir {}
|
|
|
|
`);
|
|
|
|
let exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
|
|
expect(exitCode).toEqual(0);
|
|
|
|
});
|
2019-10-23 06:20:07 -04:00
|
|
|
|
|
|
|
it('should be able to use abstract directive in other compilation units', () => {
|
|
|
|
writeConfig();
|
|
|
|
write('lib1/tsconfig.json', JSON.stringify({
|
|
|
|
extends: '../tsconfig-base.json',
|
|
|
|
compilerOptions: {rootDir: '.', outDir: '../node_modules/lib1_built'},
|
|
|
|
}));
|
|
|
|
write('lib1/index.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
2020-01-03 00:28:06 -05:00
|
|
|
|
2019-10-23 06:20:07 -04:00
|
|
|
@Directive()
|
|
|
|
export class BaseClass {}
|
|
|
|
`);
|
|
|
|
write('index.ts', `
|
|
|
|
import {NgModule, Directive} from '@angular/core';
|
|
|
|
import {BaseClass} from 'lib1_built';
|
2020-01-03 00:28:06 -05:00
|
|
|
|
2019-10-23 06:20:07 -04:00
|
|
|
@Directive({selector: 'my-dir'})
|
|
|
|
export class MyDirective extends BaseClass {}
|
2020-01-03 00:28:06 -05:00
|
|
|
|
2019-10-23 06:20:07 -04:00
|
|
|
@NgModule({declarations: [MyDirective]})
|
|
|
|
export class AppModule {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(main(['-p', path.join(basePath, 'lib1/tsconfig.json')], errorSpy)).toBe(0);
|
|
|
|
expect(main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0);
|
|
|
|
});
|
feat(compiler): allow selector-less directives as base classes (#31379)
In Angular today, the following pattern works:
```typescript
export class BaseDir {
constructor(@Inject(ViewContainerRef) protected vcr: ViewContainerRef) {}
}
@Directive({
selector: '[child]',
})
export class ChildDir extends BaseDir {
// constructor inherited from BaseDir
}
```
A decorated child class can inherit a constructor from an undecorated base
class, so long as the base class has metadata of its own (for JIT mode).
This pattern works regardless of metadata in AOT.
In Angular Ivy, this pattern does not work: without the @Directive
annotation identifying the base class as a directive, information about its
constructor parameters will not be captured by the Ivy compiler. This is a
result of Ivy's locality principle, which is the basis behind a number of
compilation optimizations.
As a solution, @Directive() without a selector will be interpreted as a
"directive base class" annotation. Such a directive cannot be declared in an
NgModule, but can be inherited from. To implement this, a few changes are
made to the ngc compiler:
* the error for a selector-less directive is now generated when an NgModule
declaring it is processed, not when the directive itself is processed.
* selector-less directives are not tracked along with other directives in
the compiler, preventing other errors (like their absence in an NgModule)
from being generated from them.
PR Close #31379
2019-07-01 19:04:58 -04:00
|
|
|
});
|
2017-06-09 17:50:57 -04:00
|
|
|
});
|