diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts index bc32f657d9..3089a9be74 100644 --- a/packages/compiler-cli/src/ngc.ts +++ b/packages/compiler-cli/src/ngc.ts @@ -29,9 +29,9 @@ function formatDiagnostics(cwd: string, diags: Diagnostics): string { if (diags && diags.length) { if (isTsDiagnostics(diags)) { return ts.formatDiagnostics(diags, { - getCurrentDirectory(): string{return cwd;}, - getCanonicalFileName(fileName: string): string{return fileName;}, - getNewLine(): string{return '\n';} + getCurrentDirectory: () => cwd, + getCanonicalFileName: fileName => fileName, + getNewLine: () => ts.sys.newLine }); } else { return diags @@ -101,30 +101,54 @@ export function readConfiguration( const ngOptions = config.angularCompilerOptions || {}; // Ignore the genDir option ngOptions.genDir = basePath; - for (const key of Object.keys(parsed.options)) { - ngOptions[key] = parsed.options[key]; - } return {parsed, ngOptions}; } -export function main(args: string[], consoleError: (s: string) => void = console.error): number { +function getProjectDirectory(project: string): string { + let isFile: boolean; + try { + isFile = fs.lstatSync(project).isFile(); + } catch (e) { + // Project doesn't exist. Assume it is a file has an extension. This case happens + // when the project file is passed to set basePath but no tsconfig.json file exists. + // It is used in tests to ensure that the options can be passed in without there being + // an actual config file. + isFile = path.extname(project) !== ''; + } + + // If project refers to a file, the project directory is the file's parent directory + // otherwise project is the project directory. + return isFile ? path.dirname(project) : project; +} + +export function main( + args: string[], consoleError: (s: string) => void = console.error, files?: string[], + options?: ts.CompilerOptions, ngOptions?: any): number { try { const parsedArgs = require('minimist')(args); const project = parsedArgs.p || parsedArgs.project || '.'; - const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project; - + const projectDir = getProjectDirectory(project); // file names in tsconfig are resolved relative to this absolute path const basePath = path.resolve(process.cwd(), projectDir); - const {parsed, ngOptions} = readConfiguration(project, basePath); + if (!files || !options || !ngOptions) { + const {parsed, ngOptions: readNgOptions} = readConfiguration(project, basePath); + if (!files) files = parsed.fileNames; + if (!options) options = parsed.options; + if (!ngOptions) ngOptions = readNgOptions; + } + + // Ignore what the tsconfig.json for baseDir and genDir ngOptions.basePath = basePath; + ngOptions.genDir = basePath; - let host = ts.createCompilerHost(parsed.options, true); + let host = ts.createCompilerHost(options, true); + host.realpath = p => p; - const rootFileNames = parsed.fileNames.slice(0); + const rootFileNames = files.map(f => path.normalize(f)); const addGeneratedFileName = (fileName: string) => { @@ -141,10 +165,11 @@ export function main(args: string[], consoleError: (s: string) => void = console host = bundleHost; } - const ngHost = ng.createHost({tsHost: host, options: ngOptions}); + const ngHostOptions = {...options, ...ngOptions}; + const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); const ngProgram = - ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngOptions}); + ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); // Check parameter diagnostics check(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 3191d93244..17b5d34024 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -385,6 +385,7 @@ function createProgramWithStubsHost( getCanonicalFileName = (fileName: string) => originalHost.getCanonicalFileName(fileName); useCaseSensitiveFileNames = () => originalHost.useCaseSensitiveFileNames(); getNewLine = () => originalHost.getNewLine(); + realPath = (p: string) => p; fileExists = (fileName: string) => this.generatedFiles.has(fileName) || originalHost.fileExists(fileName); }; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index b65d52f1d5..f5f3a24e03 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -9,6 +9,7 @@ import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; import * as fs from 'fs'; import * as path from 'path'; +import * as ts from 'typescript'; import {main} from '../src/ngc'; @@ -40,6 +41,7 @@ describe('ngc command-line', () => { write('tsconfig-base.json', `{ "compilerOptions": { "experimentalDecorators": true, + "skipLibCheck": true, "types": [], "outDir": "built", "declaration": true, @@ -72,6 +74,28 @@ describe('ngc command-line', () => { expect(result).toBe(0); }); + it('should be able to be called without a config file by passing options explicitly', () => { + write('test.ts', 'export const A = 1;'); + + const mockConsole = {error: (s: string) => {}}; + + spyOn(mockConsole, 'error'); + + const result = main( + ['-p', basePath], mockConsole.error, [path.join(basePath, 'test.ts')], { + experimentalDecorators: true, + skipLibCheck: true, + types: [], + outDir: path.join(basePath, 'built'), + declaration: true, + module: ts.ModuleKind.ES2015, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + }, + {}); + expect(mockConsole.error).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + it('should not print the stack trace if user input file does not exist', () => { writeConfig(`{ "extends": "./tsconfig-base.json", @@ -365,6 +389,61 @@ describe('ngc command-line', () => { shouldExist('index.metadata.json'); }); + it('should be able to build a flat module passing explicit options', () => { + write('public-api.ts', ` + export * from './src/flat.component'; + export * from './src/flat.module';`); + write('src/flat.component.html', '
flat module component
'); + 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, + ] + }) + export class FlatModule { + }`); + + const exitCode = main( + ['-p', path.join(basePath, 'tsconfig.json')], undefined, + [path.join(basePath, 'public-api.ts')], { + target: ts.ScriptTarget.ES5, + experimentalDecorators: true, + noImplicitAny: true, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + rootDir: basePath, + declaration: true, + lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'], + baseUrl: basePath, + outDir: path.join(basePath, 'built'), + typeRoots: [path.join(basePath, 'node_modules/@types')] + }, + { + genDir: 'ng', + flatModuleId: 'flat_module', + flatModuleOutFile: 'index.js', + skipTemplateCodegen: true + }); + expect(exitCode).toEqual(0); + shouldExist('index.js'); + shouldExist('index.metadata.json'); + }); + describe('with a third-party library', () => { const writeGenConfig = (skipCodegen: boolean) => { writeConfig(`{