import { ExecutorContext, TaskGraph, parseTargetString } from '@nx/devkit'; import { rmSync } from 'fs'; import type { BatchExecutorTaskResult } from 'nx/src/config/misc-interfaces'; import { getLastValueFromAsyncIterableIterator } from 'nx/src/utils/async-iterator'; import { updatePackageJson } from '../../utils/package-json/update-package-json'; import type { ExecutorOptions } from '../../utils/schema'; import { determineModuleFormatFromTsConfig } from './tsc.impl'; import { TypescripCompilationLogger, TypescriptCompilationResult, TypescriptInMemoryTsConfig, TypescriptProjectContext, compileTypescriptSolution, getProcessedTaskTsConfigs, } from './lib'; import { TaskInfo, createTaskInfoPerTsConfigMap, normalizeTasksOptions, watchTaskProjectsFileChangesForAssets, watchTaskProjectsPackageJsonFileChanges, } from './lib/batch'; import { createEntryPoints } from '../../utils/package-json/create-entry-points'; export async function* tscBatchExecutor( taskGraph: TaskGraph, inputs: Record, overrides: ExecutorOptions, context: ExecutorContext ) { const tasksOptions = normalizeTasksOptions(inputs, context); let shouldWatch = false; Object.values(tasksOptions).forEach((taskOptions) => { if (taskOptions.clean) { rmSync(taskOptions.outputPath, { force: true, recursive: true }); } if (taskOptions.watch) { shouldWatch = true; } }); const taskInMemoryTsConfigMap = getProcessedTaskTsConfigs( Object.keys(taskGraph.tasks), tasksOptions, context ); const tsConfigTaskInfoMap = createTaskInfoPerTsConfigMap( tasksOptions, context, Object.keys(taskGraph.tasks), taskInMemoryTsConfigMap ); const tsCompilationContext = createTypescriptCompilationContext( tsConfigTaskInfoMap, taskInMemoryTsConfigMap, context ); const logger: TypescripCompilationLogger = { error: (message, tsConfig) => { process.stderr.write(message); if (tsConfig) { tsConfigTaskInfoMap[tsConfig].terminalOutput += message; } }, info: (message, tsConfig) => { process.stdout.write(message); if (tsConfig) { tsConfigTaskInfoMap[tsConfig].terminalOutput += message; } }, warn: (message, tsConfig) => { process.stdout.write(message); if (tsConfig) { tsConfigTaskInfoMap[tsConfig].terminalOutput += message; } }, }; const processTaskPostCompilation = (tsConfig: string) => { if (tsConfigTaskInfoMap[tsConfig]) { const taskInfo = tsConfigTaskInfoMap[tsConfig]; taskInfo.assetsHandler.processAllAssetsOnceSync(); updatePackageJson( { ...taskInfo.options, additionalEntryPoints: createEntryPoints( taskInfo.options.additionalEntryPoints, context.root ), format: [determineModuleFormatFromTsConfig(tsConfig)], // As long as d.ts files match their .js counterparts, we don't need to emit them. // TSC can match them correctly based on file names. skipTypings: true, }, taskInfo.context, taskInfo.projectGraphNode, taskInfo.buildableProjectNodeDependencies ); taskInfo.endTime = Date.now(); } }; const typescriptCompilation = compileTypescriptSolution( tsCompilationContext, shouldWatch, logger, { beforeProjectCompilationCallback: (tsConfig) => { if (tsConfigTaskInfoMap[tsConfig]) { tsConfigTaskInfoMap[tsConfig].startTime = Date.now(); } }, afterProjectCompilationCallback: processTaskPostCompilation, } ); if (shouldWatch) { const taskInfos = Object.values(tsConfigTaskInfoMap); const watchAssetsChangesDisposer = await watchTaskProjectsFileChangesForAssets(taskInfos); const watchProjectsChangesDisposer = await watchTaskProjectsPackageJsonFileChanges( taskInfos, (changedTaskInfos: TaskInfo[]) => { for (const t of changedTaskInfos) { updatePackageJson( { ...t.options, additionalEntryPoints: createEntryPoints( t.options.additionalEntryPoints, context.root ), format: [determineModuleFormatFromTsConfig(t.options.tsConfig)], // As long as d.ts files match their .js counterparts, we don't need to emit them. // TSC can match them correctly based on file names. skipTypings: true, }, t.context, t.projectGraphNode, t.buildableProjectNodeDependencies ); } } ); const handleTermination = async (exitCode: number) => { watchAssetsChangesDisposer(); watchProjectsChangesDisposer(); process.exit(exitCode); }; process.on('SIGINT', () => handleTermination(128 + 2)); process.on('SIGTERM', () => handleTermination(128 + 15)); return yield* mapAsyncIterable(typescriptCompilation, async (iterator) => { // drain the iterator, we don't use the results await getLastValueFromAsyncIterableIterator(iterator); return { value: undefined, done: true }; }); } const toBatchExecutorTaskResult = ( tsConfig: string, success: boolean ): BatchExecutorTaskResult => ({ task: tsConfigTaskInfoMap[tsConfig].task, result: { success: success, terminalOutput: tsConfigTaskInfoMap[tsConfig].terminalOutput, startTime: tsConfigTaskInfoMap[tsConfig].startTime, endTime: tsConfigTaskInfoMap[tsConfig].endTime, }, }); let isCompilationDone = false; const taskTsConfigsToReport = new Set( Object.keys(taskGraph.tasks).map((t) => taskInMemoryTsConfigMap[t].path) ); let tasksToReportIterator: IterableIterator; const processSkippedTasks = () => { const { value: tsConfig, done } = tasksToReportIterator.next(); if (done) { return { value: undefined, done: true }; } tsConfigTaskInfoMap[tsConfig].startTime = Date.now(); processTaskPostCompilation(tsConfig); return { value: toBatchExecutorTaskResult(tsConfig, true), done: false }; }; return yield* mapAsyncIterable(typescriptCompilation, async (iterator) => { if (isCompilationDone) { return processSkippedTasks(); } const { value, done } = await iterator.next(); if (done) { if (taskTsConfigsToReport.size > 0) { /** * TS compilation is done but we still have tasks to report. This can * happen if, for example, a project is identified as affected, but * no file in the TS project is actually changed or if running a * task with `--skip-nx-cache` and the outputs are already there. There * can still be changes to assets or other files we need to process. * * Switch to handle the iterator for the tasks we still need to report. */ isCompilationDone = true; tasksToReportIterator = taskTsConfigsToReport.values(); return processSkippedTasks(); } return { value: undefined, done: true }; } taskTsConfigsToReport.delete(value.tsConfig); return { value: toBatchExecutorTaskResult(value.tsConfig, value.success), done: false, }; }); } export default tscBatchExecutor; async function* mapAsyncIterable( iterable: AsyncIterable, nextFn: ( iterator: AsyncIterableIterator ) => Promise> ) { return yield* { [Symbol.asyncIterator]() { const iterator: AsyncIterableIterator = iterable[Symbol.asyncIterator].call(iterable); return { async next() { return await nextFn(iterator); }, }; }, }; } function createTypescriptCompilationContext( tsConfigTaskInfoMap: Record, taskInMemoryTsConfigMap: Record, context: ExecutorContext ): Record { const tsCompilationContext: Record = Object.entries(tsConfigTaskInfoMap).reduce((acc, [tsConfig, taskInfo]) => { acc[tsConfig] = { project: taskInfo.context.projectName, tsConfig: taskInfo.tsConfig, transformers: taskInfo.options.transformers, }; return acc; }, {} as Record); Object.entries(taskInMemoryTsConfigMap).forEach(([task, tsConfig]) => { if (!tsCompilationContext[tsConfig.path]) { tsCompilationContext[tsConfig.path] = { project: parseTargetString(task, context).project, transformers: [], tsConfig: tsConfig, }; } }); return tsCompilationContext; }