feat(js): add initial/experimental batch execution implementation for tsc executor (#17287)
This commit is contained in:
parent
fe27b87327
commit
b6cdf9d975
@ -706,7 +706,7 @@ A change to be made to a string
|
||||
|
||||
### TaskGraphExecutor
|
||||
|
||||
Ƭ **TaskGraphExecutor**<`T`\>: (`taskGraph`: [`TaskGraph`](../../devkit/documents/nx_devkit#taskgraph), `options`: `Record`<`string`, `T`\>, `overrides`: `T`, `context`: [`ExecutorContext`](../../devkit/documents/nx_devkit#executorcontext)) => `Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
Ƭ **TaskGraphExecutor**<`T`\>: (`taskGraph`: [`TaskGraph`](../../devkit/documents/nx_devkit#taskgraph), `options`: `Record`<`string`, `T`\>, `overrides`: `T`, `context`: [`ExecutorContext`](../../devkit/documents/nx_devkit#executorcontext)) => `Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
#### Type parameters
|
||||
|
||||
@ -716,7 +716,7 @@ A change to be made to a string
|
||||
|
||||
#### Type declaration
|
||||
|
||||
▸ (`taskGraph`, `options`, `overrides`, `context`): `Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
▸ (`taskGraph`, `options`, `overrides`, `context`): `Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
Implementation of a target of a project that handles multiple projects to be batched
|
||||
|
||||
@ -731,7 +731,7 @@ Implementation of a target of a project that handles multiple projects to be bat
|
||||
|
||||
##### Returns
|
||||
|
||||
`Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
`Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -706,7 +706,7 @@ A change to be made to a string
|
||||
|
||||
### TaskGraphExecutor
|
||||
|
||||
Ƭ **TaskGraphExecutor**<`T`\>: (`taskGraph`: [`TaskGraph`](../../devkit/documents/nx_devkit#taskgraph), `options`: `Record`<`string`, `T`\>, `overrides`: `T`, `context`: [`ExecutorContext`](../../devkit/documents/nx_devkit#executorcontext)) => `Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
Ƭ **TaskGraphExecutor**<`T`\>: (`taskGraph`: [`TaskGraph`](../../devkit/documents/nx_devkit#taskgraph), `options`: `Record`<`string`, `T`\>, `overrides`: `T`, `context`: [`ExecutorContext`](../../devkit/documents/nx_devkit#executorcontext)) => `Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
#### Type parameters
|
||||
|
||||
@ -716,7 +716,7 @@ A change to be made to a string
|
||||
|
||||
#### Type declaration
|
||||
|
||||
▸ (`taskGraph`, `options`, `overrides`, `context`): `Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
▸ (`taskGraph`, `options`, `overrides`, `context`): `Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
Implementation of a target of a project that handles multiple projects to be batched
|
||||
|
||||
@ -731,7 +731,7 @@ Implementation of a target of a project that handles multiple projects to be bat
|
||||
|
||||
##### Returns
|
||||
|
||||
`Promise`<`Record`<`string`, { `success`: `boolean` ; `terminalOutput`: `string` }\>\>
|
||||
`Promise`<`Record`<`string`, `ExecutorTaskResult`\> \| `AsyncIterableIterator`<`Record`<`string`, `ExecutorTaskResult`\>\>\>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "tsc",
|
||||
"implementation": "/packages/js/src/executors/tsc/tsc.impl.ts",
|
||||
"batchImplementation": "./src/executors/tsc/tsc.batch-impl",
|
||||
"schema": {
|
||||
"version": 2,
|
||||
"outputCapture": "direct-nodejs",
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
packageManagerLockFile,
|
||||
readFile,
|
||||
readJson,
|
||||
rmDist,
|
||||
runCLI,
|
||||
runCLIAsync,
|
||||
runCommand,
|
||||
@ -149,6 +150,48 @@ describe('js e2e', () => {
|
||||
expect(readJson(`dist/libs/${lib}/package.json`)).not.toHaveProperty(
|
||||
'peerDependencies.tslib'
|
||||
);
|
||||
|
||||
// check batch build
|
||||
rmDist();
|
||||
const batchBuildOutput = runCLI(`build ${parentLib} --skip-nx-cache`, {
|
||||
env: { NX_BATCH_MODE: 'true' },
|
||||
});
|
||||
|
||||
expect(batchBuildOutput).toContain(`Running 2 tasks with @nx/js:tsc`);
|
||||
expect(batchBuildOutput).toContain(
|
||||
`Compiling TypeScript files for project "${lib}"...`
|
||||
);
|
||||
expect(batchBuildOutput).toContain(
|
||||
`Done compiling TypeScript files for project "${lib}".`
|
||||
);
|
||||
expect(batchBuildOutput).toContain(
|
||||
`Compiling TypeScript files for project "${parentLib}"...`
|
||||
);
|
||||
expect(batchBuildOutput).toContain(
|
||||
`Done compiling TypeScript files for project "${parentLib}".`
|
||||
);
|
||||
expect(batchBuildOutput).toContain(
|
||||
`Successfully ran target build for project ${parentLib} and 1 task it depends on`
|
||||
);
|
||||
|
||||
checkFilesExist(
|
||||
// parent
|
||||
`dist/libs/${parentLib}/package.json`,
|
||||
`dist/libs/${parentLib}/README.md`,
|
||||
`dist/libs/${parentLib}/tsconfig.tsbuildinfo`,
|
||||
`dist/libs/${parentLib}/src/index.js`,
|
||||
`dist/libs/${parentLib}/src/index.d.ts`,
|
||||
`dist/libs/${parentLib}/src/lib/${parentLib}.js`,
|
||||
`dist/libs/${parentLib}/src/lib/${parentLib}.d.ts`,
|
||||
// child
|
||||
`dist/libs/${lib}/package.json`,
|
||||
`dist/libs/${lib}/README.md`,
|
||||
`dist/libs/${lib}/tsconfig.tsbuildinfo`,
|
||||
`dist/libs/${lib}/src/index.js`,
|
||||
`dist/libs/${lib}/src/index.d.ts`,
|
||||
`dist/libs/${lib}/src/lib/${lib}.js`,
|
||||
`dist/libs/${lib}/src/lib/${lib}.d.ts`
|
||||
);
|
||||
}, 240_000);
|
||||
|
||||
it('should not create a `.babelrc` file when creating libs with js executors (--compiler=tsc)', () => {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"executors": {
|
||||
"tsc": {
|
||||
"implementation": "./src/executors/tsc/tsc.impl",
|
||||
"batchImplementation": "./src/executors/tsc/tsc.batch-impl",
|
||||
"schema": "./src/executors/tsc/schema.json",
|
||||
"description": "Build a project using TypeScript."
|
||||
},
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
import type { ExecutorContext } from '@nx/devkit';
|
||||
import { parseTargetString } from '@nx/devkit';
|
||||
import { CopyAssetsHandler } from '../../../../utils/assets/copy-assets-handler';
|
||||
import { calculateProjectDependencies } from '../../../../utils/buildable-libs-utils';
|
||||
import type { NormalizedExecutorOptions } from '../../../../utils/schema';
|
||||
import { generateTempTsConfig } from './generate-temp-tsconfig';
|
||||
import { getTaskOptions } from './get-task-options';
|
||||
import type { TaskInfo } from './types';
|
||||
|
||||
const taskTsConfigCache = new Set<string>();
|
||||
|
||||
export function buildTaskInfoPerTsConfigMap(
|
||||
tsConfigTaskInfoMap: Record<string, TaskInfo>,
|
||||
tasksOptions: Record<string, NormalizedExecutorOptions>,
|
||||
context: ExecutorContext,
|
||||
tasks: string[],
|
||||
shouldWatch: boolean
|
||||
): void {
|
||||
for (const taskName of tasks) {
|
||||
if (taskTsConfigCache.has(taskName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let taskOptions = tasksOptions[taskName];
|
||||
// task is in the batch (it's meant to be processed), create TaskInfo
|
||||
if (taskOptions) {
|
||||
const taskInfo = createTaskInfo(taskName, taskOptions, context);
|
||||
|
||||
const tsConfigPath = generateTempTsConfig(
|
||||
tasksOptions,
|
||||
taskName,
|
||||
taskOptions,
|
||||
context
|
||||
);
|
||||
|
||||
tsConfigTaskInfoMap[tsConfigPath] = taskInfo;
|
||||
taskTsConfigCache.add(taskName);
|
||||
} else {
|
||||
// if it's not included in the provided map, it could be a cached task and
|
||||
// we need to pull the options from the relevant project graph node
|
||||
taskOptions = getTaskOptions(taskName, context);
|
||||
generateTempTsConfig(tasksOptions, taskName, taskOptions, context);
|
||||
}
|
||||
|
||||
buildTaskInfoPerTsConfigMap(
|
||||
tsConfigTaskInfoMap,
|
||||
tasksOptions,
|
||||
context,
|
||||
context.taskGraph.dependencies[taskName],
|
||||
shouldWatch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskInfo(
|
||||
taskName: string,
|
||||
taskOptions: NormalizedExecutorOptions,
|
||||
context: ExecutorContext
|
||||
): TaskInfo {
|
||||
const target = parseTargetString(taskName, context.projectGraph);
|
||||
|
||||
const taskContext = {
|
||||
...context,
|
||||
// batch executors don't get these in the context, we provide them
|
||||
// here per task
|
||||
projectName: target.project,
|
||||
targetName: target.target,
|
||||
configurationName: target.configuration,
|
||||
};
|
||||
|
||||
const assetsHandler = new CopyAssetsHandler({
|
||||
projectDir: taskOptions.projectRoot,
|
||||
rootDir: context.root,
|
||||
outputDir: taskOptions.outputPath,
|
||||
assets: taskOptions.assets,
|
||||
});
|
||||
|
||||
const {
|
||||
target: projectGraphNode,
|
||||
dependencies: buildableProjectNodeDependencies,
|
||||
} = calculateProjectDependencies(
|
||||
context.projectGraph,
|
||||
context.root,
|
||||
context.taskGraph.tasks[taskName].target.project,
|
||||
context.taskGraph.tasks[taskName].target.target,
|
||||
context.taskGraph.tasks[taskName].target.configuration
|
||||
);
|
||||
|
||||
return {
|
||||
task: taskName,
|
||||
options: taskOptions,
|
||||
context: taskContext,
|
||||
assetsHandler,
|
||||
buildableProjectNodeDependencies,
|
||||
projectGraphNode,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import type { ExecutorContext } from '@nx/devkit';
|
||||
import { joinPathFragments, normalizePath, writeJsonFile } from '@nx/devkit';
|
||||
import { dirname, join, relative } from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import type { NormalizedExecutorOptions } from '../../../../utils/schema';
|
||||
import { getTaskOptions } from './get-task-options';
|
||||
|
||||
export function generateTempTsConfig(
|
||||
taskOptionsMap: Record<string, NormalizedExecutorOptions>,
|
||||
taskName: string,
|
||||
taskOptions: NormalizedExecutorOptions,
|
||||
context: ExecutorContext
|
||||
): string {
|
||||
const tmpDir = join(context.root, 'tmp');
|
||||
const originalTsConfigPath = taskOptions.tsConfig;
|
||||
const tmpTsConfigPath = join(
|
||||
tmpDir,
|
||||
relative(context.root, originalTsConfigPath)
|
||||
);
|
||||
|
||||
const projectReferences: { path: string }[] = [];
|
||||
for (const depTask of context.taskGraph.dependencies[taskName]) {
|
||||
// if included in the provided map, use it
|
||||
if (taskOptionsMap[depTask]) {
|
||||
projectReferences.push({
|
||||
path: join(
|
||||
tmpDir,
|
||||
relative(context.root, taskOptionsMap[depTask].tsConfig)
|
||||
),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// if it's not included in the provided map, it could be a cached task and
|
||||
// we need to pull the tsconfig from the relevant project graph node
|
||||
const options = getTaskOptions(depTask, context);
|
||||
if (options.tsConfig) {
|
||||
projectReferences.push({
|
||||
path: join(tmpDir, relative(context.root, options.tsConfig)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
writeJsonFile(tmpTsConfigPath, {
|
||||
extends: normalizePath(
|
||||
relative(dirname(tmpTsConfigPath), taskOptions.tsConfig)
|
||||
),
|
||||
compilerOptions: {
|
||||
rootDir: taskOptions.rootDir,
|
||||
outDir: taskOptions.outputPath,
|
||||
composite: true,
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
tsBuildInfoFile: joinPathFragments(
|
||||
taskOptions.outputPath,
|
||||
'tsconfig.tsbuildinfo'
|
||||
),
|
||||
},
|
||||
references: projectReferences,
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure the temp tsconfig has the same modified date as the original.
|
||||
* Typescript compares this against the modified date of the tsbuildinfo
|
||||
* file. If the tsbuildinfo file is older, the cache is invalidated.
|
||||
* Since we always generate the temp tsconfig, any existing tsbuildinfo
|
||||
* file will be older even if they are not older than the original tsconfig.
|
||||
*/
|
||||
ts.sys.setModifiedTime(
|
||||
tmpTsConfigPath,
|
||||
ts.sys.getModifiedTime(originalTsConfigPath)
|
||||
);
|
||||
|
||||
return tmpTsConfigPath;
|
||||
}
|
||||
43
packages/js/src/executors/tsc/lib/batch/get-task-options.ts
Normal file
43
packages/js/src/executors/tsc/lib/batch/get-task-options.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { ExecutorContext } from '@nx/devkit';
|
||||
import { parseTargetString } from '@nx/devkit';
|
||||
import type {
|
||||
ExecutorOptions,
|
||||
NormalizedExecutorOptions,
|
||||
} from '../../../../utils/schema';
|
||||
import { normalizeOptions } from '../normalize-options';
|
||||
|
||||
const tasksOptionsCache = new Map<string, NormalizedExecutorOptions>();
|
||||
|
||||
export function getTaskOptions(
|
||||
taskName: string,
|
||||
context: ExecutorContext
|
||||
): NormalizedExecutorOptions {
|
||||
if (tasksOptionsCache.has(taskName)) {
|
||||
return tasksOptionsCache.get(taskName);
|
||||
}
|
||||
|
||||
const target = context.taskGraph.tasks[taskName].target;
|
||||
const projectNode = context.projectGraph.nodes[target.project];
|
||||
const targetConfig = projectNode.data.targets?.[target.target];
|
||||
|
||||
const taskOptions: ExecutorOptions = {
|
||||
...targetConfig.options,
|
||||
...(target.configuration
|
||||
? targetConfig.configurations?.[target.configuration]
|
||||
: {}),
|
||||
};
|
||||
|
||||
const { project } = parseTargetString(taskName, context.projectGraph);
|
||||
const { sourceRoot, root } = context.projectsConfigurations.projects[project];
|
||||
|
||||
const normalizedTaskOptions = normalizeOptions(
|
||||
taskOptions,
|
||||
context.root,
|
||||
sourceRoot,
|
||||
root
|
||||
);
|
||||
|
||||
tasksOptionsCache.set(taskName, normalizedTaskOptions);
|
||||
|
||||
return normalizedTaskOptions;
|
||||
}
|
||||
8
packages/js/src/executors/tsc/lib/batch/index.ts
Normal file
8
packages/js/src/executors/tsc/lib/batch/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export * from './build-task-info-per-tsconfig-map';
|
||||
export * from './generate-temp-tsconfig';
|
||||
export * from './get-task-options';
|
||||
export * from './normalize-tasks-options';
|
||||
export * from './types';
|
||||
export * from './typescript-compilation';
|
||||
export * from './typescript-diagnostic-reporters';
|
||||
export * from './watch';
|
||||
@ -0,0 +1,25 @@
|
||||
import type { ExecutorContext } from '@nx/devkit';
|
||||
import { parseTargetString } from '@nx/devkit';
|
||||
import type {
|
||||
ExecutorOptions,
|
||||
NormalizedExecutorOptions,
|
||||
} from '../../../../utils/schema';
|
||||
import { normalizeOptions } from '../normalize-options';
|
||||
|
||||
export function normalizeTasksOptions(
|
||||
inputs: Record<string, ExecutorOptions>,
|
||||
context: ExecutorContext
|
||||
): Record<string, NormalizedExecutorOptions> {
|
||||
return Object.entries(inputs).reduce((tasksOptions, [taskName, options]) => {
|
||||
const { project } = parseTargetString(taskName, context.projectGraph);
|
||||
const { sourceRoot, root } =
|
||||
context.projectsConfigurations.projects[project];
|
||||
tasksOptions[taskName] = normalizeOptions(
|
||||
options,
|
||||
context.root,
|
||||
sourceRoot,
|
||||
root
|
||||
);
|
||||
return tasksOptions;
|
||||
}, {} as Record<string, NormalizedExecutorOptions>);
|
||||
}
|
||||
13
packages/js/src/executors/tsc/lib/batch/types.ts
Normal file
13
packages/js/src/executors/tsc/lib/batch/types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { ExecutorContext, ProjectGraphProjectNode } from '@nx/devkit';
|
||||
import type { CopyAssetsHandler } from '../../../../utils/assets/copy-assets-handler';
|
||||
import type { DependentBuildableProjectNode } from '../../../../utils/buildable-libs-utils';
|
||||
import type { NormalizedExecutorOptions } from '../../../../utils/schema';
|
||||
|
||||
export interface TaskInfo {
|
||||
task: string;
|
||||
options: NormalizedExecutorOptions;
|
||||
context: ExecutorContext;
|
||||
assetsHandler: CopyAssetsHandler;
|
||||
buildableProjectNodeDependencies: DependentBuildableProjectNode[];
|
||||
projectGraphNode: ProjectGraphProjectNode;
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
import type { TaskGraph } from '@nx/devkit';
|
||||
import { logger } from '@nx/devkit';
|
||||
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
|
||||
import type { BatchResults } from 'nx/src/tasks-runner/batch/batch-messages';
|
||||
import * as ts from 'typescript';
|
||||
import { getCustomTrasformersFactory } from '../get-custom-transformers-factory';
|
||||
import type { TaskInfo } from './types';
|
||||
import {
|
||||
formatDiagnosticReport,
|
||||
formatSolutionBuilderStatusReport,
|
||||
formatWatchStatusReport,
|
||||
} from './typescript-diagnostic-reporters';
|
||||
|
||||
// https://github.com/microsoft/TypeScript/blob/d45012c5e2ab122919ee4777a7887307c5f4a1e0/src/compiler/diagnosticMessages.json#L4050-L4053
|
||||
// Typescript diagnostic message for 5083: Cannot read file '{0}'.
|
||||
const TYPESCRIPT_CANNOT_READ_FILE = 5083;
|
||||
// https://github.com/microsoft/TypeScript/blob/d45012c5e2ab122919ee4777a7887307c5f4a1e0/src/compiler/diagnosticMessages.json#L4211-4214
|
||||
// Typescript diagnostic message for 6032: File change detected. Starting incremental compilation...
|
||||
const TYPESCRIPT_FILE_CHANGE_DETECTED_STARTING_INCREMENTAL_COMPILATION = 6032;
|
||||
|
||||
export function compileBatchTypescript(
|
||||
tsConfigTaskInfoMap: Record<string, TaskInfo>,
|
||||
taskGraph: TaskGraph,
|
||||
watch: boolean,
|
||||
postProjectCompilationCallback: (taskInfo: TaskInfo) => void
|
||||
): {
|
||||
iterator: AsyncIterable<BatchResults>;
|
||||
close: () => void | Promise<void>;
|
||||
} {
|
||||
const timeNow = Date.now();
|
||||
const defaultResults: BatchResults = Object.keys(taskGraph.tasks).reduce(
|
||||
(acc, task) => {
|
||||
acc[task] = { success: true, startTime: timeNow, terminalOutput: '' };
|
||||
return acc;
|
||||
},
|
||||
{} as BatchResults
|
||||
);
|
||||
|
||||
let tearDown: (() => void) | undefined;
|
||||
|
||||
return {
|
||||
iterator: createAsyncIterable<BatchResults>(({ next, done }) => {
|
||||
if (watch) {
|
||||
compileTSWithWatch(tsConfigTaskInfoMap, postProjectCompilationCallback);
|
||||
|
||||
tearDown = () => {
|
||||
done();
|
||||
};
|
||||
} else {
|
||||
const compilationResults = compileTS(
|
||||
tsConfigTaskInfoMap,
|
||||
postProjectCompilationCallback
|
||||
);
|
||||
next({
|
||||
...defaultResults,
|
||||
...compilationResults,
|
||||
});
|
||||
done();
|
||||
}
|
||||
}),
|
||||
close: () => tearDown?.(),
|
||||
};
|
||||
}
|
||||
|
||||
function compileTSWithWatch(
|
||||
tsConfigTaskInfoMap: Record<string, TaskInfo>,
|
||||
postProjectCompilationCallback: (taskInfo: TaskInfo) => void
|
||||
) {
|
||||
const formatDiagnosticsHost: ts.FormatDiagnosticsHost = {
|
||||
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
|
||||
getNewLine: () => ts.sys.newLine,
|
||||
getCanonicalFileName: (filename: string) =>
|
||||
ts.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase(),
|
||||
};
|
||||
const solutionHost = ts.createSolutionBuilderWithWatchHost(
|
||||
ts.sys,
|
||||
/*createProgram*/ undefined,
|
||||
(diagnostic) => {
|
||||
const formattedDiagnostic = formatDiagnosticReport(
|
||||
diagnostic,
|
||||
formatDiagnosticsHost
|
||||
);
|
||||
logger.info(formattedDiagnostic);
|
||||
},
|
||||
(diagnostic) => {
|
||||
const formattedDiagnostic = formatSolutionBuilderStatusReport(diagnostic);
|
||||
logger.info(formattedDiagnostic);
|
||||
},
|
||||
(diagnostic, newLine) => {
|
||||
const formattedDiagnostic = formatWatchStatusReport(diagnostic, newLine);
|
||||
logger.info(formattedDiagnostic);
|
||||
|
||||
if (
|
||||
diagnostic.code ===
|
||||
TYPESCRIPT_FILE_CHANGE_DETECTED_STARTING_INCREMENTAL_COMPILATION
|
||||
) {
|
||||
// there's a change, build invalidated projects
|
||||
build();
|
||||
}
|
||||
}
|
||||
);
|
||||
const rootNames = Object.keys(tsConfigTaskInfoMap);
|
||||
const solutionBuilder = ts.createSolutionBuilderWithWatch(
|
||||
solutionHost,
|
||||
rootNames,
|
||||
{}
|
||||
);
|
||||
|
||||
const build = () => {
|
||||
while (true) {
|
||||
const project = solutionBuilder.getNextInvalidatedProject();
|
||||
if (!project) {
|
||||
break;
|
||||
}
|
||||
|
||||
const taskInfo = tsConfigTaskInfoMap[project.project];
|
||||
|
||||
if (project.kind === ts.InvalidatedProjectKind.UpdateOutputFileStamps) {
|
||||
// update output timestamps and mark project as complete
|
||||
project.done();
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* This only applies when the deprecated `prepend` option is set to `true`.
|
||||
* Skip support.
|
||||
*/
|
||||
if (project.kind === ts.InvalidatedProjectKind.UpdateBundle) {
|
||||
logger.warn(
|
||||
`The project ${taskInfo.context.projectName} ` +
|
||||
`is using the deprecated "prepend" Typescript compiler option. ` +
|
||||
`This option is not supported by the batch executor and it's ignored.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// build and mark project as complete
|
||||
project.done(
|
||||
undefined,
|
||||
undefined,
|
||||
getCustomTrasformersFactory(taskInfo.options.transformers)(
|
||||
project.getProgram()
|
||||
)
|
||||
);
|
||||
postProjectCompilationCallback(taskInfo);
|
||||
}
|
||||
};
|
||||
|
||||
// initial build
|
||||
build();
|
||||
|
||||
/**
|
||||
* This is a workaround to get the TS file watching to kick off. It won't
|
||||
* build twice since the `build` call above will mark invalidated projects
|
||||
* as completed and then, the implementation of the `solutionBuilder.build`
|
||||
* skips them.
|
||||
* We can't rely solely in `solutionBuilder.build()` because it doesn't
|
||||
* accept custom transformers.
|
||||
*/
|
||||
solutionBuilder.build();
|
||||
|
||||
return solutionHost;
|
||||
}
|
||||
|
||||
function compileTS(
|
||||
tsConfigTaskInfoMap: Record<string, TaskInfo>,
|
||||
postProjectCompilationCallback: (taskInfo: TaskInfo) => void
|
||||
): BatchResults {
|
||||
const results: BatchResults = {};
|
||||
|
||||
let terminalOutput: string;
|
||||
const logInfo = (text: string): void => {
|
||||
logger.info(text);
|
||||
terminalOutput += `${text}\n`;
|
||||
};
|
||||
const logWarn = (text: string): void => {
|
||||
logger.warn(text);
|
||||
terminalOutput += `${text}\n`;
|
||||
};
|
||||
|
||||
const formatDiagnosticsHost: ts.FormatDiagnosticsHost = {
|
||||
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
|
||||
getNewLine: () => ts.sys.newLine,
|
||||
getCanonicalFileName: (filename: string) =>
|
||||
ts.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase(),
|
||||
};
|
||||
const solutionBuilderHost = ts.createSolutionBuilderHost(
|
||||
ts.sys,
|
||||
/*createProgram*/ undefined,
|
||||
(diagnostic) => {
|
||||
const formattedDiagnostic = formatDiagnosticReport(
|
||||
diagnostic,
|
||||
formatDiagnosticsHost
|
||||
);
|
||||
|
||||
// handles edge case where a wrong a project reference path can't be read
|
||||
if (diagnostic.code === TYPESCRIPT_CANNOT_READ_FILE) {
|
||||
Object.values(tsConfigTaskInfoMap).forEach((taskInfo) => {
|
||||
results[taskInfo.task] ??= { success: false, terminalOutput: '' };
|
||||
results[taskInfo.task].success = false;
|
||||
results[taskInfo.task].terminalOutput = `${
|
||||
results[taskInfo.task]?.terminalOutput
|
||||
}${formattedDiagnostic}`;
|
||||
});
|
||||
}
|
||||
|
||||
logInfo(formattedDiagnostic);
|
||||
},
|
||||
(diagnostic) => {
|
||||
const formattedDiagnostic = formatSolutionBuilderStatusReport(diagnostic);
|
||||
logInfo(formattedDiagnostic);
|
||||
}
|
||||
);
|
||||
const rootNames = Object.keys(tsConfigTaskInfoMap);
|
||||
const solutionBuilder = ts.createSolutionBuilder(
|
||||
solutionBuilderHost,
|
||||
rootNames,
|
||||
{}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const project = solutionBuilder.getNextInvalidatedProject();
|
||||
if (!project) {
|
||||
break;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
terminalOutput = '';
|
||||
const taskInfo = tsConfigTaskInfoMap[project.project];
|
||||
const projectName = taskInfo?.context?.projectName;
|
||||
|
||||
if (project.kind === ts.InvalidatedProjectKind.UpdateOutputFileStamps) {
|
||||
if (projectName) {
|
||||
logInfo(`Updating output timestamps of project "${projectName}"...`);
|
||||
}
|
||||
|
||||
// update output timestamps and mark project as complete
|
||||
const status = project.done();
|
||||
|
||||
if (projectName && status === ts.ExitStatus.Success) {
|
||||
logInfo(
|
||||
`Done updating output timestamps of project "${projectName}"...`
|
||||
);
|
||||
}
|
||||
|
||||
if (taskInfo) {
|
||||
results[taskInfo.task] = {
|
||||
success: status === ts.ExitStatus.Success,
|
||||
terminalOutput,
|
||||
startTime,
|
||||
endTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* This only applies when the deprecated `prepend` option is set to `true`.
|
||||
* Skip support.
|
||||
*/
|
||||
if (project.kind === ts.InvalidatedProjectKind.UpdateBundle) {
|
||||
logWarn(
|
||||
`The project ${taskInfo.context.projectName} ` +
|
||||
`is using the deprecated "prepend" Typescript compiler option. ` +
|
||||
`This option is not supported by the batch executor and it's ignored.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logInfo(`Compiling TypeScript files for project "${projectName}"...`);
|
||||
// build and mark project as complete
|
||||
const status = project.done(
|
||||
undefined,
|
||||
undefined,
|
||||
getCustomTrasformersFactory(taskInfo.options.transformers)(
|
||||
project.getProgram()
|
||||
)
|
||||
);
|
||||
postProjectCompilationCallback(taskInfo);
|
||||
|
||||
if (status === ts.ExitStatus.Success) {
|
||||
logInfo(`Done compiling TypeScript files for project "${projectName}".`);
|
||||
}
|
||||
|
||||
results[taskInfo.task] = {
|
||||
success: status === ts.ExitStatus.Success,
|
||||
terminalOutput,
|
||||
startTime,
|
||||
endTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
// adapted from TS default diagnostic reporter
|
||||
export function formatDiagnosticReport(
|
||||
diagnostic: ts.Diagnostic,
|
||||
host: ts.FormatDiagnosticsHost
|
||||
): string {
|
||||
const diagnostics: ts.Diagnostic[] = new Array(1);
|
||||
diagnostics[0] = diagnostic;
|
||||
const formattedDiagnostic =
|
||||
'\n' +
|
||||
ts.formatDiagnosticsWithColorAndContext(diagnostics, host) +
|
||||
host.getNewLine();
|
||||
diagnostics[0] = undefined;
|
||||
|
||||
return formattedDiagnostic;
|
||||
}
|
||||
|
||||
// adapted from TS default solution builder status reporter
|
||||
export function formatSolutionBuilderStatusReport(
|
||||
diagnostic: ts.Diagnostic
|
||||
): string {
|
||||
let formattedDiagnostic = `[${formatColorAndReset(
|
||||
getLocaleTimeString(),
|
||||
ForegroundColorEscapeSequences.Grey
|
||||
)}] `;
|
||||
formattedDiagnostic += `${ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
ts.sys.newLine
|
||||
)}${ts.sys.newLine + ts.sys.newLine}`;
|
||||
|
||||
return formattedDiagnostic;
|
||||
}
|
||||
|
||||
// adapted from TS default watch status reporter
|
||||
export function formatWatchStatusReport(
|
||||
diagnostic: ts.Diagnostic,
|
||||
newLine: string
|
||||
): string {
|
||||
let output = `[${formatColorAndReset(
|
||||
getLocaleTimeString(),
|
||||
ForegroundColorEscapeSequences.Grey
|
||||
)}] `;
|
||||
output += `${ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
ts.sys.newLine
|
||||
)}${newLine + newLine}`;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function formatColorAndReset(text: string, formatStyle: string) {
|
||||
const resetEscapeSequence = '\u001b[0m';
|
||||
return formatStyle + text + resetEscapeSequence;
|
||||
}
|
||||
|
||||
function getLocaleTimeString() {
|
||||
return new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
enum ForegroundColorEscapeSequences {
|
||||
Grey = '\u001b[90m',
|
||||
Red = '\u001b[91m',
|
||||
Yellow = '\u001b[93m',
|
||||
Blue = '\u001b[94m',
|
||||
Cyan = '\u001b[96m',
|
||||
}
|
||||
67
packages/js/src/executors/tsc/lib/batch/watch.ts
Normal file
67
packages/js/src/executors/tsc/lib/batch/watch.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { logger } from '@nx/devkit';
|
||||
import { daemonClient } from 'nx/src/daemon/client/client';
|
||||
import { join } from 'path';
|
||||
import type { TaskInfo } from './types';
|
||||
|
||||
export async function watchTaskProjectsPackageJsonFileChanges(
|
||||
taskInfos: TaskInfo[],
|
||||
callback: (changedTaskInfos: TaskInfo[]) => void
|
||||
): Promise<() => void> {
|
||||
const projects: string[] = [];
|
||||
const packageJsonTaskInfoMap = new Map<string, TaskInfo>();
|
||||
taskInfos.forEach((t) => {
|
||||
projects.push(t.context.projectName);
|
||||
packageJsonTaskInfoMap.set(join(t.options.projectRoot, 'package.json'), t);
|
||||
});
|
||||
|
||||
const unregisterFileWatcher = await daemonClient.registerFileWatcher(
|
||||
{ watchProjects: projects },
|
||||
(err, data) => {
|
||||
if (err === 'closed') {
|
||||
logger.error(`Watch error: Daemon closed the connection`);
|
||||
process.exit(1);
|
||||
} else if (err) {
|
||||
logger.error(`Watch error: ${err?.message ?? 'Unknown'}`);
|
||||
} else {
|
||||
const changedTasks = [];
|
||||
data.changedFiles.forEach((file) => {
|
||||
if (packageJsonTaskInfoMap.has(file.path)) {
|
||||
changedTasks.push(packageJsonTaskInfoMap.get(file.path));
|
||||
}
|
||||
});
|
||||
|
||||
if (changedTasks.length) {
|
||||
callback(changedTasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => unregisterFileWatcher();
|
||||
}
|
||||
|
||||
export async function watchTaskProjectsFileChangesForAssets(
|
||||
taskInfos: TaskInfo[]
|
||||
): Promise<() => void> {
|
||||
const unregisterFileWatcher = await daemonClient.registerFileWatcher(
|
||||
{
|
||||
watchProjects: taskInfos.map((t) => t.context.projectName),
|
||||
includeDependentProjects: true,
|
||||
includeGlobalWorkspaceFiles: true,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err === 'closed') {
|
||||
logger.error(`Watch error: Daemon closed the connection`);
|
||||
process.exit(1);
|
||||
} else if (err) {
|
||||
logger.error(`Watch error: ${err?.message ?? 'Unknown'}`);
|
||||
} else {
|
||||
taskInfos.forEach((t) =>
|
||||
t.assetsHandler.processWatchEvents(data.changedFiles)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => unregisterFileWatcher();
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import * as ts from 'typescript';
|
||||
import { loadTsTransformers } from '../../../utils/typescript/load-ts-transformers';
|
||||
import type { TransformerEntry } from '../../../utils/typescript/types';
|
||||
|
||||
export function getCustomTrasformersFactory(
|
||||
transformers: TransformerEntry[]
|
||||
): (program: ts.Program) => ts.CustomTransformers {
|
||||
const { compilerPluginHooks } = loadTsTransformers(transformers);
|
||||
|
||||
return (program: ts.Program): ts.CustomTransformers => ({
|
||||
before: compilerPluginHooks.beforeHooks.map(
|
||||
(hook) => hook(program) as ts.TransformerFactory<ts.SourceFile>
|
||||
),
|
||||
after: compilerPluginHooks.afterHooks.map(
|
||||
(hook) => hook(program) as ts.TransformerFactory<ts.SourceFile>
|
||||
),
|
||||
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
|
||||
(hook) => hook(program) as ts.TransformerFactory<ts.SourceFile>
|
||||
),
|
||||
});
|
||||
}
|
||||
2
packages/js/src/executors/tsc/lib/index.ts
Normal file
2
packages/js/src/executors/tsc/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './get-custom-transformers-factory';
|
||||
export * from './normalize-options';
|
||||
60
packages/js/src/executors/tsc/lib/normalize-options.ts
Normal file
60
packages/js/src/executors/tsc/lib/normalize-options.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { join, resolve } from 'path';
|
||||
import type {
|
||||
ExecutorOptions,
|
||||
NormalizedExecutorOptions,
|
||||
} from '../../../utils/schema';
|
||||
import {
|
||||
FileInputOutput,
|
||||
assetGlobsToFiles,
|
||||
} from '../../../utils/assets/assets';
|
||||
|
||||
export function normalizeOptions(
|
||||
options: ExecutorOptions,
|
||||
contextRoot: string,
|
||||
sourceRoot: string,
|
||||
projectRoot: string
|
||||
): NormalizedExecutorOptions {
|
||||
const outputPath = join(contextRoot, options.outputPath);
|
||||
const rootDir = options.rootDir
|
||||
? join(contextRoot, options.rootDir)
|
||||
: join(contextRoot, projectRoot);
|
||||
|
||||
if (options.watch == null) {
|
||||
options.watch = false;
|
||||
}
|
||||
|
||||
// TODO: put back when inlining story is more stable
|
||||
// if (options.external == null) {
|
||||
// options.external = 'all';
|
||||
// } else if (Array.isArray(options.external) && options.external.length === 0) {
|
||||
// options.external = 'none';
|
||||
// }
|
||||
|
||||
if (Array.isArray(options.external) && options.external.length > 0) {
|
||||
const firstItem = options.external[0];
|
||||
if (firstItem === 'all' || firstItem === 'none') {
|
||||
options.external = firstItem;
|
||||
}
|
||||
}
|
||||
|
||||
const files: FileInputOutput[] = assetGlobsToFiles(
|
||||
options.assets,
|
||||
contextRoot,
|
||||
outputPath
|
||||
);
|
||||
|
||||
return {
|
||||
...options,
|
||||
root: contextRoot,
|
||||
sourceRoot,
|
||||
projectRoot,
|
||||
files,
|
||||
outputPath,
|
||||
tsConfig: join(contextRoot, options.tsConfig),
|
||||
rootDir,
|
||||
mainOutputPath: resolve(
|
||||
outputPath,
|
||||
options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
|
||||
),
|
||||
};
|
||||
}
|
||||
88
packages/js/src/executors/tsc/tsc.batch-impl.ts
Normal file
88
packages/js/src/executors/tsc/tsc.batch-impl.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { ExecutorContext, TaskGraph } from '@nx/devkit';
|
||||
import { rmSync } from 'fs';
|
||||
import { updatePackageJson } from '../../utils/package-json/update-package-json';
|
||||
import type { ExecutorOptions } from '../../utils/schema';
|
||||
import type { TaskInfo } from './lib/batch';
|
||||
import {
|
||||
buildTaskInfoPerTsConfigMap,
|
||||
compileBatchTypescript,
|
||||
normalizeTasksOptions,
|
||||
watchTaskProjectsFileChangesForAssets,
|
||||
watchTaskProjectsPackageJsonFileChanges,
|
||||
} from './lib/batch';
|
||||
|
||||
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 tsConfigTaskInfoMap: Record<string, TaskInfo> = {};
|
||||
buildTaskInfoPerTsConfigMap(
|
||||
tsConfigTaskInfoMap,
|
||||
tasksOptions,
|
||||
context,
|
||||
Object.keys(taskGraph.tasks),
|
||||
shouldWatch
|
||||
);
|
||||
|
||||
const typescriptCompilation = compileBatchTypescript(
|
||||
tsConfigTaskInfoMap,
|
||||
taskGraph,
|
||||
shouldWatch,
|
||||
(taskInfo) => {
|
||||
taskInfo.assetsHandler.processAllAssetsOnceSync();
|
||||
updatePackageJson(
|
||||
taskInfo.options,
|
||||
taskInfo.context,
|
||||
taskInfo.projectGraphNode,
|
||||
taskInfo.buildableProjectNodeDependencies
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
t.context,
|
||||
t.projectGraphNode,
|
||||
t.buildableProjectNodeDependencies
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleTermination = async (exitCode: number) => {
|
||||
await typescriptCompilation.close();
|
||||
watchAssetsChangesDisposer();
|
||||
watchProjectsChangesDisposer();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
process.on('SIGINT', () => handleTermination(128 + 2));
|
||||
process.on('SIGTERM', () => handleTermination(128 + 15));
|
||||
}
|
||||
|
||||
return yield* typescriptCompilation.iterator;
|
||||
}
|
||||
|
||||
export default tscBatchExecutor;
|
||||
@ -1,9 +1,7 @@
|
||||
import { ExecutorContext } from '@nx/devkit';
|
||||
import { ExecutorOptions } from '../../utils/schema';
|
||||
import {
|
||||
createTypeScriptCompilationOptions,
|
||||
normalizeOptions,
|
||||
} from './tsc.impl';
|
||||
import { normalizeOptions } from './lib';
|
||||
import { createTypeScriptCompilationOptions } from './tsc.impl';
|
||||
|
||||
describe('tscExecutor', () => {
|
||||
let context: ExecutorContext;
|
||||
@ -38,19 +36,14 @@ describe('tscExecutor', () => {
|
||||
describe('createTypeScriptCompilationOptions', () => {
|
||||
it('should create typescript compilation options for valid config', () => {
|
||||
const result = createTypeScriptCompilationOptions(
|
||||
normalizeOptions(
|
||||
testOptions,
|
||||
'/root',
|
||||
'/root/libs/ui/src',
|
||||
'/root/libs/ui'
|
||||
),
|
||||
normalizeOptions(testOptions, '/root', 'libs/ui/src', 'libs/ui'),
|
||||
context
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
outputPath: '/root/dist/libs/ui',
|
||||
projectName: 'example',
|
||||
projectRoot: '/root/libs/ui',
|
||||
projectRoot: 'libs/ui',
|
||||
rootDir: '/root/libs/ui',
|
||||
tsConfig: '/root/libs/ui/tsconfig.json',
|
||||
watch: false,
|
||||
@ -63,8 +56,8 @@ describe('tscExecutor', () => {
|
||||
normalizeOptions(
|
||||
{ ...testOptions, rootDir: 'libs/ui/src' },
|
||||
'/root',
|
||||
'/root/libs/ui/src',
|
||||
'/root/libs/ui'
|
||||
'libs/ui/src',
|
||||
'libs/ui'
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
import { ExecutorContext } from '@nx/devkit';
|
||||
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
|
||||
import type { TypeScriptCompilationOptions } from '@nx/workspace/src/utilities/typescript/compilation';
|
||||
import { join, resolve } from 'path';
|
||||
import {
|
||||
CustomTransformers,
|
||||
Program,
|
||||
SourceFile,
|
||||
TransformerFactory,
|
||||
} from 'typescript';
|
||||
import { CopyAssetsHandler } from '../../utils/assets/copy-assets-handler';
|
||||
import { checkDependencies } from '../../utils/check-dependencies';
|
||||
import {
|
||||
@ -22,79 +14,13 @@ import {
|
||||
import { updatePackageJson } from '../../utils/package-json/update-package-json';
|
||||
import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema';
|
||||
import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files';
|
||||
import { loadTsTransformers } from '../../utils/typescript/load-ts-transformers';
|
||||
import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes';
|
||||
|
||||
export function normalizeOptions(
|
||||
options: ExecutorOptions,
|
||||
contextRoot: string,
|
||||
sourceRoot: string,
|
||||
projectRoot: string
|
||||
): NormalizedExecutorOptions {
|
||||
const outputPath = join(contextRoot, options.outputPath);
|
||||
const rootDir = options.rootDir
|
||||
? join(contextRoot, options.rootDir)
|
||||
: projectRoot;
|
||||
|
||||
if (options.watch == null) {
|
||||
options.watch = false;
|
||||
}
|
||||
|
||||
// TODO: put back when inlining story is more stable
|
||||
// if (options.external == null) {
|
||||
// options.external = 'all';
|
||||
// } else if (Array.isArray(options.external) && options.external.length === 0) {
|
||||
// options.external = 'none';
|
||||
// }
|
||||
|
||||
if (Array.isArray(options.external) && options.external.length > 0) {
|
||||
const firstItem = options.external[0];
|
||||
if (firstItem === 'all' || firstItem === 'none') {
|
||||
options.external = firstItem;
|
||||
}
|
||||
}
|
||||
|
||||
const files: FileInputOutput[] = assetGlobsToFiles(
|
||||
options.assets,
|
||||
contextRoot,
|
||||
outputPath
|
||||
);
|
||||
|
||||
return {
|
||||
...options,
|
||||
root: contextRoot,
|
||||
sourceRoot,
|
||||
projectRoot,
|
||||
files,
|
||||
outputPath,
|
||||
tsConfig: join(contextRoot, options.tsConfig),
|
||||
rootDir,
|
||||
mainOutputPath: resolve(
|
||||
outputPath,
|
||||
options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
|
||||
),
|
||||
};
|
||||
}
|
||||
import { getCustomTrasformersFactory, normalizeOptions } from './lib';
|
||||
|
||||
export function createTypeScriptCompilationOptions(
|
||||
normalizedOptions: NormalizedExecutorOptions,
|
||||
context: ExecutorContext
|
||||
): TypeScriptCompilationOptions {
|
||||
const { compilerPluginHooks } = loadTsTransformers(
|
||||
normalizedOptions.transformers
|
||||
);
|
||||
const getCustomTransformers = (program: Program): CustomTransformers => ({
|
||||
before: compilerPluginHooks.beforeHooks.map(
|
||||
(hook) => hook(program) as TransformerFactory<SourceFile>
|
||||
),
|
||||
after: compilerPluginHooks.afterHooks.map(
|
||||
(hook) => hook(program) as TransformerFactory<SourceFile>
|
||||
),
|
||||
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
|
||||
(hook) => hook(program) as TransformerFactory<SourceFile>
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
outputPath: normalizedOptions.outputPath,
|
||||
projectName: context.projectName,
|
||||
@ -103,7 +29,9 @@ export function createTypeScriptCompilationOptions(
|
||||
tsConfig: normalizedOptions.tsConfig,
|
||||
watch: normalizedOptions.watch,
|
||||
deleteOutputPath: normalizedOptions.clean,
|
||||
getCustomTransformers,
|
||||
getCustomTransformers: getCustomTrasformersFactory(
|
||||
normalizedOptions.transformers
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -104,12 +104,7 @@ export class CopyAssetsHandler {
|
||||
async processAllAssetsOnce(): Promise<void> {
|
||||
await Promise.all(
|
||||
this.assetGlobs.map(async (ag) => {
|
||||
let pattern: string;
|
||||
if (typeof ag === 'string') {
|
||||
pattern = ag;
|
||||
} else {
|
||||
pattern = ag.pattern;
|
||||
}
|
||||
const pattern = this.normalizeAssetPattern(ag);
|
||||
|
||||
// fast-glob only supports Unix paths
|
||||
const files = await fg(pattern.replace(/\\/g, '/'), {
|
||||
@ -117,27 +112,25 @@ export class CopyAssetsHandler {
|
||||
dot: true, // enable hidden files
|
||||
});
|
||||
|
||||
this.callback(
|
||||
files.reduce((acc, src) => {
|
||||
if (
|
||||
!ag.ignore?.some((ig) => minimatch(src, ig)) &&
|
||||
!this.ignore.ignores(src)
|
||||
) {
|
||||
const relPath = path.relative(ag.input, src);
|
||||
const dest = relPath.startsWith('..') ? src : relPath;
|
||||
acc.push({
|
||||
type: 'create',
|
||||
src: path.join(this.rootDir, src),
|
||||
dest: path.join(this.rootDir, ag.output, dest),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
this.callback(this.filesToEvent(files, ag));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
processAllAssetsOnceSync(): void {
|
||||
this.assetGlobs.forEach((ag) => {
|
||||
const pattern = this.normalizeAssetPattern(ag);
|
||||
|
||||
// fast-glob only supports Unix paths
|
||||
const files = fg.sync(pattern.replace(/\\/g, '/'), {
|
||||
cwd: this.rootDir,
|
||||
dot: true, // enable hidden files
|
||||
});
|
||||
|
||||
this.callback(this.filesToEvent(files, ag));
|
||||
});
|
||||
}
|
||||
|
||||
async watchAndProcessOnAssetChange(): Promise<() => void> {
|
||||
const unregisterFileWatcher = await daemonClient.registerFileWatcher(
|
||||
{
|
||||
@ -151,7 +144,7 @@ export class CopyAssetsHandler {
|
||||
} else if (err) {
|
||||
logger.error(`Watch error: ${err?.message ?? 'Unknown'}`);
|
||||
} else {
|
||||
this.processEvents(data.changedFiles);
|
||||
this.processWatchEvents(data.changedFiles);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -159,7 +152,7 @@ export class CopyAssetsHandler {
|
||||
return () => unregisterFileWatcher();
|
||||
}
|
||||
|
||||
private async processEvents(events: ChangedFile[]): Promise<void> {
|
||||
async processWatchEvents(events: ChangedFile[]): Promise<void> {
|
||||
const fileEvents: FileEvent[] = [];
|
||||
for (const event of events) {
|
||||
const pathFromRoot = path.relative(this.rootDir, event.path);
|
||||
@ -184,4 +177,26 @@ export class CopyAssetsHandler {
|
||||
|
||||
if (fileEvents.length > 0) this.callback(fileEvents);
|
||||
}
|
||||
|
||||
private filesToEvent(files: string[], assetGlob: AssetEntry): FileEvent[] {
|
||||
return files.reduce((acc, src) => {
|
||||
if (
|
||||
!assetGlob.ignore?.some((ig) => minimatch(src, ig)) &&
|
||||
!this.ignore.ignores(src)
|
||||
) {
|
||||
const relPath = path.relative(assetGlob.input, src);
|
||||
const dest = relPath.startsWith('..') ? src : relPath;
|
||||
acc.push({
|
||||
type: 'create',
|
||||
src: path.join(this.rootDir, src),
|
||||
dest: path.join(this.rootDir, assetGlob.output, dest),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private normalizeAssetPattern(assetEntry: AssetEntry): string {
|
||||
return typeof assetEntry === 'string' ? assetEntry : assetEntry.pattern;
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,12 +32,18 @@ export function isInlineGraphEmpty(inlineGraph: InlineProjectGraph): boolean {
|
||||
export function handleInliningBuild(
|
||||
context: ExecutorContext,
|
||||
options: NormalizedExecutorOptions,
|
||||
tsConfigPath: string
|
||||
tsConfigPath: string,
|
||||
projectName: string = context.projectName
|
||||
): InlineProjectGraph {
|
||||
const tsConfigJson = readJsonFile(tsConfigPath);
|
||||
const pathAliases =
|
||||
tsConfigJson['compilerOptions']?.['paths'] || readBasePathAliases(context);
|
||||
const inlineGraph = createInlineGraph(context, options, pathAliases);
|
||||
const inlineGraph = createInlineGraph(
|
||||
context,
|
||||
options,
|
||||
pathAliases,
|
||||
projectName
|
||||
);
|
||||
|
||||
if (isInlineGraphEmpty(inlineGraph)) {
|
||||
return inlineGraph;
|
||||
@ -136,7 +142,7 @@ function createInlineGraph(
|
||||
context: ExecutorContext,
|
||||
options: NormalizedExecutorOptions,
|
||||
pathAliases: Record<string, string[]>,
|
||||
projectName: string = context.projectName,
|
||||
projectName: string,
|
||||
inlineGraph: InlineProjectGraph = emptyInlineGraph()
|
||||
) {
|
||||
if (options.external == null) return inlineGraph;
|
||||
|
||||
5
packages/js/src/utils/schema.d.ts
vendored
5
packages/js/src/utils/schema.d.ts
vendored
@ -50,11 +50,12 @@ export interface ExecutorOptions {
|
||||
}
|
||||
|
||||
export interface NormalizedExecutorOptions extends ExecutorOptions {
|
||||
root?: string;
|
||||
sourceRoot?: string;
|
||||
rootDir: string;
|
||||
projectRoot: string;
|
||||
mainOutputPath: string;
|
||||
files: Array<FileInputOutput>;
|
||||
root?: string;
|
||||
sourceRoot?: string;
|
||||
}
|
||||
|
||||
export interface SwcExecutorOptions extends ExecutorOptions {
|
||||
|
||||
@ -28,6 +28,10 @@ import {
|
||||
} from '../../project-graph/project-graph';
|
||||
import { ProjectGraph } from '../../config/project-graph';
|
||||
import { readNxJson } from '../../config/configuration';
|
||||
import {
|
||||
getLastValueFromAsyncIterableIterator,
|
||||
isAsyncIterator,
|
||||
} from '../../utils/async-iterator';
|
||||
|
||||
export interface Target {
|
||||
project: string;
|
||||
@ -62,12 +66,6 @@ function isPromise<T extends { success: boolean }>(
|
||||
return typeof (v as any)?.then === 'function';
|
||||
}
|
||||
|
||||
function isAsyncIterator<T extends { success: boolean }>(
|
||||
v: Promise<{ success: boolean }> | AsyncIterableIterator<T>
|
||||
): v is AsyncIterableIterator<T> {
|
||||
return typeof (v as any)?.[Symbol.asyncIterator] === 'function';
|
||||
}
|
||||
|
||||
async function* promiseToIterator<T extends { success: boolean }>(
|
||||
v: Promise<T>
|
||||
): AsyncIterableIterator<T> {
|
||||
@ -77,8 +75,6 @@ async function* promiseToIterator<T extends { success: boolean }>(
|
||||
async function iteratorToProcessStatusCode(
|
||||
i: AsyncIterableIterator<{ success: boolean }>
|
||||
): Promise<number> {
|
||||
let success: boolean;
|
||||
|
||||
// This is a workaround to fix an issue that only happens with
|
||||
// the @angular-devkit/build-angular:browser builder. Starting
|
||||
// on version 12.0.1, a SASS compilation implementation was
|
||||
@ -87,18 +83,7 @@ async function iteratorToProcessStatusCode(
|
||||
// like CI or when running Docker builds.
|
||||
const keepProcessAliveInterval = setInterval(() => {}, 1000);
|
||||
try {
|
||||
let prev: IteratorResult<{ success: boolean }>;
|
||||
let current: IteratorResult<{ success: boolean }>;
|
||||
do {
|
||||
prev = current;
|
||||
current = await i.next();
|
||||
} while (!current.done);
|
||||
|
||||
success =
|
||||
current.value !== undefined || !prev
|
||||
? current.value.success
|
||||
: prev.value.success;
|
||||
|
||||
const { success } = await getLastValueFromAsyncIterableIterator(i);
|
||||
return success ? 0 : 1;
|
||||
} finally {
|
||||
clearInterval(keepProcessAliveInterval);
|
||||
|
||||
@ -130,6 +130,13 @@ export type CustomHasher = (
|
||||
context: HasherContext
|
||||
) => Promise<Hash>;
|
||||
|
||||
export type ExecutorTaskResult = {
|
||||
success: boolean;
|
||||
terminalOutput: string;
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation of a target of a project that handles multiple projects to be batched
|
||||
*/
|
||||
@ -147,7 +154,10 @@ export type TaskGraphExecutor<T = any> = (
|
||||
*/
|
||||
overrides: T,
|
||||
context: ExecutorContext
|
||||
) => Promise<Record<string, { success: boolean; terminalOutput: string }>>;
|
||||
) => Promise<
|
||||
| Record<string, ExecutorTaskResult>
|
||||
| AsyncIterableIterator<Record<string, ExecutorTaskResult>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Context that is passed into an executor
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { TaskGraph } from '../../config/task-graph';
|
||||
import type { ExecutorTaskResult } from '../../config/misc-interfaces';
|
||||
import type { TaskGraph } from '../../config/task-graph';
|
||||
|
||||
export enum BatchMessageType {
|
||||
Tasks,
|
||||
@ -11,17 +12,14 @@ export interface BatchTasksMessage {
|
||||
batchTaskGraph: TaskGraph;
|
||||
fullTaskGraph: TaskGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Results of running the batch. Mapped from task id to results
|
||||
*/
|
||||
export interface BatchResults {
|
||||
[taskId: string]: {
|
||||
success: boolean;
|
||||
terminalOutput?: string;
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
};
|
||||
[taskId: string]: ExecutorTaskResult;
|
||||
}
|
||||
|
||||
export interface BatchCompleteMessage {
|
||||
type: BatchMessageType.Complete;
|
||||
results: BatchResults;
|
||||
|
||||
@ -13,6 +13,10 @@ import {
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
} from '../../project-graph/project-graph';
|
||||
import { readNxJson } from '../../config/configuration';
|
||||
import {
|
||||
getLastValueFromAsyncIterableIterator,
|
||||
isAsyncIterator,
|
||||
} from '../../utils/async-iterator';
|
||||
|
||||
function getBatchExecutor(executorName: string) {
|
||||
const workspace = new Workspaces(workspaceRoot);
|
||||
@ -69,6 +73,10 @@ async function runTasks(
|
||||
throw new Error(`"${executorName} returned invalid results: ${results}`);
|
||||
}
|
||||
|
||||
if (isAsyncIterator(results)) {
|
||||
return await getLastValueFromAsyncIterableIterator(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
const isVerbose = tasks[0].overrides.verbose;
|
||||
|
||||
@ -71,6 +71,7 @@ export class ForkedProcessTaskRunner {
|
||||
for (const rootTaskId of batchTaskGraph.roots) {
|
||||
results[rootTaskId] = {
|
||||
success: false,
|
||||
terminalOutput: '',
|
||||
};
|
||||
}
|
||||
rej(
|
||||
@ -84,6 +85,14 @@ export class ForkedProcessTaskRunner {
|
||||
p.on('message', (message: BatchMessage) => {
|
||||
switch (message.type) {
|
||||
case BatchMessageType.Complete: {
|
||||
Object.entries(message.results).forEach(([taskName, result]) => {
|
||||
this.options.lifeCycle.printTaskTerminalOutput(
|
||||
batchTaskGraph.tasks[taskName],
|
||||
result.success ? 'success' : 'failure',
|
||||
result.terminalOutput
|
||||
);
|
||||
});
|
||||
|
||||
res(message.results);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -40,11 +40,12 @@ export class StoreRunInformationLifeCycle implements LifeCycle {
|
||||
taskResults: Array<{ task: Task; status: TaskStatus; code: number }>
|
||||
): void {
|
||||
for (let tr of taskResults) {
|
||||
if (tr.task.endTime && tr.task.startTime) {
|
||||
if (tr.task.startTime) {
|
||||
this.timings[tr.task.id].start = new Date(
|
||||
tr.task.startTime
|
||||
).toISOString();
|
||||
|
||||
}
|
||||
if (tr.task.endTime) {
|
||||
this.timings[tr.task.id].end = new Date(tr.task.endTime).toISOString();
|
||||
} else {
|
||||
this.timings[tr.task.id].end = this.now();
|
||||
|
||||
@ -37,8 +37,10 @@ export class TaskProfilingLifeCycle implements LifeCycle {
|
||||
metadata: TaskMetadata
|
||||
): void {
|
||||
for (let tr of taskResults) {
|
||||
if (tr.task.endTime && tr.task.startTime) {
|
||||
if (tr.task.startTime) {
|
||||
this.timings[tr.task.id].perfStart = tr.task.startTime;
|
||||
}
|
||||
if (tr.task.endTime) {
|
||||
this.timings[tr.task.id].perfEnd = tr.task.endTime;
|
||||
} else {
|
||||
this.timings[tr.task.id].perfEnd = performance.now();
|
||||
|
||||
@ -27,8 +27,10 @@ export class TaskTimingsLifeCycle implements LifeCycle {
|
||||
}>
|
||||
): void {
|
||||
for (let tr of taskResults) {
|
||||
if (tr.task.endTime && tr.task.startTime) {
|
||||
if (tr.task.startTime) {
|
||||
this.timings[tr.task.id].start = tr.task.startTime;
|
||||
}
|
||||
if (tr.task.endTime) {
|
||||
this.timings[tr.task.id].end = tr.task.endTime;
|
||||
} else {
|
||||
this.timings[tr.task.id].end = new Date().getTime();
|
||||
|
||||
@ -144,6 +144,7 @@ export class TaskOrchestrator {
|
||||
task: Task;
|
||||
status: 'local-cache' | 'local-cache-kept-existing' | 'remote-cache';
|
||||
}> {
|
||||
const startTime = new Date().getTime();
|
||||
const cachedResult = await this.cache.get(task);
|
||||
if (!cachedResult || cachedResult.code !== 0) return null;
|
||||
|
||||
@ -165,7 +166,11 @@ export class TaskOrchestrator {
|
||||
cachedResult.terminalOutput
|
||||
);
|
||||
return {
|
||||
task,
|
||||
task: {
|
||||
...task,
|
||||
startTime,
|
||||
endTime: Date.now(),
|
||||
},
|
||||
status,
|
||||
};
|
||||
}
|
||||
@ -235,7 +240,11 @@ export class TaskOrchestrator {
|
||||
const batchResultEntries = Object.entries(results);
|
||||
return batchResultEntries.map(([taskId, result]) => ({
|
||||
...result,
|
||||
task: this.taskGraph.tasks[taskId],
|
||||
task: {
|
||||
...this.taskGraph.tasks[taskId],
|
||||
startTime: result.startTime,
|
||||
endTime: result.endTime,
|
||||
},
|
||||
status: (result.success ? 'success' : 'failure') as TaskStatus,
|
||||
terminalOutput: result.terminalOutput,
|
||||
}));
|
||||
|
||||
16
packages/nx/src/utils/async-iterator.ts
Normal file
16
packages/nx/src/utils/async-iterator.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export function isAsyncIterator<T>(v: any): v is AsyncIterableIterator<T> {
|
||||
return typeof v?.[Symbol.asyncIterator] === 'function';
|
||||
}
|
||||
|
||||
export async function getLastValueFromAsyncIterableIterator<T>(
|
||||
i: AsyncIterableIterator<T>
|
||||
): Promise<T> {
|
||||
let prev: IteratorResult<T, T>;
|
||||
let current: IteratorResult<T, T>;
|
||||
do {
|
||||
prev = current;
|
||||
current = await i.next();
|
||||
} while (!current.done);
|
||||
|
||||
return current.value !== undefined || !prev ? current.value : prev.value;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user