Skip to content

Commit eecb54b

Browse files
committed
wip: actual feature
1 parent 33ef2de commit eecb54b

27 files changed

+1096
-176
lines changed

packages/compiler-cli/src/transformers/program.ts

+25-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* found in the LICENSE file at https://angular.io/license
88
*/
99

10-
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
10+
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
1111
import * as fs from 'fs';
1212
import * as path from 'path';
1313
import * as ts from 'typescript';
@@ -22,7 +22,7 @@ import {MetadataCache, MetadataTransformer} from './metadata_cache';
2222
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
2323
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
2424
import {getAngularClassTransformerFactory} from './r3_transform';
25-
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
25+
import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
2626

2727

2828

@@ -62,6 +62,7 @@ class AngularCompilerProgram implements Program {
6262
private _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter;
6363
private _tsProgram: ts.Program;
6464
private _analyzedModules: NgAnalyzedModules|undefined;
65+
private _analyzedInjectables: NgAnalyzedFileWithInjectables[]|undefined;
6566
private _structuralDiagnostics: Diagnostic[]|undefined;
6667
private _programWithStubs: ts.Program|undefined;
6768
private _optionsDiagnostics: Diagnostic[] = [];
@@ -191,12 +192,12 @@ class AngularCompilerProgram implements Program {
191192
}
192193
return Promise.resolve()
193194
.then(() => {
194-
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
195+
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
195196
return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => {
196197
if (this._analyzedModules) {
197198
throw new Error('Angular structure loaded both synchronously and asynchronously');
198199
}
199-
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
200+
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, [], rootNames);
200201
});
201202
})
202203
.catch(e => this._createProgramOnError(e));
@@ -304,8 +305,12 @@ class AngularCompilerProgram implements Program {
304305
}
305306
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
306307
};
307-
const tsCustomTransformers = this.calculateTransforms(
308-
genFileByFileName, /* partialModules */ undefined, customTransformers);
308+
309+
const modules = this._analyzedInjectables &&
310+
this.compiler.emitAllPartialModules2(this._analyzedInjectables);
311+
312+
const tsCustomTransformers =
313+
this.calculateTransforms(genFileByFileName, modules, customTransformers);
309314
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
310315
// Restore the original references before we emit so TypeScript doesn't emit
311316
// a reference to the .d.ts file.
@@ -491,9 +496,11 @@ class AngularCompilerProgram implements Program {
491496
return;
492497
}
493498
try {
494-
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
495-
const analyzedModules = this.compiler.loadFilesSync(sourceFiles);
496-
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
499+
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
500+
const {analyzedModules, analyzedInjectables} =
501+
this.compiler.loadFilesSync(sourceFiles, tsFiles);
502+
this._updateProgramWithTypeCheckStubs(
503+
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
497504
} catch (e) {
498505
this._createProgramOnError(e);
499506
}
@@ -520,6 +527,7 @@ class AngularCompilerProgram implements Program {
520527
tmpProgram: ts.Program,
521528
rootNames: string[],
522529
sourceFiles: string[],
530+
tsFiles: string[],
523531
} {
524532
if (this._analyzedModules) {
525533
throw new Error(`Internal Error: already initialized!`);
@@ -553,17 +561,23 @@ class AngularCompilerProgram implements Program {
553561

554562
const tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram);
555563
const sourceFiles: string[] = [];
564+
const tsFiles: string[] = [];
556565
tmpProgram.getSourceFiles().forEach(sf => {
557566
if (this.hostAdapter.isSourceFile(sf.fileName)) {
558567
sourceFiles.push(sf.fileName);
559568
}
569+
if (TS.test(sf.fileName) && !DTS.test(sf.fileName)) {
570+
tsFiles.push(sf.fileName);
571+
}
560572
});
561-
return {tmpProgram, sourceFiles, rootNames};
573+
return {tmpProgram, sourceFiles, tsFiles, rootNames};
562574
}
563575

564576
private _updateProgramWithTypeCheckStubs(
565-
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, rootNames: string[]) {
577+
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
578+
analyzedInjectables: NgAnalyzedFileWithInjectables[], rootNames: string[]) {
566579
this._analyzedModules = analyzedModules;
580+
this._analyzedInjectables = analyzedInjectables;
567581
tmpProgram.getSourceFiles().forEach(sf => {
568582
if (sf.fileName.endsWith('.ngfactory.ts')) {
569583
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);

packages/compiler-cli/src/transformers/util.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {CompilerOptions, DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api';
1414

1515
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
1616
export const DTS = /\.d\.ts$/;
17+
export const TS = /\.ts$/;
1718

1819
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}
1920

packages/compiler-cli/test/ngc_spec.ts

+153
Original file line numberDiff line numberDiff line change
@@ -1942,4 +1942,157 @@ describe('ngc transformer command-line', () => {
19421942
expect(emittedFile('hello-world.js')).toContain('ngComponentDef');
19431943
});
19441944
});
1945+
1946+
fdescribe('tree shakeable services', () => {
1947+
1948+
function compileService(source: string): string {
1949+
write('service.ts', source);
1950+
1951+
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
1952+
expect(exitCode).toEqual(0);
1953+
1954+
const servicePath = path.resolve(outDir, 'service.js');
1955+
return fs.readFileSync(servicePath, 'utf8');
1956+
}
1957+
1958+
beforeEach(() => {
1959+
writeConfig(`{
1960+
"extends": "./tsconfig-base.json",
1961+
"files": ["service.ts"]
1962+
}`);
1963+
write('module.ts', `
1964+
import {NgModule} from '@angular/core';
1965+
1966+
@NgModule({})
1967+
export class Module {}
1968+
`);
1969+
});
1970+
1971+
describe('doesn\'t break existing injectables', () => {
1972+
it('on simple services', () => {
1973+
const source = compileService(`
1974+
import {Injectable, NgModule} from '@angular/core';
1975+
1976+
@Injectable()
1977+
export class Service {
1978+
constructor(public param: string) {}
1979+
}
1980+
1981+
@NgModule({
1982+
providers: [{provide: Service, useValue: new Service('test')}],
1983+
})
1984+
export class ServiceModule {}
1985+
`);
1986+
expect(source).not.toMatch(/ngInjectableDef/);
1987+
});
1988+
it('on a service with a base class service', () => {
1989+
const source = compileService(`
1990+
import {Injectable, NgModule} from '@angular/core';
1991+
1992+
@Injectable()
1993+
export class Dep {}
1994+
1995+
export class Base {
1996+
constructor(private dep: Dep) {}
1997+
}
1998+
@Injectable()
1999+
export class Service extends Base {}
2000+
2001+
@NgModule({
2002+
providers: [Service],
2003+
})
2004+
export class ServiceModule {}
2005+
`);
2006+
expect(source).not.toMatch(/ngInjectableDef/);
2007+
});
2008+
});
2009+
2010+
it('compiles a basic InjectableDef', () => {
2011+
const source = compileService(`
2012+
import {Injectable} from '@angular/core';
2013+
import {Module} from './module';
2014+
2015+
@Injectable({
2016+
scope: Module,
2017+
})
2018+
export class Service {}
2019+
`);
2020+
expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/);
2021+
expect(source).toMatch(/ngInjectableDef.*token: Service/);
2022+
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
2023+
});
2024+
2025+
it('compiles a useValue InjectableDef', () => {
2026+
const source = compileService(`
2027+
import {Injectable} from '@angular/core';
2028+
import {Module} from './module';
2029+
2030+
export const CONST_SERVICE: Service = null;
2031+
2032+
@Injectable({
2033+
scope: Module,
2034+
useValue: CONST_SERVICE
2035+
})
2036+
export class Service {}
2037+
`);
2038+
expect(source).toMatch(/ngInjectableDef.*return CONST_SERVICE/);
2039+
});
2040+
2041+
it('compiles a useExisting InjectableDef', () => {
2042+
const source = compileService(`
2043+
import {Injectable} from '@angular/core';
2044+
import {Module} from './module';
2045+
2046+
@Injectable()
2047+
export class Existing {}
2048+
2049+
@Injectable({
2050+
scope: Module,
2051+
useExisting: Existing,
2052+
})
2053+
export class Service {}
2054+
`);
2055+
expect(source).toMatch(/ngInjectableDef.*return ..\.inject\(Existing\)/);
2056+
});
2057+
2058+
it('compiles a useFactory InjectableDef with optional dep', () => {
2059+
const source = compileService(`
2060+
import {Injectable, Optional} from '@angular/core';
2061+
import {Module} from './module';
2062+
2063+
@Injectable()
2064+
export class Existing {}
2065+
2066+
@Injectable({
2067+
scope: Module,
2068+
useFactory: (existing: Existing|null) => new Service(existing),
2069+
deps: [[new Optional(), Existing]],
2070+
})
2071+
export class Service {
2072+
constructor(e: Existing|null) {}
2073+
}
2074+
`);
2075+
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, null, 0\)/);
2076+
});
2077+
2078+
it('compiles a useFactory InjectableDef with skip-self dep', () => {
2079+
const source = compileService(`
2080+
import {Injectable, SkipSelf} from '@angular/core';
2081+
import {Module} from './module';
2082+
2083+
@Injectable()
2084+
export class Existing {}
2085+
2086+
@Injectable({
2087+
scope: Module,
2088+
useFactory: (existing: Existing) => new Service(existing),
2089+
deps: [[new SkipSelf(), Existing]],
2090+
})
2091+
export class Service {
2092+
constructor(e: Existing) {}
2093+
}
2094+
`);
2095+
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, undefined, 1\)/);
2096+
});
2097+
});
19452098
});

0 commit comments

Comments
 (0)