/** * @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; }