nx/packages/js/src/executors/tsc/tsc.batch-impl.ts

277 lines
8.7 KiB
TypeScript

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<string, ExecutorOptions>,
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<string>;
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<TypescriptCompilationResult>,
nextFn: (
iterator: AsyncIterableIterator<TypescriptCompilationResult>
) => Promise<IteratorResult<BatchExecutorTaskResult>>
) {
return yield* {
[Symbol.asyncIterator]() {
const iterator: AsyncIterableIterator<TypescriptCompilationResult> =
iterable[Symbol.asyncIterator].call(iterable);
return {
async next() {
return await nextFn(iterator);
},
};
},
};
}
function createTypescriptCompilationContext(
tsConfigTaskInfoMap: Record<string, TaskInfo>,
taskInMemoryTsConfigMap: Record<string, TypescriptInMemoryTsConfig>,
context: ExecutorContext
): Record<string, TypescriptProjectContext> {
const tsCompilationContext: Record<string, TypescriptProjectContext> =
Object.entries(tsConfigTaskInfoMap).reduce((acc, [tsConfig, taskInfo]) => {
acc[tsConfig] = {
project: taskInfo.context.projectName,
tsConfig: taskInfo.tsConfig,
transformers: taskInfo.options.transformers,
};
return acc;
}, {} as Record<string, TypescriptProjectContext>);
Object.entries(taskInMemoryTsConfigMap).forEach(([task, tsConfig]) => {
if (!tsCompilationContext[tsConfig.path]) {
tsCompilationContext[tsConfig.path] = {
project: parseTargetString(task, context).project,
transformers: [],
tsConfig: tsConfig,
};
}
});
return tsCompilationContext;
}