/** * @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 * as ng from '@angular/compiler-cli'; import {BazelOptions, CachedFileLoader, CompilerHost, FileCache, FileLoader, UncachedFileLoader, constructManifest, debug, fixUmdModuleDeclarations, parseTsconfig, resolveNormalizedPath, runAsWorker, runWorkerLoop} from '@bazel/typescript'; import * as fs from 'fs'; import * as path from 'path'; import * as tsickle from 'tsickle'; import * as ts from 'typescript'; const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const NGC_GEN_FILES = /^(.*?)\.(ngfactory|ngsummary|ngstyle|shim\.ngstyle)(.*)$/; // FIXME: we should be able to add the assets to the tsconfig so FileLoader // knows about them const NGC_ASSETS = /\.(css|html|ngsummary\.json)$/; const BAZEL_BIN = /\b(blaze|bazel)-out\b.*?\bbin\b/; // TODO(alexeagle): probably not needed, see // https://github.com/bazelbuild/rules_typescript/issues/28 const ALLOW_NON_HERMETIC_READS = true; // Note: We compile the content of node_modules with plain ngc command line. const ALL_DEPS_COMPILED_WITH_BAZEL = false; const NODE_MODULES = 'node_modules/'; export function main(args) { if (runAsWorker(args)) { runWorkerLoop(runOneBuild); } else { return runOneBuild(args) ? 0 : 1; } return 0; } /** The one FileCache instance used in this process. */ const fileCache = new FileCache(debug); export function runOneBuild(args: string[], inputs?: {[path: string]: string}): boolean { if (args[0] === '-p') args.shift(); // Strip leading at-signs, used to indicate a params file const project = args[0].replace(/^@+/, ''); const [parsedOptions, errors] = parseTsconfig(project); if (errors && errors.length) { console.error(ng.formatDiagnostics(errors)); return false; } const {options: tsOptions, bazelOpts, files, config} = parsedOptions; const expectedOuts = config['angularCompilerOptions']['expectedOut']; const {basePath} = ng.calcProjectFileAndBasePath(project); const compilerOpts = ng.createNgCompilerOptions(basePath, config, tsOptions); const tsHost = ts.createCompilerHost(compilerOpts, true); const {diagnostics} = compile({ allowNonHermeticReads: ALLOW_NON_HERMETIC_READS, allDepsCompiledWithBazel: ALL_DEPS_COMPILED_WITH_BAZEL, compilerOpts, tsHost, bazelOpts, files, inputs, expectedOuts }); if (diagnostics.length) { console.error(ng.formatDiagnostics(diagnostics)); } return diagnostics.every(d => d.category !== ts.DiagnosticCategory.Error); } export function relativeToRootDirs(filePath: string, rootDirs: string[]): string { if (!filePath) return filePath; // NB: the rootDirs should have been sorted longest-first for (const dir of rootDirs || []) { const rel = path.posix.relative(dir, filePath); if (rel.indexOf('.') != 0) return rel; } return filePath; } export function compile({allowNonHermeticReads, allDepsCompiledWithBazel = true, compilerOpts, tsHost, bazelOpts, files, inputs, expectedOuts, gatherDiagnostics}: { allowNonHermeticReads: boolean, allDepsCompiledWithBazel?: boolean, compilerOpts: ng.CompilerOptions, tsHost: ts.CompilerHost, inputs?: {[path: string]: string}, bazelOpts: BazelOptions, files: string[], expectedOuts: string[], gatherDiagnostics?: (program: ng.Program) => ng.Diagnostics }): {diagnostics: ng.Diagnostics, program: ng.Program} { let fileLoader: FileLoader; if (bazelOpts.maxCacheSizeMb !== undefined) { const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * (1 << 20); fileCache.setMaxCacheSize(maxCacheSizeBytes); } else { fileCache.resetMaxCacheSize(); } if (inputs) { fileLoader = new CachedFileLoader(fileCache, allowNonHermeticReads); // Resolve the inputs to absolute paths to match TypeScript internals const resolvedInputs: {[path: string]: string} = {}; for (const key of Object.keys(inputs)) { resolvedInputs[resolveNormalizedPath(key)] = inputs[key]; } fileCache.updateCache(resolvedInputs); } else { fileLoader = new UncachedFileLoader(); } if (!bazelOpts.es5Mode) { compilerOpts.annotateForClosureCompiler = true; compilerOpts.annotationsAs = 'static fields'; } if (!compilerOpts.rootDirs) { throw new Error('rootDirs is not set!'); } const bazelBin = compilerOpts.rootDirs.find(rootDir => BAZEL_BIN.test(rootDir)); if (!bazelBin) { throw new Error(`Couldn't find bazel bin in the rootDirs: ${compilerOpts.rootDirs}`); } const writtenExpectedOuts = [...expectedOuts]; const originalWriteFile = tsHost.writeFile.bind(tsHost); tsHost.writeFile = (fileName: string, content: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { const relative = relativeToRootDirs(fileName.replace(/\\/g, '/'), [compilerOpts.rootDir]); const expectedIdx = writtenExpectedOuts.findIndex(o => o === relative); if (expectedIdx >= 0) { writtenExpectedOuts.splice(expectedIdx, 1); originalWriteFile(fileName, content, writeByteOrderMark, onError, sourceFiles); } }; // Patch fileExists when resolving modules, so that CompilerHost can ask TypeScript to // resolve non-existing generated files that don't exist on disk, but are // synthetic and added to the `programWithStubs` based on real inputs. const generatedFileModuleResolverHost = Object.create(tsHost); generatedFileModuleResolverHost.fileExists = (fileName: string) => { const match = NGC_GEN_FILES.exec(fileName); if (match) { const [, file, suffix, ext] = match; // Performance: skip looking for files other than .d.ts or .ts if (ext !== '.ts' && ext !== '.d.ts') return false; if (suffix.indexOf('ngstyle') >= 0) { // Look for foo.css on disk fileName = file; } else { // Look for foo.d.ts or foo.ts on disk fileName = file + (ext || ''); } } return tsHost.fileExists(fileName); }; function generatedFileModuleResolver( moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions): ts.ResolvedModuleWithFailedLookupLocations { return ts.resolveModuleName( moduleName, containingFile, compilerOptions, generatedFileModuleResolverHost); } const bazelHost = new CompilerHost( files, compilerOpts, bazelOpts, tsHost, fileLoader, allowNonHermeticReads, generatedFileModuleResolver); // Prevent tsickle adding any types at all if we don't want closure compiler annotations. bazelHost.transformTypesToClosure = compilerOpts.annotateForClosureCompiler; const origBazelHostFileExist = bazelHost.fileExists; bazelHost.fileExists = (fileName: string) => { if (NGC_ASSETS.test(fileName)) { return tsHost.fileExists(fileName); } return origBazelHostFileExist.call(bazelHost, fileName); }; const origBazelHostShouldNameModule = bazelHost.shouldNameModule.bind(bazelHost); bazelHost.shouldNameModule = (fileName: string) => { // The bundle index file is synthesized in bundle_index_host so it's not in the // compilationTargetSrc. // However we still want to give it an AMD module name for devmode. // We can't easily tell which file is the synthetic one, so we build up the path we expect // it to have // and compare against that. if (fileName === path.join(compilerOpts.baseUrl, bazelOpts.package, compilerOpts.flatModuleOutFile + '.ts')) return true; return origBazelHostShouldNameModule(fileName) || NGC_GEN_FILES.test(fileName); }; const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost}); ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) => { if ((compilerOpts.module === ts.ModuleKind.UMD || compilerOpts.module === ts.ModuleKind.AMD) && ngHost.amdModuleName) { return ngHost.amdModuleName({ fileName: importedFilePath } as ts.SourceFile); } const result = relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, ''); if (result.startsWith(NODE_MODULES)) { return result.substr(NODE_MODULES.length); } return bazelOpts.workspaceName + '/' + result; }; ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) => path.posix.join( bazelOpts.workspaceName, relativeToRootDirs(fileName, compilerOpts.rootDirs).replace(EXT, '')); if (allDepsCompiledWithBazel) { // Note: The default implementation would work as well, // but we can be faster as we know how `toSummaryFileName` works. // Note: We can't do this if some deps have been compiled with the command line, // as that has a different implementation of fromSummaryFileName / toSummaryFileName ngHost.fromSummaryFileName = (fileName: string, referringLibFileName: string) => { const workspaceRelative = fileName.split('/').splice(1).join('/'); return resolveNormalizedPath(bazelBin, workspaceRelative) + '.d.ts'; }; } // Patch a property on the ngHost that allows the resourceNameToModuleName function to // report better errors. (ngHost as any).reportMissingResource = (resourceName: string) => { console.error(`\nAsset not found:\n ${resourceName}`); console.error('Check that it\'s included in the `assets` attribute of the `ng_module` rule.\n'); }; const emitCallback: ng.TsEmitCallback = ({ program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers = {}, }) => tsickle.emitWithTsickle( program, bazelHost, bazelHost, compilerOpts, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, { beforeTs: customTransformers.before, afterTs: [ ...(customTransformers.after || []), fixUmdModuleDeclarations((sf: ts.SourceFile) => bazelHost.amdModuleName(sf)), ], }); if (!gatherDiagnostics) { gatherDiagnostics = (program) => gatherDiagnosticsForInputsOnly(compilerOpts, bazelOpts, program); } const {diagnostics, emitResult, program} = ng.performCompilation({ rootNames: files, options: compilerOpts, host: ngHost, emitCallback, mergeEmitResultsCallback: tsickle.mergeEmitResults, gatherDiagnostics }); const tsickleEmitResult = emitResult as tsickle.EmitResult; let externs = '/** @externs */\n'; if (!diagnostics.length) { if (bazelOpts.tsickleGenerateExterns) { externs += tsickle.getGeneratedExterns(tsickleEmitResult.externs); } if (bazelOpts.manifest) { const manifest = constructManifest(tsickleEmitResult.modulesManifest, bazelHost); fs.writeFileSync(bazelOpts.manifest, manifest); } } // If compilation fails unexpectedly, performCompilation returns no program. // Make sure not to crash but report the diagnostics. if (!program) return {program, diagnostics}; if (!bazelOpts.nodeModulesPrefix) { // If there is no node modules, then metadata.json should be emitted since // there is no other way to obtain the information generateMetadataJson(program.getTsProgram(), files, compilerOpts.rootDirs, bazelBin, tsHost); } if (bazelOpts.tsickleExternsPath) { // Note: when tsickleExternsPath is provided, we always write a file as a // marker that compilation succeeded, even if it's empty (just containing an // @externs). fs.writeFileSync(bazelOpts.tsickleExternsPath, externs); } for (const missing of writtenExpectedOuts) { originalWriteFile(missing, '', false); } return {program, diagnostics}; } /** * Generate metadata.json for the specified `files`. By default, metadata.json * is only generated by the compiler if --flatModuleOutFile is specified. But * if compiled under blaze, we want the metadata to be generated for each * Angular component. */ function generateMetadataJson( program: ts.Program, files: string[], rootDirs: string[], bazelBin: string, tsHost: ts.CompilerHost) { const collector = new ng.MetadataCollector(); for (const file of files) { const sourceFile = program.getSourceFile(file); if (sourceFile) { const metadata = collector.getMetadata(sourceFile); if (metadata) { const relative = relativeToRootDirs(file, rootDirs); const shortPath = relative.replace(EXT, '.metadata.json'); const outFile = resolveNormalizedPath(bazelBin, shortPath); const data = JSON.stringify(metadata); tsHost.writeFile(outFile, data, false, undefined, []); } } } } function isCompilationTarget(bazelOpts: BazelOptions, sf: ts.SourceFile): boolean { return !NGC_GEN_FILES.test(sf.fileName) && (bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1); } function gatherDiagnosticsForInputsOnly( options: ng.CompilerOptions, bazelOpts: BazelOptions, ngProgram: ng.Program): (ng.Diagnostic | ts.Diagnostic)[] { const tsProgram = ngProgram.getTsProgram(); const diagnostics: (ng.Diagnostic | ts.Diagnostic)[] = []; // These checks mirror ts.getPreEmitDiagnostics, with the important // exception of avoiding b/30708240, which is that if you call // program.getDeclarationDiagnostics() it somehow corrupts the emit. diagnostics.push(...tsProgram.getOptionsDiagnostics()); diagnostics.push(...tsProgram.getGlobalDiagnostics()); for (const sf of tsProgram.getSourceFiles().filter(f => isCompilationTarget(bazelOpts, f))) { // Note: We only get the diagnostics for individual files // to e.g. not check libraries. diagnostics.push(...tsProgram.getSyntacticDiagnostics(sf)); diagnostics.push(...tsProgram.getSemanticDiagnostics(sf)); } if (!diagnostics.length) { // only gather the angular diagnostics if we have no diagnostics // in any other files. diagnostics.push(...ngProgram.getNgStructuralDiagnostics()); diagnostics.push(...ngProgram.getNgSemanticDiagnostics()); } return diagnostics; } if (require.main === module) { process.exitCode = main(process.argv.slice(2)); }