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(/^@+/, ''); let fileLoader: FileLoader; if (inputs) { fileLoader = new CachedFileLoader(fileCache, ALLOW_NON_HERMETIC_READS); // Resolve the inputs to absolute paths to match TypeScript internals const resolvedInputs: {[path: string]: string} = {}; for (const key of Object.keys(inputs)) { resolvedInputs[path.resolve(key)] = inputs[key]; } fileCache.updateCache(resolvedInputs); } else { fileLoader = new UncachedFileLoader(); } const [{options: tsOptions, bazelOpts, files, config}] = parseTsconfig(project); const expectedOuts = config['angularCompilerOptions']['expectedOut']; const {basePath} = ng.calcProjectFileAndBasePath(project); const compilerOpts = ng.createNgCompilerOptions(basePath, config, tsOptions); const {diagnostics} = compile({fileLoader, compilerOpts, bazelOpts, files, expectedOuts}); return diagnostics.every(d => d.category !== ts.DiagnosticCategory.Error); }
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 (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[path.resolve(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, [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); 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) => 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) => 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) => path.resolve(bazelBin, fileName) + '.d.ts'; } 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, 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 (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}; }
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} = {}; const inputKeys = Object.keys(inputs); for (let i = 0; i < inputKeys.length; i++) { const key = inputKeys[i]; resolvedInputs[resolveNormalizedPath(key)] = inputs[key]; } fileCache.updateCache(resolvedInputs); } else { fileLoader = new UncachedFileLoader(); } if (!bazelOpts.es5Mode) { compilerOpts.annotateForClosureCompiler = true; compilerOpts.annotationsAs = 'static fields'; } // Detect from compilerOpts whether the entrypoint is being invoked in Ivy mode. const isInIvyMode = compilerOpts.enableIvy === 'ngtsc' || compilerOpts.enableIvy === 'tsc'; // Disable downleveling and Closure annotation if in Ivy mode. if (isInIvyMode) { // In pass-through mode for TypeScript, we want to turn off decorator transpilation entirely. // This causes ngc to be have exactly like tsc. if (compilerOpts.enableIvy === 'tsc') { compilerOpts.annotateForClosureCompiler = false; } compilerOpts.annotationsAs = 'decorators'; } 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); // Also need to disable decorator downleveling in the BazelHost in Ivy mode. if (isInIvyMode) { bazelHost.transformDecorators = false; } // 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; // Also handle the case when angular is built from source as an external repository if (fileName === path.join( compilerOpts.baseUrl, 'external/angular', bazelOpts.package, compilerOpts.flatModuleOutFile + '.ts')) return true; return origBazelHostShouldNameModule(fileName) || NGC_GEN_FILES.test(fileName); }; const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost}); const fileNameToModuleNameCache = new Map<string, string>(); ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) => { // Memoize this lookup to avoid expensive re-parses of the same file // When run as a worker, the actual ts.SourceFile is cached // but when we don't run as a worker, there is no cache. // For one example target in g3, we saw a cache hit rate of 7590/7695 if (fileNameToModuleNameCache.has(importedFilePath)) { return fileNameToModuleNameCache.get(importedFilePath); } const result = doFileNameToModuleName(importedFilePath); fileNameToModuleNameCache.set(importedFilePath, result); return result; }; function doFileNameToModuleName(importedFilePath: string): string { try { const sourceFile = ngHost.getSourceFile(importedFilePath, ts.ScriptTarget.Latest); if (sourceFile && sourceFile.moduleName) { return sourceFile.moduleName; } } catch (err) { // File does not exist or parse error. Ignore this case and continue onto the // other methods of resolving the module below. } 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, }); 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 (let i = 0; i < writtenExpectedOuts.length; i++) { originalWriteFile(writtenExpectedOuts[i], '', false); } return {program, diagnostics}; }
export function compile({allowNonHermeticReads, compilerOpts, tsHost, bazelOpts, files, inputs, expectedOuts, gatherDiagnostics}: { allowNonHermeticReads: 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 (inputs) { fileLoader = new CachedFileLoader(fileCache, ALLOW_NON_HERMETIC_READS); // Resolve the inputs to absolute paths to match TypeScript internals const resolvedInputs: {[path: string]: string} = {}; for (const key of Object.keys(inputs)) { resolvedInputs[path.resolve(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 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, [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); } // TODO(alexeagle): does this also work in third_party? const allowNonHermeticRead = false; const bazelHost = new CompilerHost( files, compilerOpts, bazelOpts, tsHost, fileLoader, ALLOW_NON_HERMETIC_READS, generatedFileModuleResolver); const origBazelHostFileExist = bazelHost.fileExists; bazelHost.fileExists = (fileName: string) => { if (NGC_ASSETS.test(fileName)) { return tsHost.fileExists(fileName); } return origBazelHostFileExist.call(bazelHost, fileName); }; const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost}); ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) => relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, ''); ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) => ngHost.fileNameToModuleName(fileName, referringSrcFileName); 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)), ], }); const {diagnostics, emitResult, program} = ng.performCompilation( {rootNames: files, options: compilerOpts, host: ngHost, emitCallback, gatherDiagnostics}); const tsickleEmitResult = emitResult as tsickle.EmitResult; let externs = '/** @externs */\n'; if (diagnostics.length) { console.error(ng.formatDiagnostics(compilerOpts, diagnostics)); } else { if (bazelOpts.tsickleGenerateExterns) { externs += tsickle.getGeneratedExterns(tsickleEmitResult.externs); } if (bazelOpts.manifest) { const manifest = constructManifest(tsickleEmitResult.modulesManifest, bazelHost); fs.writeFileSync(bazelOpts.manifest, manifest); } } 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}; }