mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
As outlined in the previous commit which enabled the `esModuleInterop` TypeScript compiler option, we need to update all namespace imports for `typescript` to default imports. This is needed to allow for TypeScript to be imported at runtime from an ES module. Similar changes are needed for modules like `semver` where the types incorrectly suggest named exports that will not exist at runtime when imported from ESM. This commit refactors all imports to match with the lint rule we have configured in the previous commit. See the previous commit for more details on why certain imports have been changed. A special case are the imports to `@babel/core` and `@babel/types`. For these a special interop is needed as both default imports, or named imports break the other module format. e.g default imports would work well for ESM, but it breaks for CJS. For CJS, the named imports would only work, but in ESM, only the default export exist. We work around this for now until the devmode is using ESM as well (which would be consistent with prodmode and gives us more valuable test results). More details on the interop can be found in the `babel_core.ts` files (two interops are needed for both localize/or the compiler-cli). PR Close #43431
791 lines
32 KiB
TypeScript
791 lines
32 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
/// <reference types="node" />
|
|
import * as ng from '@angular/compiler-cli';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import ts from 'typescript';
|
|
|
|
import {formatDiagnostics} from '../../src/perform_compile';
|
|
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
|
|
import {createSrcToOutPathMapper, resetTempProgramHandlerForTest, setTempProgramHandlerForTest} from '../../src/transformers/program';
|
|
import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
|
import {expectNoDiagnosticsInProgram, setup, stripAnsi, TestSupport} from '../test_support';
|
|
|
|
describe('ng program', () => {
|
|
let testSupport: TestSupport;
|
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
|
|
|
beforeEach(() => {
|
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
|
testSupport = setup();
|
|
});
|
|
|
|
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
|
const templateEntry =
|
|
template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``;
|
|
return `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({selector: '${prefix}', ${templateEntry}})
|
|
export class ${prefix}Comp {}
|
|
|
|
@NgModule({declarations: [${prefix}Comp]})
|
|
export class ${prefix}Module {}
|
|
`;
|
|
}
|
|
|
|
function compileLib(libName: string) {
|
|
testSupport.writeFiles({
|
|
[`${libName}_src/index.ts`]: createModuleAndCompSource(libName),
|
|
});
|
|
const options = testSupport.createCompilerOptions();
|
|
const program = ng.createProgram({
|
|
rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)],
|
|
options,
|
|
host: ng.createCompilerHost({options}),
|
|
});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
fs.symlinkSync(
|
|
path.resolve(testSupport.basePath, 'built', `${libName}_src`),
|
|
path.resolve(testSupport.basePath, 'node_modules', libName), 'dir');
|
|
program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata});
|
|
}
|
|
|
|
function compile(
|
|
oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, rootNames?: string[],
|
|
host?: CompilerHost): {program: ng.Program, emitResult: ts.EmitResult} {
|
|
const options = testSupport.createCompilerOptions(overrideOptions);
|
|
if (!rootNames) {
|
|
rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')];
|
|
}
|
|
if (!host) {
|
|
host = ng.createCompilerHost({options});
|
|
}
|
|
const program = ng.createProgram({
|
|
rootNames: rootNames,
|
|
options,
|
|
host,
|
|
oldProgram,
|
|
});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
const emitResult = program.emit();
|
|
return {emitResult, program};
|
|
}
|
|
|
|
function createWatchModeHost(): ng.CompilerHost {
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
|
|
const originalGetSourceFile = host.getSourceFile;
|
|
const cache = new Map<string, ts.SourceFile>();
|
|
host.getSourceFile = function(fileName: string, languageVersion: ts.ScriptTarget):
|
|
ts.SourceFile|
|
|
undefined {
|
|
const sf = originalGetSourceFile.call(host, fileName, languageVersion);
|
|
if (sf) {
|
|
if (cache.has(sf.fileName)) {
|
|
const oldSf = cache.get(sf.fileName)!;
|
|
if (oldSf.getFullText() === sf.getFullText()) {
|
|
return oldSf;
|
|
}
|
|
}
|
|
cache.set(sf.fileName, sf);
|
|
}
|
|
return sf;
|
|
};
|
|
return host;
|
|
}
|
|
|
|
function resolveFiles(rootNames: string[]) {
|
|
const preOptions = testSupport.createCompilerOptions();
|
|
const preHost = ts.createCompilerHost(preOptions);
|
|
// don't resolve symlinks
|
|
preHost.realpath = (f) => f;
|
|
const preProgram = ts.createProgram(rootNames, preOptions, preHost);
|
|
return preProgram.getSourceFiles().map(sf => sf.fileName);
|
|
}
|
|
|
|
describe('reuse of old program', () => {
|
|
it('should reuse generated code for libraries from old programs', () => {
|
|
compileLib('lib');
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile().program;
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
const p2 = compile(p1).program;
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
|
|
// import a library for which we didn't generate code before
|
|
compileLib('lib2');
|
|
testSupport.writeFiles({
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
export * from 'lib2/index';
|
|
`,
|
|
});
|
|
const p3 = compile(p2).program;
|
|
expect(p3.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p3.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
|
|
const p4 = compile(p3).program;
|
|
expect(p4.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p4.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
// Note: this is the case for watch mode with declaration:false
|
|
it('should reuse generated code from libraries from old programs with declaration:false',
|
|
() => {
|
|
compileLib('lib');
|
|
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile(undefined, {declaration: false}).program;
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
const p2 = compile(p1, {declaration: false}).program;
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('should only emit changed files', () => {
|
|
testSupport.writeFiles({
|
|
'src/index.ts': createModuleAndCompSource('comp', 'index.html'),
|
|
'src/index.html': `Start`
|
|
});
|
|
const options: ng.CompilerOptions = {declaration: false};
|
|
const host = ng.createCompilerHost({options});
|
|
const originalGetSourceFile = host.getSourceFile;
|
|
const fileCache = new Map<string, ts.SourceFile>();
|
|
host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget) => {
|
|
if (fileCache.has(fileName)) {
|
|
return fileCache.get(fileName);
|
|
}
|
|
const sf = originalGetSourceFile.call(host, fileName, languageVersion);
|
|
if (sf !== undefined) {
|
|
fileCache.set(fileName, sf);
|
|
}
|
|
return sf;
|
|
};
|
|
|
|
const written = new Map<string, string>();
|
|
host.writeFile = (fileName: string, data: string) => written.set(fileName, data);
|
|
|
|
// compile libraries
|
|
const p1 = compile(undefined, options, undefined, host).program;
|
|
|
|
// compile without libraries
|
|
const p2 = compile(p1, options, undefined, host).program;
|
|
expect(written.has(path.posix.join(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
|
let ngFactoryContent =
|
|
written.get(path.posix.join(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
|
expect(ngFactoryContent).toMatch(/Start/);
|
|
|
|
// no change -> no emit
|
|
written.clear();
|
|
const p3 = compile(p2, options, undefined, host).program;
|
|
expect(written.size).toBe(0);
|
|
|
|
// change a user file
|
|
written.clear();
|
|
fileCache.delete(path.posix.join(testSupport.basePath, 'src/index.ts'));
|
|
const p4 = compile(p3, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
expect(written.has(path.posix.join(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
|
|
|
// change a file that is input to generated files
|
|
written.clear();
|
|
testSupport.writeFiles({'src/index.html': 'Hello'});
|
|
const p5 = compile(p4, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
ngFactoryContent =
|
|
written.get(path.posix.join(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
|
expect(ngFactoryContent).toMatch(/Hello/);
|
|
|
|
// change a file and create an intermediate program that is not emitted
|
|
written.clear();
|
|
fileCache.delete(path.posix.join(testSupport.basePath, 'src/index.ts'));
|
|
const p6 = ng.createProgram({
|
|
rootNames: [path.posix.join(testSupport.basePath, 'src/index.ts')],
|
|
options: testSupport.createCompilerOptions(options),
|
|
host,
|
|
oldProgram: p5
|
|
});
|
|
const p7 = compile(p6, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
});
|
|
|
|
it('should set emitSkipped to false for full and incremental emit', () => {
|
|
testSupport.writeFiles({
|
|
'src/index.ts': createModuleAndCompSource('main'),
|
|
});
|
|
const {emitResult: emitResult1, program: p1} = compile();
|
|
expect(emitResult1.emitSkipped).toBe(false);
|
|
const {emitResult: emitResult2, program: p2} = compile(p1);
|
|
expect(emitResult2.emitSkipped).toBe(false);
|
|
const {emitResult: emitResult3, program: p3} = compile(p2);
|
|
expect(emitResult3.emitSkipped).toBe(false);
|
|
});
|
|
|
|
it('should store library summaries on emit', () => {
|
|
compileLib('lib');
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile().program;
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /src\/main.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
describe(
|
|
'verify that program structure is reused within tsc in order to speed up incremental compilation',
|
|
() => {
|
|
afterEach(resetTempProgramHandlerForTest);
|
|
|
|
function captureStructureReuse(compile: () => void): StructureIsReused|null {
|
|
let structureReuse: StructureIsReused|null = null;
|
|
setTempProgramHandlerForTest(program => {
|
|
structureReuse = tsStructureIsReused(program);
|
|
});
|
|
compile();
|
|
return structureReuse;
|
|
}
|
|
|
|
it('should reuse the old ts program completely if nothing changed', () => {
|
|
testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')});
|
|
const host = createWatchModeHost();
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile(undefined, undefined, undefined, host).program;
|
|
const p2 = compile(p1, undefined, undefined, host).program;
|
|
const structureReuse =
|
|
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
|
expect(structureReuse).toBe(StructureIsReused.Completely);
|
|
});
|
|
|
|
it('should reuse the old ts program completely if a template or a ts file changed',
|
|
() => {
|
|
const host = createWatchModeHost();
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
|
'src/main.html': `Some template`,
|
|
'src/util.ts': `export const x = 1`,
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from './util';
|
|
`
|
|
});
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile(undefined, undefined, undefined, host).program;
|
|
const p2 = compile(p1, undefined, undefined, host).program;
|
|
testSupport.writeFiles({
|
|
'src/main.html': `Another template`,
|
|
'src/util.ts': `export const x = 2`,
|
|
});
|
|
const structureReuse =
|
|
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
|
expect(structureReuse).toBe(StructureIsReused.Completely);
|
|
});
|
|
|
|
it('should not reuse the old ts program if an import changed', () => {
|
|
const host = createWatchModeHost();
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/util.ts': `export const x = 1`,
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from './util';
|
|
`
|
|
});
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile(undefined, undefined, undefined, host).program;
|
|
const p2 = compile(p1, undefined, undefined, host).program;
|
|
testSupport.writeFiles(
|
|
{'src/util.ts': `import {Injectable} from '@angular/core'; export const x = 1;`});
|
|
const structureReuse =
|
|
captureStructureReuse(() => compile(p2, undefined, undefined, host));
|
|
expect(structureReuse).toBe(StructureIsReused.SafeModules);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not typecheck templates if skipTemplateCodegen is set but fullTemplateTypeCheck is not',
|
|
() => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule((() => {if (1==1) return null as any;}) as any)
|
|
export class SomeClassWithInvalidMetadata {}
|
|
`,
|
|
});
|
|
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
const emitResult = program.emit({emitFlags: EmitFlags.All});
|
|
expect(emitResult.diagnostics.length).toBe(0);
|
|
|
|
testSupport.shouldExist('built/src/main.metadata.json');
|
|
});
|
|
|
|
it('should typecheck templates if skipTemplateCodegen and fullTemplateTypeCheck is set', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`),
|
|
});
|
|
const options = testSupport.createCompilerOptions({
|
|
skipTemplateCodegen: true,
|
|
fullTemplateTypeCheck: true,
|
|
});
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
const diags = program.getNgSemanticDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'mainComp'.`);
|
|
});
|
|
|
|
it('should be able to use asynchronously loaded resources', (done) => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
|
// Note: we need to be able to resolve the template synchronously,
|
|
// only the content is delivered asynchronously.
|
|
'src/main.html': '',
|
|
});
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
host.readResource = () => Promise.resolve('Hello world!');
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
program.loadNgStructureAsync().then(() => {
|
|
program.emit();
|
|
const ngFactoryPath = path.resolve(testSupport.basePath, 'built/src/main.ngfactory.js');
|
|
const factory = fs.readFileSync(ngFactoryPath, 'utf8');
|
|
expect(factory).toContain('Hello world!');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work with noResolve', () => {
|
|
// create a temporary ts program to get the list of all files from angular...
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
});
|
|
const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.ts')]);
|
|
|
|
// now do the actual test with noResolve
|
|
const program = compile(undefined, {noResolve: true}, allRootNames);
|
|
|
|
testSupport.shouldExist('built/src/main.ngfactory.js');
|
|
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
|
|
});
|
|
|
|
it('should work with tsx files', () => {
|
|
// create a temporary ts program to get the list of all files from angular...
|
|
testSupport.writeFiles({
|
|
'src/main.tsx': createModuleAndCompSource('main'),
|
|
});
|
|
const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.tsx')]);
|
|
|
|
const program = compile(undefined, {jsx: ts.JsxEmit.React}, allRootNames);
|
|
|
|
testSupport.shouldExist('built/src/main.js');
|
|
testSupport.shouldExist('built/src/main.d.ts');
|
|
testSupport.shouldExist('built/src/main.ngfactory.js');
|
|
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
|
|
testSupport.shouldExist('built/src/main.ngsummary.json');
|
|
});
|
|
|
|
it('should emit also empty generated files depending on the options', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({selector: 'main', template: '', styleUrls: ['main.css']})
|
|
export class MainComp {}
|
|
|
|
@NgModule({declarations: [MainComp]})
|
|
export class MainModule {}
|
|
`,
|
|
'src/main.css': ``,
|
|
'src/util.ts': 'export const x = 1;',
|
|
'src/index.ts': `
|
|
export * from './util';
|
|
export * from './main';
|
|
`,
|
|
});
|
|
const options = testSupport.createCompilerOptions({
|
|
allowEmptyCodegenFiles: true,
|
|
enableSummariesForJit: true,
|
|
});
|
|
const host = ng.createCompilerHost({options});
|
|
const written = new Map < string, {
|
|
original: ReadonlyArray<ts.SourceFile>|undefined;
|
|
data: string;
|
|
}
|
|
> ();
|
|
|
|
host.writeFile =
|
|
(fileName: string, data: string, writeByteOrderMark: boolean,
|
|
onError: ((message: string) => void)|undefined,
|
|
sourceFiles?: ReadonlyArray<ts.SourceFile>) => {
|
|
written.set(fileName, {original: sourceFiles, data});
|
|
};
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
|
program.emit();
|
|
|
|
const enum ShouldBe { Empty, EmptyExport, NoneEmpty }
|
|
function assertGenFile(
|
|
fileName: string, checks: {originalFileName: string, shouldBe: ShouldBe}) {
|
|
const writeData = written.get(path.posix.join(testSupport.basePath, fileName));
|
|
expect(writeData).toBeTruthy();
|
|
expect(
|
|
writeData!.original!.some(
|
|
sf => sf.fileName === path.posix.join(testSupport.basePath, checks.originalFileName)))
|
|
.toBe(true);
|
|
switch (checks.shouldBe) {
|
|
case ShouldBe.Empty:
|
|
expect(writeData!.data).toMatch(/^(\s*\/\*([^*]|\*[^\/])*\*\/\s*)?$/);
|
|
break;
|
|
case ShouldBe.EmptyExport:
|
|
expect(writeData!.data)
|
|
.toMatch(/^((\s*\/\*([^*]|\*[^\/])*\*\/\s*)|(\s*export\s*{\s*};\s*))$/m);
|
|
break;
|
|
case ShouldBe.NoneEmpty:
|
|
expect(writeData!.data).not.toBe('');
|
|
break;
|
|
}
|
|
}
|
|
|
|
assertGenFile(
|
|
'built/src/util.ngfactory.js',
|
|
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
|
assertGenFile(
|
|
'built/src/util.ngfactory.d.ts',
|
|
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.js',
|
|
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.d.ts',
|
|
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.json',
|
|
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.NoneEmpty});
|
|
|
|
// Note: we always fill non shim and shim style files as they might
|
|
// be shared by component with and without ViewEncapsulation.
|
|
assertGenFile(
|
|
'built/src/main.css.ngstyle.js',
|
|
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.NoneEmpty});
|
|
assertGenFile(
|
|
'built/src/main.css.ngstyle.d.ts',
|
|
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.EmptyExport});
|
|
// Note: this file is not empty as we actually generated code for it
|
|
assertGenFile(
|
|
'built/src/main.css.shim.ngstyle.js',
|
|
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.NoneEmpty});
|
|
assertGenFile(
|
|
'built/src/main.css.shim.ngstyle.d.ts',
|
|
{originalFileName: 'src/main.ts', shouldBe: ShouldBe.EmptyExport});
|
|
});
|
|
|
|
it('should not emit /// references in .d.ts files', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
});
|
|
compile(undefined, {declaration: true}, [path.resolve(testSupport.basePath, 'src/main.ts')]);
|
|
|
|
const dts =
|
|
fs.readFileSync(path.resolve(testSupport.basePath, 'built', 'src', 'main.d.ts')).toString();
|
|
expect(dts).toMatch('export declare class');
|
|
expect(dts).not.toMatch('///');
|
|
});
|
|
|
|
it('should not emit generated files whose sources are outside of the rootDir', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
`
|
|
});
|
|
const options =
|
|
testSupport.createCompilerOptions({rootDir: path.resolve(testSupport.basePath, 'src')});
|
|
const host = ng.createCompilerHost({options});
|
|
const writtenFileNames: string[] = [];
|
|
const oldWriteFile = host.writeFile;
|
|
host.writeFile = (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
|
writtenFileNames.push(fileName);
|
|
oldWriteFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
|
};
|
|
|
|
compile(/*oldProgram*/ undefined, options, /*rootNames*/ undefined, host);
|
|
|
|
// no emit for files from node_modules as they are outside of rootDir
|
|
expect(writtenFileNames.some(f => /node_modules/.test(f))).toBe(false);
|
|
|
|
// emit all gen files for files under src/
|
|
testSupport.shouldExist('built/main.js');
|
|
testSupport.shouldExist('built/main.d.ts');
|
|
testSupport.shouldExist('built/main.ngfactory.js');
|
|
testSupport.shouldExist('built/main.ngfactory.d.ts');
|
|
testSupport.shouldExist('built/main.ngsummary.json');
|
|
});
|
|
|
|
describe('createSrcToOutPathMapper', () => {
|
|
it('should return identity mapping if no outDir is present', () => {
|
|
const mapper = createSrcToOutPathMapper(undefined, undefined, undefined, path.posix);
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
|
});
|
|
|
|
it('should return identity mapping if first src and out fileName have same dir', () => {
|
|
const mapper = createSrcToOutPathMapper('/tmp', '/tmp/a/x.ts', '/tmp/a/x.js', path.posix);
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the outDir is inside of the rootDir', () => {
|
|
const mapper =
|
|
createSrcToOutPathMapper('/tmp/out', '/tmp/a/x.ts', '/tmp/out/a/x.js', path.posix);
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/out/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the outDir is outside of the rootDir', () => {
|
|
const mapper = createSrcToOutPathMapper('/out', '/tmp/a/x.ts', '/out/a/x.js', path.posix);
|
|
expect(mapper('/tmp/b/y.js')).toBe('/out/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the common prefix of sampleSrc and sampleOut is outside of outDir',
|
|
() => {
|
|
const mapper = createSrcToOutPathMapper(
|
|
'/dist/common', '/src/common/x.ts', '/dist/common/x.js', path.posix);
|
|
expect(mapper('/src/common/y.js')).toBe('/dist/common/y.js');
|
|
});
|
|
|
|
it('should work on windows with normalized paths', () => {
|
|
const mapper =
|
|
createSrcToOutPathMapper('c:/tmp/out', 'c:/tmp/a/x.ts', 'c:/tmp/out/a/x.js', path.win32);
|
|
expect(mapper('c:/tmp/b/y.js')).toBe('c:/tmp/out/b/y.js');
|
|
});
|
|
|
|
it('should work on windows with non-normalized paths', () => {
|
|
const mapper = createSrcToOutPathMapper(
|
|
'c:\\tmp\\out', 'c:\\tmp\\a\\x.ts', 'c:\\tmp\\out\\a\\x.js', path.win32);
|
|
expect(mapper('c:\\tmp\\b\\y.js')).toBe('c:/tmp/out/b/y.js');
|
|
});
|
|
});
|
|
|
|
it('should report errors for ts and ng errors on emit with noEmitOnError=true', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
// Ts error
|
|
let x: string = 1;
|
|
|
|
// Ng error
|
|
@Component({selector: 'comp', templateUrl: './main.html'})
|
|
export class MyComp {}
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`,
|
|
'src/main.html': '{{nonExistent}}'
|
|
});
|
|
const options = testSupport.createCompilerOptions({noEmitOnError: true});
|
|
const host = ng.createCompilerHost({options});
|
|
const program1 = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
const errorDiags =
|
|
program1.emit().diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
|
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
|
.toContain(
|
|
`src/main.ts:5:13 - error TS2322: Type 'number' is not assignable to type 'string'.`);
|
|
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
|
.toContain(
|
|
`src/main.html:1:1 - error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`);
|
|
});
|
|
|
|
it('should not report emit errors with noEmitOnError=false', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
@NgModule()
|
|
`
|
|
});
|
|
const options = testSupport.createCompilerOptions({noEmitOnError: false});
|
|
const host = ng.createCompilerHost({options});
|
|
const program1 = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
expect(program1.emit().diagnostics.length).toBe(0);
|
|
});
|
|
|
|
describe('errors', () => {
|
|
const fileWithStructuralError = `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule(() => (1===1 ? null as any : null as any))
|
|
export class MyModule {}
|
|
`;
|
|
const fileWithGoodContent = `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class MyModule {}
|
|
`;
|
|
|
|
it('should not throw on structural errors but collect them', () => {
|
|
testSupport.write('src/index.ts', fileWithStructuralError);
|
|
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
|
|
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
|
expect(structuralErrors.length).toBe(1);
|
|
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
|
});
|
|
|
|
it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => {
|
|
testSupport.write('src/index.ts', fileWithStructuralError);
|
|
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
|
program.loadNgStructureAsync().then(() => {
|
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
|
expect(structuralErrors.length).toBe(1);
|
|
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should include non-formatted errors (e.g. invalid templateUrl)', () => {
|
|
testSupport.write('src/index.ts', `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'my-component',
|
|
templateUrl: 'template.html', // invalid template url
|
|
})
|
|
export class MyComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [MyComponent]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram({
|
|
rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')],
|
|
options,
|
|
host,
|
|
});
|
|
|
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
|
expect(structuralErrors.length).toBe(1);
|
|
expect(structuralErrors[0].messageText).toContain('Couldn\'t resolve resource template.html');
|
|
});
|
|
|
|
it('should be able report structural errors with noResolve:true and generateCodeForLibraries:false ' +
|
|
'even if getSourceFile throws for non existent files',
|
|
() => {
|
|
testSupport.write('src/index.ts', fileWithGoodContent);
|
|
|
|
// compile angular and produce .ngsummary.json / ngfactory.d.ts files
|
|
compile();
|
|
|
|
testSupport.write('src/ok.ts', fileWithGoodContent);
|
|
testSupport.write('src/error.ts', fileWithStructuralError);
|
|
|
|
// Make sure the ok.ts file is before the error.ts file,
|
|
// so we added a .ngfactory.ts file for it.
|
|
const allRootNames = resolveFiles(
|
|
['src/ok.ts', 'src/error.ts'].map(fn => path.resolve(testSupport.basePath, fn)));
|
|
|
|
const options = testSupport.createCompilerOptions({
|
|
noResolve: true,
|
|
generateCodeForLibraries: false,
|
|
});
|
|
const host = ng.createCompilerHost({options});
|
|
const originalGetSourceFile = host.getSourceFile;
|
|
host.getSourceFile =
|
|
(fileName: string, languageVersion: ts.ScriptTarget,
|
|
onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined => {
|
|
// We should never try to load .ngfactory.ts files
|
|
if (fileName.match(/\.ngfactory\.ts$/)) {
|
|
throw new Error(`Non existent ngfactory file: ` + fileName);
|
|
}
|
|
return originalGetSourceFile.call(host, fileName, languageVersion, onError);
|
|
};
|
|
const program = ng.createProgram({rootNames: allRootNames, options, host});
|
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
|
expect(structuralErrors.length).toBe(1);
|
|
expect(structuralErrors[0].messageText)
|
|
.toContain('Function expressions are not supported');
|
|
});
|
|
});
|
|
});
|