diff --git a/docs/generated/cli/show.md b/docs/generated/cli/show.md index c321d452b2bd05..155464723a2af4 100644 --- a/docs/generated/cli/show.md +++ b/docs/generated/cli/show.md @@ -165,6 +165,12 @@ Type: `boolean` Untracked changes +##### verbose + +Type: `boolean` + +Prints additional information about the commands (e.g., stack traces) + ##### version Type: `boolean` @@ -199,6 +205,12 @@ Type: `string` Which project should be viewed? +##### verbose + +Type: `boolean` + +Prints additional information about the commands (e.g., stack traces) + ##### version Type: `boolean` diff --git a/docs/generated/packages/nx/documents/show.md b/docs/generated/packages/nx/documents/show.md index c321d452b2bd05..155464723a2af4 100644 --- a/docs/generated/packages/nx/documents/show.md +++ b/docs/generated/packages/nx/documents/show.md @@ -165,6 +165,12 @@ Type: `boolean` Untracked changes +##### verbose + +Type: `boolean` + +Prints additional information about the commands (e.g., stack traces) + ##### version Type: `boolean` @@ -199,6 +205,12 @@ Type: `string` Which project should be viewed? +##### verbose + +Type: `boolean` + +Prints additional information about the commands (e.g., stack traces) + ##### version Type: `boolean` diff --git a/packages/nx/src/command-line/affected/command-object.ts b/packages/nx/src/command-line/affected/command-object.ts index 8cf5757790bc41..454780c29b0712 100644 --- a/packages/nx/src/command-line/affected/command-object.ts +++ b/packages/nx/src/command-line/affected/command-object.ts @@ -1,4 +1,4 @@ -import { boolean, CommandModule, middleware } from 'yargs'; +import { CommandModule } from 'yargs'; import { linkToNxDevAndExamples } from '../yargs-utils/documentation'; import { withAffectedOptions, @@ -10,6 +10,7 @@ import { withRunOptions, withTargetAndConfigurationOption, } from '../yargs-utils/shared-options'; +import { handleErrors } from '../../utils/params'; export const yargsAffectedCommand: CommandModule = { command: 'affected', @@ -36,8 +37,17 @@ export const yargsAffectedCommand: CommandModule = { }), 'affected' ), - handler: async (args) => - (await import('./affected')).affected('affected', withOverrides(args)), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./affected')).affected( + 'affected', + withOverrides(args) + ); + } + ); + }, }; export const yargsAffectedTestCommand: CommandModule = { @@ -50,11 +60,17 @@ export const yargsAffectedTestCommand: CommandModule = { ), 'affected' ), - handler: async (args) => - (await import('./affected')).affected('affected', { - ...withOverrides(args), - target: 'test', - }), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./affected')).affected('affected', { + ...withOverrides(args), + target: 'test', + }); + } + ); + }, }; export const yargsAffectedBuildCommand: CommandModule = { @@ -67,11 +83,17 @@ export const yargsAffectedBuildCommand: CommandModule = { ), 'affected' ), - handler: async (args) => - (await import('./affected')).affected('affected', { - ...withOverrides(args), - target: 'build', - }), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./affected')).affected('affected', { + ...withOverrides(args), + target: 'build', + }); + } + ); + }, }; export const yargsAffectedLintCommand: CommandModule = { @@ -84,11 +106,17 @@ export const yargsAffectedLintCommand: CommandModule = { ), 'affected' ), - handler: async (args) => - (await import('./affected')).affected('affected', { - ...withOverrides(args), - target: 'lint', - }), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./affected')).affected('affected', { + ...withOverrides(args), + target: 'lint', + }); + } + ); + }, }; export const yargsAffectedE2ECommand: CommandModule = { @@ -101,11 +129,17 @@ export const yargsAffectedE2ECommand: CommandModule = { ), 'affected' ), - handler: async (args) => - (await import('./affected')).affected('affected', { - ...withOverrides(args), - target: 'e2e', - }), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./affected')).affected('affected', { + ...withOverrides(args), + target: 'e2e', + }); + } + ); + }, }; export const affectedGraphDeprecationMessage = @@ -122,12 +156,18 @@ export const yargsAffectedGraphCommand: CommandModule = { withAffectedOptions(withDepGraphOptions(yargs)), 'affected:graph' ), - handler: async (args) => - await ( - await import('./affected') - ).affected('graph', { - ...args, - }), + handler: async (args) => { + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return await ( + await import('./affected') + ).affected('graph', { + ...args, + }); + } + ); + }, deprecated: affectedGraphDeprecationMessage, }; @@ -157,10 +197,15 @@ export const yargsPrintAffectedCommand: CommandModule = { 'Select the type of projects to be returned (e.g., --type=app)', }), handler: async (args) => { - await ( - await import('./affected') - ).affected('print-affected', withOverrides(args)); - process.exit(0); + return handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + await ( + await import('./affected') + ).affected('print-affected', withOverrides(args)); + process.exit(0); + } + ); }, deprecated: printAffectedDeprecationMessage, }; diff --git a/packages/nx/src/command-line/generate/generate.ts b/packages/nx/src/command-line/generate/generate.ts index b0de98758108dc..de19ab9cc2313a 100644 --- a/packages/nx/src/command-line/generate/generate.ts +++ b/packages/nx/src/command-line/generate/generate.ts @@ -279,6 +279,7 @@ function throwInvalidInvocation(availableGenerators: string[]) { )})` ); } + export function printGenHelp( opts: GenerateOptions, schema: Schema, @@ -306,12 +307,11 @@ export async function generate(cwd: string, args: { [k: string]: any }) { } const verbose = process.env.NX_VERBOSE_LOGGING === 'true'; - const nxJsonConfiguration = readNxJson(); - const projectGraph = await createProjectGraphAsync({ exitOnError: true }); - const projectsConfigurations = - readProjectsConfigurationFromProjectGraph(projectGraph); - return handleErrors(verbose, async () => { + const nxJsonConfiguration = readNxJson(); + const projectGraph = await createProjectGraphAsync(); + const projectsConfigurations = + readProjectsConfigurationFromProjectGraph(projectGraph); const opts = await convertToGenerateOptions( args, 'generate', diff --git a/packages/nx/src/command-line/run-many/command-object.ts b/packages/nx/src/command-line/run-many/command-object.ts index afe7167d8d0488..4cc6ff2f89043c 100644 --- a/packages/nx/src/command-line/run-many/command-object.ts +++ b/packages/nx/src/command-line/run-many/command-object.ts @@ -7,6 +7,7 @@ import { withOverrides, withBatch, } from '../yargs-utils/shared-options'; +import { handleErrors } from '../../utils/params'; export const yargsRunManyCommand: CommandModule = { command: 'run-many', @@ -21,5 +22,10 @@ export const yargsRunManyCommand: CommandModule = { 'run-many' ), handler: async (args) => - (await import('./run-many')).runMany(withOverrides(args)), + await handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + (await import('./run-many')).runMany(withOverrides(args)); + } + ), }; diff --git a/packages/nx/src/command-line/run/command-object.ts b/packages/nx/src/command-line/run/command-object.ts index 4425b517c8a271..571dfd33499b50 100644 --- a/packages/nx/src/command-line/run/command-object.ts +++ b/packages/nx/src/command-line/run/command-object.ts @@ -4,6 +4,7 @@ import { withOverrides, withRunOneOptions, } from '../yargs-utils/shared-options'; +import { handleErrors } from '../../utils/params'; export const yargsRunCommand: CommandModule = { command: 'run [project][:target][:configuration] [_..]', @@ -16,7 +17,12 @@ export const yargsRunCommand: CommandModule = { You can skip the use of Nx cache by using the --skip-nx-cache option.`, builder: (yargs) => withRunOneOptions(withBatch(yargs)), handler: async (args) => - (await import('./run-one')).runOne(process.cwd(), withOverrides(args)), + await handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + (await import('./run-one')).runOne(process.cwd(), withOverrides(args)); + } + ), }; /** @@ -26,6 +32,15 @@ export const yargsNxInfixCommand: CommandModule = { ...yargsRunCommand, command: '$0 [project] [_..]', describe: 'Run a target for a project', - handler: async (args) => - (await import('./run-one')).runOne(process.cwd(), withOverrides(args, 0)), + handler: async (args) => { + await handleErrors( + (args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./run-one')).runOne( + process.cwd(), + withOverrides(args, 0) + ); + } + ); + }, }; diff --git a/packages/nx/src/command-line/run/run-one.ts b/packages/nx/src/command-line/run/run-one.ts index b7fb30a4317dd2..e9f935f389512d 100644 --- a/packages/nx/src/command-line/run/run-one.ts +++ b/packages/nx/src/command-line/run/run-one.ts @@ -37,7 +37,7 @@ export async function runOne( workspaceConfigurationCheck(); const nxJson = readNxJson(); - const projectGraph = await createProjectGraphAsync({ exitOnError: true }); + const projectGraph = await createProjectGraphAsync(); const opts = parseRunOneOptions(cwd, args, projectGraph, nxJson); diff --git a/packages/nx/src/command-line/show/command-object.ts b/packages/nx/src/command-line/show/command-object.ts index 0909ee9c4f2b18..387c15e9596b6f 100644 --- a/packages/nx/src/command-line/show/command-object.ts +++ b/packages/nx/src/command-line/show/command-object.ts @@ -1,6 +1,7 @@ import type { ProjectGraphProjectNode } from '../../config/project-graph'; import { CommandModule, showHelp } from 'yargs'; import { parseCSV, withAffectedOptions } from '../yargs-utils/shared-options'; +import { handleErrors } from '../../utils/params'; export interface NxShowArgs { json?: boolean; @@ -17,11 +18,13 @@ export type ShowProjectsOptions = NxShowArgs & { type: ProjectGraphProjectNode['type']; projects: string[]; withTarget: string[]; + verbose: boolean; }; export type ShowProjectOptions = NxShowArgs & { projectName: string; web?: boolean; + verbose: boolean; }; export const yargsShowCommand: CommandModule< @@ -83,6 +86,11 @@ const showProjectsCommand: CommandModule = { description: 'Select only projects of the given type', choices: ['app', 'lib', 'e2e'], }) + .option('verbose', { + type: 'boolean', + description: + 'Prints additional information about the commands (e.g., stack traces)', + }) .implies('untracked', 'affected') .implies('uncommitted', 'affected') .implies('files', 'affected') @@ -108,7 +116,14 @@ const showProjectsCommand: CommandModule = { '$0 show projects --affected --exclude=*-e2e', 'Show affected projects in the workspace, excluding end-to-end projects' ) as any, - handler: (args) => import('./show').then((m) => m.showProjectsHandler(args)), + handler: (args) => { + return handleErrors( + args.verbose ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./show')).showProjectsHandler(args); + } + ); + }, }; const showProjectCommand: CommandModule = { @@ -126,6 +141,11 @@ const showProjectCommand: CommandModule = { type: 'boolean', description: 'Show project details in the browser', }) + .option('verbose', { + type: 'boolean', + description: + 'Prints additional information about the commands (e.g., stack traces)', + }) .check((argv) => { if (argv.web) { argv.json = false; @@ -136,5 +156,12 @@ const showProjectCommand: CommandModule = { '$0 show project my-app', 'View project information for my-app in JSON format' ), - handler: (args) => import('./show').then((m) => m.showProjectHandler(args)), + handler: (args) => { + return handleErrors( + args.verbose ?? process.env.NX_VERBOSE_LOGGING === 'true', + async () => { + return (await import('./show')).showProjectHandler(args); + } + ); + }, }; diff --git a/packages/nx/src/daemon/client/client.ts b/packages/nx/src/daemon/client/client.ts index a52a9cfd4ef991..7e54b14af59c6d 100644 --- a/packages/nx/src/daemon/client/client.ts +++ b/packages/nx/src/daemon/client/client.ts @@ -25,6 +25,8 @@ import { safelyCleanUpExistingProcess } from '../cache'; import { Hash } from '../../hasher/task-hasher'; import { Task, TaskGraph } from '../../config/task-graph'; import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils'; +import { DaemonProjectGraphError } from '../daemon-project-graph-error'; +import { ProjectGraphError } from '../../project-graph/project-graph'; const DAEMON_ENV_SETTINGS = { ...process.env, @@ -129,13 +131,21 @@ export class DaemonClient { projectGraph: ProjectGraph; sourceMaps: ConfigurationSourceMaps; }> { - const response = await this.sendToDaemonViaQueue({ - type: 'REQUEST_PROJECT_GRAPH', - }); - return { - projectGraph: response.projectGraph, - sourceMaps: response.sourceMaps, - }; + try { + const response = await this.sendToDaemonViaQueue({ + type: 'REQUEST_PROJECT_GRAPH', + }); + return { + projectGraph: response.projectGraph, + sourceMaps: response.sourceMaps, + }; + } catch (e) { + if (e.name === DaemonProjectGraphError.name) { + throw ProjectGraphError.fromDaemonProjectGraphError(e); + } else { + throw e; + } + } } async getAllFileData(): Promise { diff --git a/packages/nx/src/daemon/daemon-project-graph-error.ts b/packages/nx/src/daemon/daemon-project-graph-error.ts new file mode 100644 index 00000000000000..518af6db42d38d --- /dev/null +++ b/packages/nx/src/daemon/daemon-project-graph-error.ts @@ -0,0 +1,15 @@ +import { ProjectGraph } from '../config/project-graph'; +import { ConfigurationSourceMaps } from '../project-graph/utils/project-configuration-utils'; + +export class DaemonProjectGraphError extends Error { + constructor( + public errors: any[], + readonly projectGraph: ProjectGraph, + readonly sourceMaps: ConfigurationSourceMaps + ) { + super( + `The Daemon Process threw an error while calculating the project graph. Convert this error to a ProjectGraphError to get more information.` + ); + this.name = this.constructor.name; + } +} diff --git a/packages/nx/src/daemon/server/handle-hash-tasks.ts b/packages/nx/src/daemon/server/handle-hash-tasks.ts index 62f5670b8bbd66..c8cb7b7daec3e7 100644 --- a/packages/nx/src/daemon/server/handle-hash-tasks.ts +++ b/packages/nx/src/daemon/server/handle-hash-tasks.ts @@ -2,6 +2,7 @@ import { Task, TaskGraph } from '../../config/task-graph'; import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation'; import { InProcessTaskHasher } from '../../hasher/task-hasher'; import { readNxJson } from '../../config/configuration'; +import { DaemonProjectGraphError } from '../daemon-project-graph-error'; /** * We use this not to recreated hasher for every hash operation @@ -16,8 +17,23 @@ export async function handleHashTasks(payload: { tasks: Task[]; taskGraph: TaskGraph; }) { - const { projectGraph, allWorkspaceFiles, fileMap, rustReferences } = - await getCachedSerializedProjectGraphPromise(); + const { + error, + projectGraph: _graph, + allWorkspaceFiles, + fileMap, + rustReferences, + } = await getCachedSerializedProjectGraphPromise(); + + let projectGraph = _graph; + if (error) { + if (error instanceof DaemonProjectGraphError) { + projectGraph = error.projectGraph; + } else { + throw error; + } + } + const nxJson = readNxJson(); if (projectGraph !== storedProjectGraph) { diff --git a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts index c1a316de06c0f4..97392511f89a49 100644 --- a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts +++ b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts @@ -8,15 +8,18 @@ import { } from '../../config/project-graph'; import { ProjectConfiguration } from '../../config/workspace-json-project-json'; import { hashArray } from '../../hasher/file-hasher'; -import { buildProjectGraphUsingProjectFileMap as buildProjectGraphUsingFileMap } from '../../project-graph/build-project-graph'; +import { + buildProjectGraphUsingProjectFileMap as buildProjectGraphUsingFileMap, + CreateDependenciesError, +} from '../../project-graph/build-project-graph'; import { updateFileMap } from '../../project-graph/file-map-utils'; import { FileMapCache, nxProjectGraph, readFileMapCache, + writeCache, } from '../../project-graph/nx-deps-cache'; import { - RetrievedGraphNodes, retrieveProjectConfigurations, retrieveWorkspaceFiles, } from '../../project-graph/utils/retrieve-workspace-files'; @@ -29,10 +32,16 @@ import { workspaceRoot } from '../../utils/workspace-root'; import { notifyFileWatcherSockets } from './file-watching/file-watcher-sockets'; import { serverLogger } from './logger'; import { NxWorkspaceFilesExternals } from '../../native'; +import { + ConfigurationResult, + ProjectConfigurationsError, +} from '../../project-graph/utils/project-configuration-utils'; +import { DaemonProjectGraphError } from '../daemon-project-graph-error'; interface SerializedProjectGraph { error: Error | null; projectGraph: ProjectGraph | null; + projectFileMapCache: FileMapCache | null; fileMap: FileMap | null; allWorkspaceFiles: FileData[] | null; serializedProjectGraph: string | null; @@ -85,6 +94,7 @@ export async function getCachedSerializedProjectGraphPromise(): Promise, deletedFiles: string[] ) { @@ -219,29 +229,76 @@ async function processFilesAndCreateAndSerializeProjectGraph(): Promise 0) { + return { + error: new DaemonProjectGraphError( + errors, + g.projectGraph, + graphNodes.sourceMaps + ), + projectGraph: null, + projectFileMapCache: null, + fileMap: null, + rustReferences: null, + allWorkspaceFiles: null, + serializedProjectGraph: null, + serializedSourceMaps: null, + }; + } else { + writeCache(g.projectFileMapCache, g.projectGraph); + return g; + } } catch (err) { - return Promise.resolve({ + return { error: err, projectGraph: null, + projectFileMapCache: null, fileMap: null, rustReferences: null, allWorkspaceFiles: null, serializedProjectGraph: null, serializedSourceMaps: null, - }); + }; } } @@ -263,7 +320,7 @@ function copyFileMap(m: FileMap) { async function createAndSerializeProjectGraph({ projects, sourceMaps, -}: RetrievedGraphNodes): Promise { +}: ConfigurationResult): Promise { try { performance.mark('create-project-graph-start'); const fileMap = copyFileMap(fileMapWithFiles.fileMap); @@ -276,9 +333,9 @@ async function createAndSerializeProjectGraph({ fileMap, allWorkspaceFiles, rustReferences, - currentProjectFileMapCache || readFileMapCache(), - true + currentProjectFileMapCache || readFileMapCache() ); + currentProjectFileMapCache = projectFileMapCache; currentProjectGraph = projectGraph; @@ -302,6 +359,7 @@ async function createAndSerializeProjectGraph({ return { error: null, projectGraph, + projectFileMapCache, fileMap, allWorkspaceFiles, serializedProjectGraph, @@ -315,6 +373,7 @@ async function createAndSerializeProjectGraph({ return { error: e, projectGraph: null, + projectFileMapCache: null, fileMap: null, allWorkspaceFiles: null, serializedProjectGraph: null, diff --git a/packages/nx/src/daemon/server/shutdown-utils.ts b/packages/nx/src/daemon/server/shutdown-utils.ts index 22affc1b46a840..921b5a248f4b96 100644 --- a/packages/nx/src/daemon/server/shutdown-utils.ts +++ b/packages/nx/src/daemon/server/shutdown-utils.ts @@ -8,17 +8,21 @@ import type { Watcher } from '../../native'; export const SERVER_INACTIVITY_TIMEOUT_MS = 10800000 as const; // 10800000 ms = 3 hours let watcherInstance: Watcher | undefined; + export function storeWatcherInstance(instance: Watcher) { watcherInstance = instance; } + export function getWatcherInstance() { return watcherInstance; } let outputWatcherInstance: Watcher | undefined; + export function storeOutputWatcherInstance(instance: Watcher) { outputWatcherInstance = instance; } + export function getOutputWatcherInstance() { return outputWatcherInstance; } @@ -95,10 +99,7 @@ export async function respondWithErrorAndExit( description, error.message ); - console.error(error); - - error.message = `${error.message}\n\nBecause of the error the Nx daemon process has exited. The next Nx command is going to restart the daemon process.\nIf the error persists, please run "nx reset".`; + console.error(error.stack); await respondToClient(socket, serializeResult(error, null, null), null); - process.exit(1); } diff --git a/packages/nx/src/daemon/socket-utils.ts b/packages/nx/src/daemon/socket-utils.ts index 49bd03903e9cd5..9796bae866a74c 100644 --- a/packages/nx/src/daemon/socket-utils.ts +++ b/packages/nx/src/daemon/socket-utils.ts @@ -2,6 +2,7 @@ import { unlinkSync } from 'fs'; import { platform } from 'os'; import { join, resolve } from 'path'; import { DAEMON_SOCKET_PATH, socketDir } from './tmp-dir'; +import { DaemonProjectGraphError } from './daemon-project-graph-error'; export const isWindows = platform() === 'win32'; @@ -31,7 +32,14 @@ function serializeError(error: Error | null): string | null { if (!error) { return null; } - return JSON.stringify(error, Object.getOwnPropertyNames(error)); + + if (error instanceof DaemonProjectGraphError) { + error.errors = error.errors.map((e) => JSON.parse(serializeError(e))); + } + + return `{${Object.getOwnPropertyNames(error) + .map((k) => `"${k}": ${JSON.stringify(error[k])}`) + .join(',')}}`; } // Prepare a serialized project graph result for sending over IPC from the server to the client diff --git a/packages/nx/src/project-graph/build-project-graph.ts b/packages/nx/src/project-graph/build-project-graph.ts index babf9873812342..8e0a6103f52486 100644 --- a/packages/nx/src/project-graph/build-project-graph.ts +++ b/packages/nx/src/project-graph/build-project-graph.ts @@ -69,8 +69,7 @@ export async function buildProjectGraphUsingProjectFileMap( fileMap: FileMap, allWorkspaceFiles: FileData[], rustReferences: NxWorkspaceFilesExternals, - fileMapCache: FileMapCache | null, - shouldWriteCache: boolean + fileMapCache: FileMapCache | null ): Promise<{ projectGraph: ProjectGraph; projectFileMapCache: FileMapCache; @@ -116,7 +115,6 @@ export async function buildProjectGraphUsingProjectFileMap( filesToProcess ); let projectGraph = await buildProjectGraphUsingContext( - nxJson, externalNodes, context, cachedFileData, @@ -128,9 +126,6 @@ export async function buildProjectGraphUsingProjectFileMap( fileMap, rootTsConfig ); - if (shouldWriteCache) { - writeCache(projectFileMapCache, projectGraph); - } return { projectGraph, projectFileMapCache, @@ -162,7 +157,6 @@ function readCombinedDeps() { } async function buildProjectGraphUsingContext( - nxJson: NxJsonConfiguration, knownExternalNodes: Record, ctx: CreateDependenciesContext, cachedFileData: CachedFileData, @@ -179,9 +173,23 @@ async function buildProjectGraphUsingContext( await normalizeProjectNodes(ctx, builder); const initProjectGraph = builder.getUpdatedProjectGraph(); - const r = await updateProjectGraphWithPlugins(ctx, initProjectGraph); + let updatedGraph; + let error; + try { + updatedGraph = await updateProjectGraphWithPlugins(ctx, initProjectGraph); + } catch (e) { + if (e instanceof CreateDependenciesError) { + updatedGraph = e.partialProjectGraph; + error = e; + } else { + throw e; + } + } - const updatedBuilder = new ProjectGraphBuilder(r, ctx.fileMap.projectFileMap); + const updatedBuilder = new ProjectGraphBuilder( + updatedGraph, + ctx.fileMap.projectFileMap + ); for (const proj of Object.keys(cachedFileData.projectFileMap)) { for (const f of ctx.fileMap.projectFileMap[proj] || []) { const cached = cachedFileData.projectFileMap[proj][f.file]; @@ -208,7 +216,11 @@ async function buildProjectGraphUsingContext( 'build project graph:end' ); - return finalGraph; + if (!error) { + return finalGraph; + } else { + throw new CreateDependenciesError(error.errors, finalGraph); + } } function createContext( @@ -245,6 +257,7 @@ async function updateProjectGraphWithPlugins( context.projects ); let graph = initProjectGraph; + const errors: Array = []; for (const { plugin } of plugins) { try { if ( @@ -281,12 +294,11 @@ async function updateProjectGraphWithPlugins( ); } } catch (e) { - let message = `Failed to process the project graph with "${plugin.name}".`; - if (e instanceof Error) { - e.message = message + '\n' + e.message; - throw e; - } - throw new Error(message); + errors.push( + new ProcessProjectGraphError(plugin.name, { + cause: e, + }) + ); } } @@ -316,13 +328,12 @@ async function updateProjectGraphWithPlugins( 'sourceFile' in dep ? dep.sourceFile : null ); } - } catch (e) { - let message = `Failed to process project dependencies with "${plugin.name}".`; - if (e instanceof Error) { - e.message = message + '\n' + e.message; - throw e; - } - throw new Error(message); + } catch (cause) { + errors.push( + new ProcessDependenciesError(plugin.name, { + cause, + }) + ); } performance.mark(`${plugin.name}:createDependencies - end`); @@ -333,7 +344,52 @@ async function updateProjectGraphWithPlugins( ); }) ); - return builder.getUpdatedProjectGraph(); + + const result = builder.getUpdatedProjectGraph(); + + if (errors.length === 0) { + return result; + } else { + throw new CreateDependenciesError(errors, result); + } +} + +export class ProcessDependenciesError extends Error { + constructor(public readonly pluginName: string, { cause }) { + super( + `The "${pluginName}" plugin threw an error while creating dependencies:`, + { + cause, + } + ); + this.name = this.constructor.name; + this.stack = `${this.message}\n ${cause.stack.split('\n').join('\n ')}`; + } +} + +export class ProcessProjectGraphError extends Error { + constructor(public readonly pluginName: string, { cause }) { + super( + `The "${pluginName}" plugin threw an error while processing the project graph:`, + { + cause, + } + ); + this.name = this.constructor.name; + this.stack = `${this.message}\n ${cause.stack.split('\n').join('\n ')}`; + } +} + +export class CreateDependenciesError extends Error { + constructor( + public readonly errors: Array< + ProcessDependenciesError | ProcessProjectGraphError + >, + public readonly partialProjectGraph: ProjectGraph + ) { + super('Failed to create dependencies. See above for errors'); + this.name = this.constructor.name; + } } function readRootTsConfig() { diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index 7da1ded77ccca1..c82211e40d583a 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -1,5 +1,14 @@ -import { readFileMapCache, readProjectGraphCache } from './nx-deps-cache'; -import { buildProjectGraphUsingProjectFileMap } from './build-project-graph'; +import { + readFileMapCache, + readProjectGraphCache, + writeCache, +} from './nx-deps-cache'; +import { + CreateDependenciesError, + ProcessDependenciesError, + ProcessProjectGraphError, + buildProjectGraphUsingProjectFileMap, +} from './build-project-graph'; import { output } from '../utils/output'; import { markDaemonAsDisabled, writeDaemonLogs } from '../daemon/tmp-dir'; import { ProjectGraph } from '../config/project-graph'; @@ -18,6 +27,14 @@ import { } from './utils/retrieve-workspace-files'; import { readNxJson } from '../config/nx-json'; import { unregisterPluginTSTranspiler } from '../utils/nx-plugin'; +import { + ConfigurationResult, + ConfigurationSourceMaps, + CreateNodesError, + MergeNodesError, + ProjectConfigurationsError, +} from './utils/project-configuration-utils'; +import { DaemonProjectGraphError } from '../daemon/daemon-project-graph-error'; /** * Synchronously reads the latest cached copy of the workspace's ProjectGraph. @@ -83,8 +100,23 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { const nxJson = readNxJson(); performance.mark('retrieve-project-configurations:start'); + let configurationResult: ConfigurationResult; + let projectConfigurationsError: ProjectConfigurationsError; + try { + configurationResult = await retrieveProjectConfigurations( + workspaceRoot, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigurationsError = e; + configurationResult = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } const { projects, externalNodes, sourceMaps, projectRootMap } = - await retrieveProjectConfigurations(workspaceRoot, nxJson); + configurationResult; performance.mark('retrieve-project-configurations:end'); performance.mark('retrieve-workspace-files:start'); @@ -94,34 +126,132 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { const cacheEnabled = process.env.NX_CACHE_PROJECT_GRAPH !== 'false'; performance.mark('build-project-graph-using-project-file-map:start'); - const projectGraph = ( - await buildProjectGraphUsingProjectFileMap( + let createDependenciesError: CreateDependenciesError; + let projectGraphResult: Awaited< + ReturnType + >; + try { + projectGraphResult = await buildProjectGraphUsingProjectFileMap( projects, externalNodes, fileMap, allWorkspaceFiles, rustReferences, - cacheEnabled ? readFileMapCache() : null, - cacheEnabled - ) - ).projectGraph; + cacheEnabled ? readFileMapCache() : null + ); + } catch (e) { + if (e instanceof CreateDependenciesError) { + projectGraphResult = { + projectGraph: e.partialProjectGraph, + projectFileMapCache: null, + }; + createDependenciesError = e; + } else { + throw e; + } + } + + const { projectGraph, projectFileMapCache } = projectGraphResult; performance.mark('build-project-graph-using-project-file-map:end'); unregisterPluginTSTranspiler(); delete global.NX_GRAPH_CREATION; - return { projectGraph, sourceMaps }; + const errors = [ + ...(projectConfigurationsError?.errors ?? []), + ...(createDependenciesError?.errors ?? []), + ]; + + if (errors.length > 0) { + throw new ProjectGraphError(errors, projectGraph, sourceMaps); + } else { + if (cacheEnabled) { + writeCache(projectFileMapCache, projectGraph); + } + return { projectGraph, sourceMaps }; + } +} + +export class ProjectGraphError extends Error { + readonly #errors: Array< + CreateNodesError | ProcessDependenciesError | ProcessProjectGraphError + >; + readonly #partialProjectGraph: ProjectGraph; + readonly #partialSourceMaps: ConfigurationSourceMaps; + + constructor( + errors: Array< + | CreateNodesError + | MergeNodesError + | ProcessDependenciesError + | ProcessProjectGraphError + >, + partialProjectGraph: ProjectGraph, + partialSourceMaps: ConfigurationSourceMaps + ) { + super(`Failed to process project graph.`); + this.name = this.constructor.name; + this.#errors = errors; + this.#partialProjectGraph = partialProjectGraph; + this.#partialSourceMaps = partialSourceMaps; + this.stack = `${this.message}\n ${errors + .map((error) => error.stack.split('\n').join('\n ')) + .join('\n')}`; + } + + /** + * The daemon cannot throw errors which contain methods as they are not serializable. + * + * This method creates a new {@link ProjectGraphError} from a {@link DaemonProjectGraphError} with the methods based on the same serialized data. + */ + static fromDaemonProjectGraphError(e: DaemonProjectGraphError) { + return new ProjectGraphError(e.errors, e.projectGraph, e.sourceMaps); + } + + /** + * This gets the partial project graph despite the errors which occured. + * This partial project graph may be missing nodes, properties of nodes, or dependencies. + * This is useful mostly for visualization/debugging. It should not be used for running tasks. + */ + getPartialProjectGraph() { + return this.#partialProjectGraph; + } + + getPartialSourcemaps() { + return this.#partialSourceMaps; + } + + getErrors() { + return this.#errors; + } } function handleProjectGraphError(opts: { exitOnError: boolean }, e) { if (opts.exitOnError) { - const lines = e.message.split('\n'); - output.error({ - title: lines[0], - bodyLines: lines.slice(1), - }); - if (process.env.NX_VERBOSE_LOGGING === 'true') { - console.error(e); + const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true'; + if (e instanceof ProjectGraphError) { + let title = e.message; + if (isVerbose) { + title += ' See errors below.'; + } + + const bodyLines = isVerbose + ? [e.stack] + : ['Pass --verbose to see the stacktraces.']; + + output.error({ + title, + bodyLines: bodyLines, + }); + } else { + const lines = e.message.split('\n'); + output.error({ + title: lines[0], + bodyLines: lines.slice(1), + }); + if (isVerbose) { + console.error(e); + } } process.exit(1); } else { @@ -202,9 +332,6 @@ export async function createProjectGraphAndSourceMapsAsync( try { const projectGraphAndSourceMaps = await daemonClient.getProjectGraphAndSourceMaps(); - if (opts.resetDaemonClient) { - daemonClient.reset(); - } performance.mark('create-project-graph-async:end'); performance.measure( 'create-project-graph-async', @@ -241,6 +368,10 @@ export async function createProjectGraphAndSourceMapsAsync( } handleProjectGraphError(opts, e); + } finally { + if (opts.resetDaemonClient) { + daemonClient.reset(); + } } } } diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index 4fbf9043b5d5d1..fc95a2176834bc 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -15,6 +15,7 @@ import { import { minimatch } from 'minimatch'; import { join } from 'path'; +import { performance } from 'perf_hooks'; export type SourceInformation = [file: string, plugin: string]; export type ConfigurationSourceMaps = Record< @@ -278,30 +279,32 @@ export function mergeProjectConfigurationIntoRootMap( export type ConfigurationResult = { projects: Record; externalNodes: Record; - rootMap: Record; + projectRootMap: Record; sourceMaps: ConfigurationSourceMaps; }; +type CreateNodesResultWithContext = CreateNodesResult & { + file: string; + pluginName: string; +}; /** * Transforms a list of project paths into a map of project configurations. * + * @param root The workspace root * @param nxJson The NxJson configuration * @param workspaceFiles A list of non-ignored workspace files * @param plugins The plugins that should be used to infer project configuration - * @param root The workspace root */ -export function buildProjectsConfigurationsFromProjectPathsAndPlugins( +export function createProjectConfigurations( + root: string = workspaceRoot, nxJson: NxJsonConfiguration, workspaceFiles: string[], // making this parameter allows devkit to pick up newly created projects - plugins: LoadedNxPlugin[], - root: string = workspaceRoot + plugins: LoadedNxPlugin[] ): Promise { - type CreateNodesResultWithContext = CreateNodesResult & { - file: string; - pluginName: string; - }; + performance.mark('build-project-configs:start'); const results: Array>> = []; + const errors: Array = []; // We iterate over plugins first - this ensures that plugins specified first take precedence. for (const { plugin, options } of plugins) { @@ -331,12 +334,18 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins( if (r instanceof Promise) { pluginResults.push( r - .catch((e) => { + .catch((error) => { performance.mark(`${plugin.name}:createNodes:${file} - end`); - throw new CreateNodesError( - `Unable to create nodes for ${file} using plugin ${plugin.name}.`, - e + errors.push( + new CreateNodesError({ + file, + pluginName: plugin.name, + error, + }) ); + return { + projects: {}, + }; }) .then((r) => { performance.mark(`${plugin.name}:createNodes:${file} - end`); @@ -361,14 +370,17 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins( pluginName: plugin.name, }); } - } catch (e) { - throw new CreateNodesError( - `Unable to create nodes for ${file} using plugin ${plugin.name}.`, - e + } catch (error) { + errors.push( + new CreateNodesError({ + file, + pluginName: plugin.name, + error, + }) ); } } - // If there are no promises (counter undefined) or all promises have resolved (counter === 0) + results.push( Promise.all(pluginResults).then((results) => { performance.mark(`${plugin.name}:createNodes - end`); @@ -417,10 +429,13 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins( configurationSourceMaps, sourceInfo ); - } catch (e) { - throw new CreateNodesError( - `Unable to merge project information for "${project.root}" from ${result.file} using plugin ${result.pluginName}.`, - e + } catch (error) { + errors.push( + new MergeNodesError({ + file, + pluginName, + error, + }) ); } } @@ -437,12 +452,28 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins( 'createNodes:merge - end' ); - return { - projects, - externalNodes, - rootMap, - sourceMaps: configurationSourceMaps, - }; + performance.mark('build-project-configs:end'); + performance.measure( + 'build-project-configs', + 'build-project-configs:start', + 'build-project-configs:end' + ); + + if (errors.length === 0) { + return { + projects, + externalNodes, + projectRootMap: rootMap, + sourceMaps: configurationSourceMaps, + }; + } else { + throw new ProjectConfigurationsError(errors, { + projects, + externalNodes, + projectRootMap: rootMap, + sourceMaps: configurationSourceMaps, + }); + } }); } @@ -493,20 +524,59 @@ export function readProjectConfigurationsFromRootMap( return projects; } -class CreateNodesError extends Error { - constructor(msg, cause: Error | unknown) { - const message = `${msg} ${ - !cause - ? '' - : cause instanceof Error - ? `\n\n\t Inner Error: ${cause.stack}` - : cause - }`; - // These errors are thrown during a JS callback which is invoked via rust. - // The errors messaging gets lost in the rust -> js -> rust transition, but - // logging the error here will ensure that it is visible in the console. - console.error(message); - super(message, { cause }); +export class ProjectConfigurationsError extends Error { + constructor( + public readonly errors: Array, + public readonly partialProjectConfigurationsResult: ConfigurationResult + ) { + super('Failed to create project configurations'); + this.name = this.constructor.name; + } +} + +export class CreateNodesError extends Error { + file: string; + pluginName: string; + + constructor({ + file, + pluginName, + error, + }: { + file: string; + pluginName: string; + error: Error; + }) { + const msg = `The "${pluginName}" plugin threw an error while creating nodes from ${file}:`; + + super(msg, { cause: error }); + this.name = this.constructor.name; + this.file = file; + this.pluginName = pluginName; + this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`; + } +} + +export class MergeNodesError extends Error { + file: string; + pluginName: string; + + constructor({ + file, + pluginName, + error, + }: { + file: string; + pluginName: string; + error: Error; + }) { + const msg = `The nodes created from ${file} by the "${pluginName}" could not be merged into the project graph:`; + + super(msg, { cause: error }); + this.name = this.constructor.name; + this.file = file; + this.pluginName = pluginName; + this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`; } } diff --git a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts index 6290fcd7578b53..ba1cdb73a37720 100644 --- a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts +++ b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts @@ -7,11 +7,10 @@ import { shouldMergeAngularProjects, } from '../../adapter/angular-json'; import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; -import { ProjectGraphExternalNode } from '../../config/project-graph'; import { getNxPackageJsonWorkspacesPlugin } from '../../plugins/package-json-workspaces'; import { - buildProjectsConfigurationsFromProjectPathsAndPlugins, - ConfigurationSourceMaps, + createProjectConfigurations, + ConfigurationResult, } from './project-configuration-utils'; import { getDefaultPlugins, @@ -73,7 +72,7 @@ export async function retrieveWorkspaceFiles( export async function retrieveProjectConfigurations( workspaceRoot: string, nxJson: NxJsonConfiguration -): Promise { +): Promise { const plugins = await loadNxPlugins( nxJson?.plugins ?? [], getNxRequirePaths(workspaceRoot), @@ -86,7 +85,7 @@ export async function retrieveProjectConfigurations( export async function retrieveProjectConfigurationsWithAngularProjects( workspaceRoot: string, nxJson: NxJsonConfiguration -): Promise { +): Promise { const plugins = await loadNxPlugins( nxJson?.plugins ?? [], getNxRequirePaths(workspaceRoot), @@ -103,18 +102,11 @@ export async function retrieveProjectConfigurationsWithAngularProjects( return _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins); } -export type RetrievedGraphNodes = { - externalNodes: Record; - projects: Record; - sourceMaps: ConfigurationSourceMaps; - projectRootMap: Record; -}; - function _retrieveProjectConfigurations( workspaceRoot: string, nxJson: NxJsonConfiguration, plugins: LoadedNxPlugin[] -): Promise { +): Promise { const globPatterns = configurationGlobs(plugins); const workspaceFiles = globWithWorkspaceContext(workspaceRoot, globPatterns); @@ -169,37 +161,6 @@ export async function retrieveProjectConfigurationsWithoutPluginInference( return projects; } -export async function createProjectConfigurations( - workspaceRoot: string, - nxJson: NxJsonConfiguration, - configFiles: string[], - plugins: LoadedNxPlugin[] -): Promise { - performance.mark('build-project-configs:start'); - - const { projects, externalNodes, rootMap, sourceMaps } = - await buildProjectsConfigurationsFromProjectPathsAndPlugins( - nxJson, - configFiles, - plugins, - workspaceRoot - ); - - performance.mark('build-project-configs:end'); - performance.measure( - 'build-project-configs', - 'build-project-configs:start', - 'build-project-configs:end' - ); - - return { - projects, - externalNodes, - projectRootMap: rootMap, - sourceMaps, - }; -} - export function configurationGlobs(plugins: LoadedNxPlugin[]): string[] { const globPatterns = []; for (const { plugin } of plugins) { diff --git a/packages/nx/src/utils/output.ts b/packages/nx/src/utils/output.ts index e40599e12c4ca8..4530ed20f826ba 100644 --- a/packages/nx/src/utils/output.ts +++ b/packages/nx/src/utils/output.ts @@ -2,7 +2,7 @@ import * as chalk from 'chalk'; import { EOL } from 'os'; import * as readline from 'readline'; import { isCI } from './is-ci'; -import { TaskStatus } from '../tasks-runner/tasks-runner'; +import type { TaskStatus } from '../tasks-runner/tasks-runner'; const GH_GROUP_PREFIX = '::group::'; const GH_GROUP_SUFFIX = '::endgroup::'; diff --git a/packages/nx/src/utils/params.ts b/packages/nx/src/utils/params.ts index 5e1e9f84e83134..9384a95468fa2b 100644 --- a/packages/nx/src/utils/params.ts +++ b/packages/nx/src/utils/params.ts @@ -1,10 +1,11 @@ import { logger } from './logger'; -import { NxJsonConfiguration } from '../config/nx-json'; -import { +import type { NxJsonConfiguration } from '../config/nx-json'; +import type { TargetConfiguration, ProjectsConfigurations, } from '../config/workspace-json-project-json'; import { output } from './output'; +import type { ProjectGraphError } from '../project-graph/project-graph'; const LIST_CHOICE_DISPLAY_LIMIT = 10; @@ -96,6 +97,20 @@ export async function handleErrors(isVerbose: boolean, fn: Function) { err ||= new Error('Unknown error caught'); if (err.constructor.name === 'UnsuccessfulWorkflowExecution') { logger.error('The generator workflow failed. See above.'); + } else if (err.name === 'ProjectGraphError') { + let title = err.message; + if (isVerbose) { + title += ' See errors below.'; + } + + const bodyLines = isVerbose + ? [err.stack] + : ['Pass --verbose to see the stacktraces.']; + + output.error({ + title, + bodyLines: bodyLines, + }); } else { const lines = (err.message ? err.message : err.toString()).split('\n'); const bodyLines = lines.slice(1);