1122 lines
43 KiB
TypeScript
1122 lines
43 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 * as ts from 'typescript';
|
|
|
|
import {formatDiagnostics} from '../../src/perform_compile';
|
|
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
|
|
import {createSrcToOutPathMapper} 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',
|
|
() => {
|
|
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;
|
|
compile(p2, undefined, undefined, host);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).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`,
|
|
});
|
|
compile(p2, undefined, undefined, host);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).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;`});
|
|
compile(p2, undefined, undefined, host);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).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');
|
|
});
|
|
});
|
|
|
|
describe('listLazyRoutes', () => {
|
|
function writeSomeRoutes() {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot([{loadChildren: './child#ChildModule'}])]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild([{loadChildren: './child2#ChildModule2'}])]
|
|
})
|
|
export class ChildModule {}
|
|
`,
|
|
'src/child2.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule2 {}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function createProgram(rootNames: string[], overrideOptions: ng.CompilerOptions = {}) {
|
|
const options = testSupport.createCompilerOptions(overrideOptions);
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: rootNames.map(p => path.resolve(testSupport.basePath, p)), options, host});
|
|
return {program, options};
|
|
}
|
|
|
|
function normalizeRoutes(lazyRoutes: LazyRoute[]) {
|
|
return lazyRoutes.map(
|
|
r => ({
|
|
route: r.route,
|
|
module: {name: r.module.name, filePath: r.module.filePath},
|
|
referencedModule:
|
|
{name: r.referencedModule.name, filePath: r.referencedModule.filePath},
|
|
}));
|
|
}
|
|
|
|
it('should list all lazyRoutes', () => {
|
|
writeSomeRoutes();
|
|
const {program, options} = createProgram(['src/main.ts', 'src/child.ts', 'src/child2.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes())).toEqual([
|
|
{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'ChildModule2',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child2.ts')
|
|
},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should emit correctly after listing lazyRoutes', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot([{loadChildren: './lazy/lazy#LazyModule'}])]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/lazy/lazy.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/main.ts', 'src/lazy/lazy.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
program.listLazyRoutes();
|
|
program.emit();
|
|
|
|
const ngFactoryPath = path.resolve(testSupport.basePath, 'built/src/lazy/lazy.ngfactory.js');
|
|
const lazyNgFactory = fs.readFileSync(ngFactoryPath, 'utf8');
|
|
|
|
expect(lazyNgFactory).toContain('import * as i1 from "./lazy";');
|
|
});
|
|
|
|
it('should list lazyRoutes given an entryRoute recursively', () => {
|
|
writeSomeRoutes();
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'ChildModule2',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child2.ts')
|
|
},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/child#ChildModule'))).toEqual([
|
|
{
|
|
module: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'ChildModule2',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child2.ts')
|
|
},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes pointing to a default export', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot([{loadChildren: './child'}])]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export default class ChildModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule: {
|
|
name: undefined as any as string, // TODO: Review use of `any` here (#19904)
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
route: './child'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes from imported modules', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
import {NestedMainModule} from './nested/main';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot([{loadChildren: './child#ChildModule'}]),
|
|
NestedMainModule,
|
|
]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule {}
|
|
`,
|
|
'src/nested/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild([{loadChildren: './child#NestedChildModule'}])]
|
|
})
|
|
export class NestedMainModule {}
|
|
`,
|
|
'src/nested/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class NestedChildModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module: {
|
|
name: 'NestedMainModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/nested/main.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'NestedChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/nested/child.ts')
|
|
},
|
|
route: './child#NestedChildModule'
|
|
},
|
|
{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
route: './child#ChildModule'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should dedupe lazyRoutes given an entryRoute', () => {
|
|
writeSomeRoutes();
|
|
testSupport.writeFiles({
|
|
'src/index.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot([{loadChildren: './main#MainModule'}]),
|
|
RouterModule.forRoot([{loadChildren: './child#ChildModule'}]),
|
|
]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/index.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module: {
|
|
name: 'ChildModule',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'ChildModule2',
|
|
filePath: path.posix.join(testSupport.basePath, 'src/child2.ts')
|
|
},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes given an entryRoute even with static errors', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule, Component} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@Component({
|
|
selector: 'url-comp',
|
|
// Non existent external template
|
|
templateUrl: 'non-existent.html',
|
|
})
|
|
export class ErrorComp {}
|
|
|
|
@Component({
|
|
selector: 'err-comp',
|
|
// Error in template
|
|
template: '<input/>{{',
|
|
})
|
|
export class ErrorComp2 {}
|
|
|
|
// Component with metadata errors.
|
|
@Component(() => {if (1==1) return null as any;})
|
|
export class ErrorComp3 {}
|
|
|
|
// Unused component
|
|
@Component({
|
|
selector: 'unused-comp',
|
|
template: ''
|
|
})
|
|
export class UnusedComp {}
|
|
|
|
@NgModule({
|
|
declarations: [ErrorComp, ErrorComp2, ErrorComp3, NonExistentComp],
|
|
imports: [RouterModule.forRoot([{loadChildren: './child#ChildModule'}])]
|
|
})
|
|
export class MainModule {}
|
|
|
|
@NgModule({
|
|
// Component used in 2 NgModules
|
|
declarations: [ErrorComp],
|
|
})
|
|
export class Mod2 {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule {}
|
|
`,
|
|
});
|
|
const program = createProgram(['src/main.ts'], {collectAllErrors: true}).program;
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([{
|
|
module:
|
|
{name: 'MainModule', filePath: path.posix.join(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.posix.join(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
}]);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|