diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index f242a5b64b..482b8ecba8 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -20,8 +20,7 @@ export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Sp export * from './src/transformers/api'; export * from './src/transformers/entry_points'; -export {main as ngc} from './src/ngc'; -export {performCompilation} from './src/ngc'; +export {performCompilation} from './src/perform-compile'; // TODO(hansl): moving to Angular 4 need to update this API. export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'; diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts index e39757f865..658ed31d6e 100644 --- a/packages/compiler-cli/src/ngc.ts +++ b/packages/compiler-cli/src/ngc.ts @@ -10,185 +10,14 @@ import 'reflect-metadata'; import {isSyntaxError, syntaxError} from '@angular/compiler'; -import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; -import * as api from './transformers/api'; -import * as ng from './transformers/entry_points'; - -const TS_EXT = /\.ts$/; - -type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; - -function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { - return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); -} - -function formatDiagnostics(cwd: string, diags: Diagnostics): string { - if (diags && diags.length) { - if (isTsDiagnostics(diags)) { - return ts.formatDiagnostics(diags, { - getCurrentDirectory: () => cwd, - getCanonicalFileName: fileName => fileName, - getNewLine: () => ts.sys.newLine - }); - } else { - return diags - .map(d => { - let res = api.DiagnosticCategory[d.category]; - if (d.span) { - res += - ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; - } - if (d.span && d.span.details) { - res += `: ${d.span.details}, ${d.message}\n`; - } else { - res += `: ${d.message}\n`; - } - return res; - }) - .join(); - } - } else - return ''; -} - -function check(cwd: string, ...args: Diagnostics[]) { - if (args.some(diags => !!(diags && diags[0]))) { - throw syntaxError(args.map(diags => { - if (diags && diags[0]) { - return formatDiagnostics(cwd, diags); - } - }) - .filter(message => !!message) - .join('')); - } -} - -function syntheticError(message: string): ts.Diagnostic { - return { - file: null as any as ts.SourceFile, - start: 0, - length: 0, - messageText: message, - category: ts.DiagnosticCategory.Error, - code: 0 - }; -} - -export function readConfiguration( - project: string, basePath: string, checkFunc: (cwd: string, ...args: any[]) => void = check, - existingOptions?: ts.CompilerOptions) { - // Allow a directory containing tsconfig.json as the project value - // Note, TS@next returns an empty array, while earlier versions throw - const projectFile = - fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; - let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); - - if (error) checkFunc(basePath, [error]); - const parseConfigHost = { - useCaseSensitiveFileNames: true, - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: ts.sys.readFile - }; - const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); - - checkFunc(basePath, parsed.errors); - - // Default codegen goes to the current directory - // Parsed options are already converted to absolute paths - const ngOptions = config.angularCompilerOptions || {}; - // Ignore the genDir option - ngOptions.genDir = basePath; - - return {parsed, ngOptions}; -} - -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 performCompilation( - basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, - consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check, tsCompilerHost?: ts.CompilerHost) { - try { - ngOptions.basePath = basePath; - ngOptions.genDir = basePath; - - let host = tsCompilerHost || ts.createCompilerHost(options, true); - host.realpath = p => p; - - const rootFileNames = files.map(f => path.normalize(f)); - - const addGeneratedFileName = (fileName: string) => { - if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { - rootFileNames.push(fileName); - } - }; - - if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { - const {host: bundleHost, indexName, errors} = - createBundleIndexHost(ngOptions, rootFileNames, host); - if (errors) checkFunc(basePath, errors); - if (indexName) addGeneratedFileName(indexName); - host = bundleHost; - } - - const ngHostOptions = {...options, ...ngOptions}; - const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); - - const ngProgram = - ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); - - // Check parameter diagnostics - checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); - - // Check syntactic diagnostics - checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); - - // Check TypeScript semantic and Angular structure diagnostics - checkFunc( - basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); - - // Check Angular semantic diagnostics - checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); - - ngProgram.emit({ - emitFlags: api.EmitFlags.Default | - ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) - }); - } catch (e) { - if (isSyntaxError(e)) { - console.error(e.message); - consoleError(e.message); - return 1; - } - throw e; - } - - return 0; -} +import {performCompilation, readConfiguration, throwOnDiagnostics} from './perform-compile'; export function main( args: string[], consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check): number { + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics): number { try { const parsedArgs = require('minimist')(args); const project = parsedArgs.p || parsedArgs.project || '.'; diff --git a/packages/compiler-cli/src/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts new file mode 100644 index 0000000000..55feddb347 --- /dev/null +++ b/packages/compiler-cli/src/perform-compile.ts @@ -0,0 +1,192 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +import {isSyntaxError, syntaxError} from '@angular/compiler'; +import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import * as api from './transformers/api'; +import * as ng from './transformers/entry_points'; + +const TS_EXT = /\.ts$/; + +export type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; + +function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { + return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); +} + +function formatDiagnostics(cwd: string, diags: Diagnostics): string { + if (diags && diags.length) { + if (isTsDiagnostics(diags)) { + return ts.formatDiagnostics(diags, { + getCurrentDirectory: () => cwd, + getCanonicalFileName: fileName => fileName, + getNewLine: () => ts.sys.newLine + }); + } else { + return diags + .map(d => { + let res = api.DiagnosticCategory[d.category]; + if (d.span) { + res += + ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; + } + if (d.span && d.span.details) { + res += `: ${d.span.details}, ${d.message}\n`; + } else { + res += `: ${d.message}\n`; + } + return res; + }) + .join(); + } + } else + return ''; +} + +/** + * Throw a syntax error exception with a message formatted for output + * if the args parameter contains diagnostics errors. + * + * @param cwd The directory to report error as relative to. + * @param args A list of potentially empty diagnostic errors. + */ +export function throwOnDiagnostics(cwd: string, ...args: Diagnostics[]) { + if (args.some(diags => !!(diags && diags[0]))) { + throw syntaxError(args.map(diags => { + if (diags && diags[0]) { + return formatDiagnostics(cwd, diags); + } + }) + .filter(message => !!message) + .join('')); + } +} + +function syntheticError(message: string): ts.Diagnostic { + return { + file: null as any as ts.SourceFile, + start: 0, + length: 0, + messageText: message, + category: ts.DiagnosticCategory.Error, + code: 0 + }; +} + +export function readConfiguration( + project: string, basePath: string, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + existingOptions?: ts.CompilerOptions) { + // Allow a directory containing tsconfig.json as the project value + // Note, TS@next returns an empty array, while earlier versions throw + const projectFile = + fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; + let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); + + if (error) checkFunc(basePath, [error]); + const parseConfigHost = { + useCaseSensitiveFileNames: true, + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile + }; + const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); + + checkFunc(basePath, parsed.errors); + + // Default codegen goes to the current directory + // Parsed options are already converted to absolute paths + const ngOptions = config.angularCompilerOptions || {}; + // Ignore the genDir option + ngOptions.genDir = basePath; + + return {parsed, ngOptions}; +} + +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 performCompilation( + basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, + consoleError: (s: string) => void = console.error, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + tsCompilerHost?: ts.CompilerHost) { + try { + ngOptions.basePath = basePath; + ngOptions.genDir = basePath; + + let host = tsCompilerHost || ts.createCompilerHost(options, true); + host.realpath = p => p; + + const rootFileNames = files.map(f => path.normalize(f)); + + const addGeneratedFileName = (fileName: string) => { + if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { + rootFileNames.push(fileName); + } + }; + + if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { + const {host: bundleHost, indexName, errors} = + createBundleIndexHost(ngOptions, rootFileNames, host); + if (errors) checkFunc(basePath, errors); + if (indexName) addGeneratedFileName(indexName); + host = bundleHost; + } + + const ngHostOptions = {...options, ...ngOptions}; + const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); + + const ngProgram = + ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); + + // Check parameter diagnostics + checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); + + // Check syntactic diagnostics + checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); + + // Check TypeScript semantic and Angular structure diagnostics + checkFunc( + basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); + + // Check Angular semantic diagnostics + checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); + + ngProgram.emit({ + emitFlags: api.EmitFlags.Default | + ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) + }); + } catch (e) { + if (isSyntaxError(e)) { + console.error(e.message); + consoleError(e.message); + return 1; + } + throw e; + } + + return 0; +} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index d150590c2b..0a994cf6e8 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -11,7 +11,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main, performCompilation} from '../src/ngc'; +import {main} from '../src/ngc'; +import {performCompilation} from '../src/perform-compile'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/');