From 99bccec06c786e329883189e01ea0e55ea68c660 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 16 Apr 2024 16:43:27 +0200 Subject: [PATCH 1/4] feat(graph): show partial project graph & errors in graph app --- graph/client/src/app/routes.tsx | 17 ++- graph/client/src/app/shell.tsx | 62 ++++++-- .../src/app/ui-components/error-boundary.tsx | 99 +++++++++++-- .../ui-components/project-details-modal.tsx | 14 +- graph/project-details/src/index.ts | 1 + .../src/lib/project-details-page.tsx | 10 +- .../src/lib/project-details-wrapper.tsx | 23 ++- graph/shared/src/index.ts | 1 + graph/shared/src/lib/error-toast.tsx | 132 ++++++++++++++++++ .../mock-project-graph-service.ts | 1 + graph/ui-components/src/index.ts | 2 + .../ui-components/src/lib/error-renderer.tsx | 63 +++++++++ graph/ui-components/src/lib/modal.tsx | 108 ++++++++++++++ .../lib/project-details/project-details.tsx | 4 + packages/nx/src/command-line/graph/graph.ts | 108 +++++++++++--- packages/nx/src/daemon/client/client.ts | 11 +- .../file-watching/file-watcher-sockets.ts | 1 - .../project-json/build-nodes/project-json.ts | 2 + .../nx/src/project-graph/project-graph.ts | 2 +- 19 files changed, 600 insertions(+), 61 deletions(-) create mode 100644 graph/shared/src/lib/error-toast.tsx create mode 100644 graph/ui-components/src/lib/error-renderer.tsx create mode 100644 graph/ui-components/src/lib/modal.tsx diff --git a/graph/client/src/app/routes.tsx b/graph/client/src/app/routes.tsx index 2fc24977988ac..9b247bba98d91 100644 --- a/graph/client/src/app/routes.tsx +++ b/graph/client/src/app/routes.tsx @@ -1,12 +1,16 @@ -import { redirect, RouteObject } from 'react-router-dom'; +import { redirect, RouteObject, json } from 'react-router-dom'; import { ProjectsSidebar } from './feature-projects/projects-sidebar'; import { TasksSidebar } from './feature-tasks/tasks-sidebar'; import { Shell } from './shell'; /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph'; +import type { + GraphError, + ProjectGraphClientResponse, +} from 'nx/src/command-line/graph/graph'; // nx-ignore-next-line import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph'; +/* eslint-enable @nx/enforce-module-boundaries */ import { getEnvironmentConfig, getProjectGraphDataService, @@ -78,6 +82,7 @@ const projectDetailsLoader = async ( hash: string; project: ProjectGraphProjectNode; sourceMap: Record; + errors?: GraphError[]; }> => { const workspaceData = await workspaceDataLoader(selectedWorkspaceId); const sourceMaps = await sourceMapsLoader(selectedWorkspaceId); @@ -85,10 +90,18 @@ const projectDetailsLoader = async ( const project = workspaceData.projects.find( (project) => project.name === projectName ); + if (!project) { + throw json({ + id: 'project-not-found', + projectName, + errors: workspaceData.errors, + }); + } return { hash: workspaceData.hash, project, sourceMap: sourceMaps[project.data.root], + errors: workspaceData.errors, }; }; diff --git a/graph/client/src/app/shell.tsx b/graph/client/src/app/shell.tsx index a670617bf57a8..d8fed74e30477 100644 --- a/graph/client/src/app/shell.tsx +++ b/graph/client/src/app/shell.tsx @@ -1,30 +1,48 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import { + GraphError, + ProjectGraphClientResponse, +} from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ + import { ArrowDownTrayIcon, ArrowLeftCircleIcon, InformationCircleIcon, } from '@heroicons/react/24/outline'; +import { + ErrorToast, + fetchProjectGraph, + getProjectGraphDataService, + useEnvironmentConfig, + useIntervalWhen, +} from '@nx/graph/shared'; +import { Dropdown, Spinner } from '@nx/graph/ui-components'; +import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme'; +import { Tooltip } from '@nx/graph/ui-tooltips'; import classNames from 'classnames'; -import { DebuggerPanel } from './ui-components/debugger-panel'; -import { getGraphService } from './machines/graph.service'; +import { useLayoutEffect, useState } from 'react'; import { Outlet, useNavigate, useNavigation, useParams, + useRouteLoaderData, } from 'react-router-dom'; -import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme'; -import { Dropdown, Spinner } from '@nx/graph/ui-components'; -import { useCurrentPath } from './hooks/use-current-path'; -import { ExperimentalFeature } from './ui-components/experimental-feature'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { RankdirPanel } from './feature-projects/panels/rankdir-panel'; +import { useCurrentPath } from './hooks/use-current-path'; import { getProjectGraphService } from './machines/get-services'; -import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { Tooltip } from '@nx/graph/ui-tooltips'; +import { getGraphService } from './machines/graph.service'; +import { DebuggerPanel } from './ui-components/debugger-panel'; +import { ExperimentalFeature } from './ui-components/experimental-feature'; import { TooltipDisplay } from './ui-tooltips/graph-tooltip-display'; -import { useEnvironmentConfig } from '@nx/graph/shared'; export function Shell(): JSX.Element { const projectGraphService = getProjectGraphService(); + const projectGraphDataService = getProjectGraphDataService(); + const graphService = getGraphService(); const lastPerfReport = useSyncExternalStore( @@ -43,9 +61,30 @@ export function Shell(): JSX.Element { const navigate = useNavigate(); const { state: navigationState } = useNavigation(); const currentPath = useCurrentPath(); - const { selectedWorkspaceId } = useParams(); + const params = useParams(); const currentRoute = currentPath.currentPath; + const [errors, setErrors] = useState(undefined); + const { errors: routerErrors } = useRouteLoaderData('selectedWorkspace') as { + errors: GraphError[]; + }; + useLayoutEffect(() => { + setErrors(routerErrors); + }, [routerErrors]); + useIntervalWhen( + () => { + fetchProjectGraph( + projectGraphDataService, + params, + environmentConfig.appConfig + ).then((response: ProjectGraphClientResponse) => { + setErrors(response.errors); + }); + }, + 1000, + environmentConfig.watch + ); + const topLevelRoute = currentRoute.startsWith('/tasks') ? '/tasks' : '/projects'; @@ -165,7 +204,7 @@ export function Shell(): JSX.Element { {environment.appConfig.showDebugger ? ( @@ -217,6 +256,7 @@ export function Shell(): JSX.Element { + ); } diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx index 92e366f3e711b..3f575c6866908 100644 --- a/graph/client/src/app/ui-components/error-boundary.tsx +++ b/graph/client/src/app/ui-components/error-boundary.tsx @@ -1,27 +1,100 @@ -import { useEnvironmentConfig } from '@nx/graph/shared'; -import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-details-header'; -import { useRouteError } from 'react-router-dom'; +import { ProjectDetailsHeader } from '@nx/graph/project-details'; +import { + fetchProjectGraph, + getProjectGraphDataService, + useEnvironmentConfig, + useIntervalWhen, +} from '@nx/graph/shared'; +import { ErrorRenderer } from '@nx/graph/ui-components'; +import { + isRouteErrorResponse, + useParams, + useRouteError, +} from 'react-router-dom'; export function ErrorBoundary() { let error = useRouteError(); console.error(error); - const environment = useEnvironmentConfig()?.environment; - let message = 'Disconnected from graph server. '; - if (environment === 'nx-console') { - message += 'Please refresh the page.'; + const { environment, appConfig, watch } = useEnvironmentConfig(); + const projectGraphDataService = getProjectGraphDataService(); + const params = useParams(); + + const hasErrorData = + isRouteErrorResponse(error) && error.data.errors?.length > 0; + + useIntervalWhen( + async () => { + fetchProjectGraph(projectGraphDataService, params, appConfig).then( + (data) => { + if ( + isRouteErrorResponse(error) && + error.data.id === 'project-not-found' && + data.projects.find((p) => p.name === error.data.projectName) + ) { + window.location.reload(); + } + return; + } + ); + }, + 1000, + watch + ); + + let message: string | JSX.Element; + let stack: string; + if (isRouteErrorResponse(error) && error.data.id === 'project-not-found') { + message = ( +

+ Project {error.data.projectName} not found. +

+ ); } else { - message += 'Please rerun your command and refresh the page.'; + message = 'Disconnected from graph server. '; + if (environment === 'nx-console') { + message += 'Please refresh the page.'; + } else { + message += 'Please rerun your command and refresh the page.'; + } + stack = error.toString(); } return (
- -

Error

-
-

{message}

-

Error message: {error?.toString()}

+ {environment !== 'nx-console' && } +
+

Error

+
+ +
+ {hasErrorData && ( +
+

+ Nx encountered the following issues while processing the project + graph:{' '} +

+
+ +
+
+ )}
); } + +function ErrorWithStack({ + message, + stack, +}: { + message: string | JSX.Element; + stack?: string; +}) { + return ( +
+

{message}

+ {stack &&

Error message: {stack}

} +
+ ); +} diff --git a/graph/client/src/app/ui-components/project-details-modal.tsx b/graph/client/src/app/ui-components/project-details-modal.tsx index 7ee2a4ca8a9bb..17eb137fb4bcd 100644 --- a/graph/client/src/app/ui-components/project-details-modal.tsx +++ b/graph/client/src/app/ui-components/project-details-modal.tsx @@ -1,11 +1,11 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line +import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph'; +// nx-ignore-next-line +import { ProjectDetailsWrapper } from '@nx/graph/project-details'; +/* eslint-enable @nx/enforce-module-boundaries */ import { useFloating } from '@floating-ui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; -import { ProjectDetailsWrapper } from '@nx/graph/project-details'; -/* eslint-disable @nx/enforce-module-boundaries */ -// nx-ignore-next-line -import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph'; import { useEffect, useState } from 'react'; import { useRouteLoaderData, useSearchParams } from 'react-router-dom'; @@ -50,15 +50,15 @@ export function ProjectDetailsModal() { return ( isOpen && (
-
+
-
+
diff --git a/graph/project-details/src/index.ts b/graph/project-details/src/index.ts index 2ae2b30601c45..035f68de28901 100644 --- a/graph/project-details/src/index.ts +++ b/graph/project-details/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/project-details-wrapper'; export * from './lib/project-details-page'; +export * from './lib/project-details-header'; diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx index 1b325f8be18dc..32d5ab9f72362 100644 --- a/graph/project-details/src/lib/project-details-page.tsx +++ b/graph/project-details/src/lib/project-details-page.tsx @@ -1,6 +1,10 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { ProjectGraphProjectNode } from '@nx/devkit'; +// nx-ignore-next-line +import { GraphError } from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ + import { ScrollRestoration, useParams, @@ -16,12 +20,13 @@ import { import { ProjectDetailsHeader } from './project-details-header'; export function ProjectDetailsPage() { - const { project, sourceMap, hash } = useRouteLoaderData( + const { project, sourceMap, hash, errors } = useRouteLoaderData( 'selectedProjectDetails' ) as { hash: string; project: ProjectGraphProjectNode; sourceMap: Record; + errors?: GraphError[]; }; const { environment, watch, appConfig } = useEnvironmentConfig(); @@ -56,6 +61,7 @@ export function ProjectDetailsPage() {
diff --git a/graph/project-details/src/lib/project-details-wrapper.tsx b/graph/project-details/src/lib/project-details-wrapper.tsx index 712abe434685e..2746d00c6bbe3 100644 --- a/graph/project-details/src/lib/project-details-wrapper.tsx +++ b/graph/project-details/src/lib/project-details-wrapper.tsx @@ -1,9 +1,13 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line import type { ProjectGraphProjectNode } from '@nx/devkit'; +// nx-ignore-next-line +import { GraphError } from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom'; import { connect } from 'react-redux'; import { + ErrorToast, getExternalApiService, useEnvironmentConfig, useRouteConstructor, @@ -23,6 +27,7 @@ type ProjectDetailsProps = mapStateToPropsType & mapDispatchToPropsType & { project: ProjectGraphProjectNode; sourceMap: Record; + errors?: GraphError[]; }; export function ProjectDetailsWrapperComponent({ @@ -31,6 +36,7 @@ export function ProjectDetailsWrapperComponent({ setExpandTargets, expandTargets, collapseAllTargets, + errors, }: ProjectDetailsProps) { const environment = useEnvironmentConfig()?.environment; const externalApiService = getExternalApiService(); @@ -158,13 +164,16 @@ export function ProjectDetailsWrapperComponent({ } return ( - + <> + + + ); } diff --git a/graph/shared/src/index.ts b/graph/shared/src/index.ts index dd2268fea7ad8..7418b557e2742 100644 --- a/graph/shared/src/index.ts +++ b/graph/shared/src/index.ts @@ -6,3 +6,4 @@ export * from './lib/use-route-constructor'; export * from './lib/use-interval-when'; export * from './lib/project-graph-data-service/get-project-graph-data-service'; export * from './lib/fetch-project-graph'; +export * from './lib/error-toast'; diff --git a/graph/shared/src/lib/error-toast.tsx b/graph/shared/src/lib/error-toast.tsx new file mode 100644 index 0000000000000..4ce3564c65943 --- /dev/null +++ b/graph/shared/src/lib/error-toast.tsx @@ -0,0 +1,132 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import { GraphError } from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ + +import { + createRef, + ForwardedRef, + forwardRef, + useCallback, + useImperativeHandle, + useLayoutEffect, +} from 'react'; + +import { Transition } from '@headlessui/react'; +import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; +import { SetURLSearchParams, useSearchParams } from 'react-router-dom'; +import { ErrorRenderer, Modal, ModalHandle } from '@nx/graph/ui-components'; + +export interface ErrorToastImperativeHandle { + closeModal: () => void; + openModal: () => void; +} + +interface ErrorToastProps { + errors?: GraphError[] | undefined; +} + +export const ErrorToast = forwardRef( + ( + { errors }: ErrorToastProps, + ref: ForwardedRef + ) => { + const inputsModalRef = createRef(); + + const [searchParams, setSearchParams] = useSearchParams(); + + useImperativeHandle(ref, () => ({ + openModal: () => { + inputsModalRef?.current?.openModal(); + }, + closeModal: () => { + inputsModalRef?.current?.closeModal(); + }, + })); + + const handleModalOpen = useCallback(() => { + if (searchParams.get('show-error') === 'true') return; + setSearchParams( + (currentSearchParams) => { + currentSearchParams.set('show-error', 'true'); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + }, [setSearchParams, searchParams]); + + const handleModalClose = useCallback(() => { + if (!searchParams.get('show-error')) return; + setSearchParams( + (currentSearchParams) => { + currentSearchParams.delete('show-error'); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + }, [setSearchParams, searchParams]); + + useLayoutEffect(() => { + if (searchParams.get('show-error') === 'true') { + if (errors && errors.length > 0) { + inputsModalRef.current?.openModal(); + } else { + setSearchParams( + (currentSearchParams) => { + currentSearchParams.delete('show-error'); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + } + } + }, [searchParams, inputsModalRef, errors, setSearchParams]); + + return ( + 0} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > +
+
inputsModalRef.current?.openModal()} + className="z-50 mx-auto flex w-fit max-w-[75%] cursor-pointer items-center rounded-md bg-red-600 p-4 text-slate-200 shadow-lg" + > + + Some project information might be missing. Click to see errors. +
+ {errors?.length > 0 && ( + + + + )} +
+
+ ); + } +); + +export const useRouterHandleModalOpen = ( + searchParams: URLSearchParams, + setSearchParams: SetURLSearchParams +) => + useCallback(() => { + if (searchParams.get('show-error') === 'true') return; + setSearchParams( + (currentSearchParams) => { + currentSearchParams.set('show-error', 'true'); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + }, [setSearchParams, searchParams]); diff --git a/graph/shared/src/lib/project-graph-data-service/mock-project-graph-service.ts b/graph/shared/src/lib/project-graph-data-service/mock-project-graph-service.ts index ca18670f74cf5..b992e2cafe59e 100644 --- a/graph/shared/src/lib/project-graph-data-service/mock-project-graph-service.ts +++ b/graph/shared/src/lib/project-graph-data-service/mock-project-graph-service.ts @@ -61,6 +61,7 @@ export class MockProjectGraphService implements ProjectGraphService { focus: null, exclude: [], groupByFolder: false, + isPartial: false, }; private taskGraphsResponse: TaskGraphClientResponse = { diff --git a/graph/ui-components/src/index.ts b/graph/ui-components/src/index.ts index 82bc51d1ec3fe..7ca48bebb0462 100644 --- a/graph/ui-components/src/index.ts +++ b/graph/ui-components/src/index.ts @@ -2,3 +2,5 @@ export * from './lib/debounced-text-input'; export * from './lib/tag'; export * from './lib/dropdown'; export * from './lib/spinner'; +export * from './lib/error-renderer'; +export * from './lib/modal'; diff --git a/graph/ui-components/src/lib/error-renderer.tsx b/graph/ui-components/src/lib/error-renderer.tsx new file mode 100644 index 0000000000000..4c3bd5a6cfee7 --- /dev/null +++ b/graph/ui-components/src/lib/error-renderer.tsx @@ -0,0 +1,63 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import { GraphError } from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ +export function ErrorRenderer({ errors }: { errors: GraphError[] }) { + return ( +
+ {errors.map((error, index) => { + const errorHeading = + error.pluginName && error.name + ? `${error.name} - ${error.pluginName}` + : error.name ?? error.message; + const fileSpecifier = + isCauseWithLocation(error.cause) && error.cause.errors.length === 1 + ? `${error.fileName}:${error.cause.errors[0].location.line}:${error.cause.errors[0].location.column}` + : error.fileName; + return ( +
+ + {errorHeading} + - + {fileSpecifier} + +
+              {isCauseWithErrors(error.cause) &&
+              error.cause.errors.length === 1 ? (
+                
+ {error.message}
+ {error.cause.errors[0].text}{' '} +
+ ) : ( +
{error.stack}
+ )} +
+
+ ); + })} +
+ ); +} + +function isCauseWithLocation(cause: unknown): cause is { + errors: { + location: { + column: number; + line: number; + }; + text: string; + }[]; +} { + return ( + isCauseWithErrors(cause) && + (cause as any).errors[0].location && + (cause as any).errors[0].location.column && + (cause as any).errors[0].location.line + ); +} + +function isCauseWithErrors( + cause: unknown +): cause is { errors: { text: string }[] } { + return cause && (cause as any).errors && (cause as any).errors[0].text; +} diff --git a/graph/ui-components/src/lib/modal.tsx b/graph/ui-components/src/lib/modal.tsx new file mode 100644 index 0000000000000..1cac5f2fbf1e1 --- /dev/null +++ b/graph/ui-components/src/lib/modal.tsx @@ -0,0 +1,108 @@ +// component from https://tailwindui.com/components/application-ui/overlays/dialogs +import { Dialog, Transition } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { + ForwardedRef, + Fragment, + ReactNode, + forwardRef, + useEffect, + useImperativeHandle, + useState, +} from 'react'; + +export interface ModalProps { + children: ReactNode; + title: string; + onOpen?: () => void; + onClose?: () => void; +} + +export interface ModalHandle { + openModal: () => void; + closeModal: () => void; +} + +export const Modal = forwardRef( + ( + { children, title, onOpen, onClose }: ModalProps, + ref: ForwardedRef + ) => { + const [open, setOpen] = useState(false); + + useEffect(() => { + if (open) { + onOpen?.(); + } else { + onClose?.(); + } + }, [open, onOpen, onClose]); + + useImperativeHandle(ref, () => ({ + closeModal: () => { + setOpen(false); + }, + openModal: () => { + setOpen(true); + }, + })); + + return ( + + + +
+ + +
+
+ + +
+ + {title} + + +
+
+ {children} +
+
+
+
+
+
+
+ ); + } +); diff --git a/graph/ui-project-details/src/lib/project-details/project-details.tsx b/graph/ui-project-details/src/lib/project-details/project-details.tsx index 0e47fff8f9eac..14197a5e0fee6 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -3,6 +3,9 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line import type { ProjectGraphProjectNode } from '@nx/devkit'; +// nx-ignore-next-line +import { GraphError } from 'nx/src/command-line/graph/graph'; +/* eslint-enable @nx/enforce-module-boundaries */ import { EyeIcon } from '@heroicons/react/24/outline'; import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; @@ -15,6 +18,7 @@ import { TargetTechnologies } from '../target-technologies/target-technologies'; export interface ProjectDetailsProps { project: ProjectGraphProjectNode; sourceMap: Record; + errors?: GraphError[]; variant?: 'default' | 'compact'; onViewInProjectGraph?: (data: { projectName: string }) => void; onViewInTaskGraph?: (data: { diff --git a/packages/nx/src/command-line/graph/graph.ts b/packages/nx/src/command-line/graph/graph.ts index 6343e8b5662e0..ef9cd1771201d 100644 --- a/packages/nx/src/command-line/graph/graph.ts +++ b/packages/nx/src/command-line/graph/graph.ts @@ -36,6 +36,7 @@ import { pruneExternalNodes } from '../../project-graph/operators'; import { createProjectGraphAndSourceMapsAsync, createProjectGraphAsync, + handleProjectGraphError, } from '../../project-graph/project-graph'; import { createTaskGraph, @@ -48,21 +49,35 @@ import { HashPlanner, transferProjectGraph } from '../../native'; import { transformProjectGraphForRust } from '../../native/transform-objects'; import { getAffectedGraphNodes } from '../affected/affected'; import { readFileMapCache } from '../../project-graph/nx-deps-cache'; +import { Hash, getNamedInputs } from '../../hasher/task-hasher'; import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils'; -import { filterUsingGlobPatterns } from '../../hasher/task-hasher'; import { createTaskHasher } from '../../hasher/create-task-hasher'; +import { filterUsingGlobPatterns } from '../../hasher/task-hasher'; +import { ProjectGraphError } from '../../project-graph/error-types'; + +export interface GraphError { + message: string; + stack: string; + cause: unknown; + name: string; + pluginName: string; + fileName?: string; +} + export interface ProjectGraphClientResponse { hash: string; projects: ProjectGraphProjectNode[]; dependencies: Record; - fileMap: ProjectFileMap; + fileMap?: ProjectFileMap; layout: { appsDir: string; libsDir: string }; affected: string[]; focus: string; groupByFolder: boolean; exclude: string[]; + isPartial: boolean; + errors?: GraphError[]; } export interface TaskGraphClientResponse { @@ -273,10 +288,30 @@ export async function generateGraph( ? args.targets[0] : args.targets; - const { projectGraph: rawGraph, sourceMaps } = - await createProjectGraphAndSourceMapsAsync({ - exitOnError: true, - }); + let rawGraph: ProjectGraph; + let sourceMaps: ConfigurationSourceMaps; + let isPartial = false; + try { + const projectGraphAndSourceMaps = + await createProjectGraphAndSourceMapsAsync({ + exitOnError: false, + }); + rawGraph = projectGraphAndSourceMaps.projectGraph; + sourceMaps = projectGraphAndSourceMaps.sourceMaps; + } catch (e) { + if (e instanceof ProjectGraphError) { + output.warn({ + title: 'Failed to process project graph. Showing partial graph.', + }); + rawGraph = e.getPartialProjectGraph(); + sourceMaps = e.getPartialSourcemaps(); + + isPartial = true; + } + if (!rawGraph) { + handleProjectGraphError({ exitOnError: true }, e); + } + } let prunedGraph = pruneExternalNodes(rawGraph); const projects = Object.values( @@ -632,6 +667,8 @@ let currentProjectGraphClientResponse: ProjectGraphClientResponse = { focus: null, groupByFolder: false, exclude: [], + isPartial: false, + errors: [], }; let currentSourceMapsClientResponse: ConfigurationSourceMaps = {}; @@ -649,7 +686,11 @@ function debounce(fn: (...args) => void, time: number) { function createFileWatcher() { return daemonClient.registerFileWatcher( - { watchProjects: 'all', includeGlobalWorkspaceFiles: true }, + { + watchProjects: 'all', + includeGlobalWorkspaceFiles: true, + allowPartialGraph: true, + }, debounce(async (error, changes) => { if (error === 'closed') { output.error({ title: `Watch error: Daemon closed the connection` }); @@ -687,11 +728,39 @@ async function createProjectGraphAndSourceMapClientResponse( }> { performance.mark('project graph watch calculation:start'); - const { projectGraph, sourceMaps } = - await createProjectGraphAndSourceMapsAsync({ exitOnError: true }); + let projectGraph: ProjectGraph; + let sourceMaps: ConfigurationSourceMaps; + let isPartial = false; + let errors: GraphError[] | undefined; + try { + const projectGraphAndSourceMaps = + await createProjectGraphAndSourceMapsAsync({ exitOnError: false }); + projectGraph = projectGraphAndSourceMaps.projectGraph; + sourceMaps = projectGraphAndSourceMaps.sourceMaps; + } catch (e) { + if (e instanceof ProjectGraphError) { + projectGraph = e.getPartialProjectGraph(); + sourceMaps = e.getPartialSourcemaps(); + errors = e.getErrors().map((e) => ({ + message: e.message, + stack: e.stack, + cause: e.cause, + name: e.name, + pluginName: (e as any).pluginName, + fileName: + (e as any).file ?? (e.cause as any)?.errors?.[0]?.location?.file, + })); + isPartial = true; + } + + if (!projectGraph) { + handleProjectGraphError({ exitOnError: true }, e); + } + } let graph = pruneExternalNodes(projectGraph); - let fileMap = readFileMapCache().fileMap.projectFileMap; + let fileMap: ProjectFileMap | undefined = + readFileMapCache()?.fileMap.projectFileMap; performance.mark('project graph watch calculation:end'); performance.mark('project graph response generation:start'); @@ -700,7 +769,9 @@ async function createProjectGraphAndSourceMapClientResponse( const dependencies = graph.dependencies; const hasher = createHash('sha256'); - hasher.update(JSON.stringify({ layout, projects, dependencies, sourceMaps })); + hasher.update( + JSON.stringify({ layout, projects, dependencies, sourceMaps, errors }) + ); const hash = hasher.digest('hex'); @@ -727,6 +798,8 @@ async function createProjectGraphAndSourceMapClientResponse( dependencies, affected, fileMap, + isPartial, + errors, }, sourceMapResponse: sourceMaps, }; @@ -736,12 +809,15 @@ async function createTaskGraphClientResponse( pruneExternal: boolean = false ): Promise { let graph: ProjectGraph; + try { + graph = await createProjectGraphAsync({ exitOnError: false }); + } catch (e) { + if (e instanceof ProjectGraphError) { + graph = e.getPartialProjectGraph(); + } + } if (pruneExternal) { - graph = pruneExternalNodes( - await createProjectGraphAsync({ exitOnError: true }) - ); - } else { - graph = await createProjectGraphAsync({ exitOnError: true }); + graph = pruneExternalNodes(graph); } const nxJson = readNxJson(); diff --git a/packages/nx/src/daemon/client/client.ts b/packages/nx/src/daemon/client/client.ts index 3bd55fdf96cb1..a4ebf81faa1b3 100644 --- a/packages/nx/src/daemon/client/client.ts +++ b/packages/nx/src/daemon/client/client.ts @@ -179,6 +179,7 @@ export class DaemonClient { watchProjects: string[] | 'all'; includeGlobalWorkspaceFiles?: boolean; includeDependentProjects?: boolean; + allowPartialGraph?: boolean; }, callback: ( error: Error | null | 'closed', @@ -188,7 +189,15 @@ export class DaemonClient { } | null ) => void ): Promise { - await this.getProjectGraphAndSourceMaps(); + try { + await this.getProjectGraphAndSourceMaps(); + } catch (e) { + if (config.allowPartialGraph && e instanceof ProjectGraphError) { + // we are fine with partial graph + } else { + throw e; + } + } let messenger: DaemonSocketMessenger | undefined; await this.queue.sendToQueue(() => { diff --git a/packages/nx/src/daemon/server/file-watching/file-watcher-sockets.ts b/packages/nx/src/daemon/server/file-watching/file-watcher-sockets.ts index bc1f64fde45a8..453010d913bc9 100644 --- a/packages/nx/src/daemon/server/file-watching/file-watcher-sockets.ts +++ b/packages/nx/src/daemon/server/file-watching/file-watcher-sockets.ts @@ -1,6 +1,5 @@ import { Socket } from 'net'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; -import { ProjectGraph } from '../../../config/project-graph'; import { findAllProjectNodeDependencies } from '../../../utils/project-graph-utils'; import { PromisedBasedQueue } from '../../../utils/promised-based-queue'; import { currentProjectGraph } from '../project-graph-incremental-recomputation'; diff --git a/packages/nx/src/plugins/project-json/build-nodes/project-json.ts b/packages/nx/src/plugins/project-json/build-nodes/project-json.ts index 9dfc44fcbc262..51d0989504317 100644 --- a/packages/nx/src/plugins/project-json/build-nodes/project-json.ts +++ b/packages/nx/src/plugins/project-json/build-nodes/project-json.ts @@ -4,6 +4,7 @@ import { ProjectConfiguration } from '../../../config/workspace-json-project-jso import { toProjectName } from '../../../config/workspaces'; import { readJsonFile } from '../../../utils/fileutils'; import { NxPluginV2 } from '../../../project-graph/plugins'; +import { CreateNodesError } from '../../../project-graph/error-types'; export const ProjectJsonProjectsPlugin: NxPluginV2 = { name: 'nx/core/project-json', @@ -13,6 +14,7 @@ export const ProjectJsonProjectsPlugin: NxPluginV2 = { const json = readJsonFile( join(workspaceRoot, file) ); + const project = buildProjectFromProjectJson(json, file); return { projects: { diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index b6d221025db68..05e2086d5a7ad 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -171,7 +171,7 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { } } -function handleProjectGraphError(opts: { exitOnError: boolean }, e) { +export function handleProjectGraphError(opts: { exitOnError: boolean }, e) { if (opts.exitOnError) { const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true'; if (e instanceof ProjectGraphError) { From d531c5b6b44a27c90bfda0454c33e34e9e569c03 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 30 Apr 2024 13:25:49 +0200 Subject: [PATCH 2/4] fix(core): handle AggregateProjectGraphError w/o errors --- .../daemon/server/project-graph-incremental-recomputation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1680bb1c358e7..61120a66e6558 100644 --- a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts +++ b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts @@ -261,7 +261,7 @@ async function processFilesAndCreateAndSerializeProjectGraph( const errors = [...(projectConfigurationsError?.errors ?? [])]; if (g.error) { - if (isAggregateProjectGraphError(g.error)) { + if (isAggregateProjectGraphError(g.error) && g.error.errors?.length) { errors.push(...g.error.errors); } else { return { From d238f297453e01e9d98ff4536a04e4bb6cc649c4 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 30 Apr 2024 13:45:53 +0200 Subject: [PATCH 3/4] fix(graph): adjust error display colors & overflow --- graph/client/src/app/ui-components/error-boundary.tsx | 2 +- graph/ui-components/src/lib/error-renderer.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx index 3f575c6866908..db3262ac1a8df 100644 --- a/graph/client/src/app/ui-components/error-boundary.tsx +++ b/graph/client/src/app/ui-components/error-boundary.tsx @@ -93,7 +93,7 @@ function ErrorWithStack({ }) { return (
-

{message}

+

{message}

{stack &&

Error message: {stack}

}
); diff --git a/graph/ui-components/src/lib/error-renderer.tsx b/graph/ui-components/src/lib/error-renderer.tsx index 4c3bd5a6cfee7..3d5d1917329d3 100644 --- a/graph/ui-components/src/lib/error-renderer.tsx +++ b/graph/ui-components/src/lib/error-renderer.tsx @@ -16,12 +16,12 @@ export function ErrorRenderer({ errors }: { errors: GraphError[] }) { : error.fileName; return (
- + {errorHeading} - {fileSpecifier} -
+            
               {isCauseWithErrors(error.cause) &&
               error.cause.errors.length === 1 ? (
                 
From 3f8144b5899e4b2ad3a1decbdf28b3525caa8d92 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 30 Apr 2024 13:47:15 +0200 Subject: [PATCH 4/4] fix(graph): formatting fixes after rebase --- graph/client/src/app/feature-projects/project-list.tsx | 2 +- graph/client/src/app/shell.tsx | 4 ++-- .../client/src/app/ui-components/project-details-modal.tsx | 4 ++-- graph/ui-components/src/lib/error-renderer.tsx | 2 +- graph/ui-components/src/lib/modal.tsx | 6 +++--- .../src/lib/project-details/project-details.tsx | 2 +- .../target-configuration-details-group-list.tsx | 2 +- .../target-configuration-details-header.tsx | 4 ++-- .../target-configuration-details.tsx | 6 +++--- nx-dev/nx-dev/pages/tips.tsx | 4 ++-- nx-dev/ui-markdoc/src/lib/tags/call-to-action.component.tsx | 2 +- nx-dev/ui-markdoc/src/lib/tags/cards.component.tsx | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx index fed918e0ed075..4ee3bf2e006d4 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -219,7 +219,7 @@ function SubProjectList({
) : null} -
    +
      {sortedProjects.map((project) => { return ( - +
diff --git a/graph/client/src/app/ui-components/project-details-modal.tsx b/graph/client/src/app/ui-components/project-details-modal.tsx index 17eb137fb4bcd..e70a0b67faf77 100644 --- a/graph/client/src/app/ui-components/project-details-modal.tsx +++ b/graph/client/src/app/ui-components/project-details-modal.tsx @@ -50,7 +50,7 @@ export function ProjectDetailsModal() { return ( isOpen && (
-
+
diff --git a/graph/ui-components/src/lib/error-renderer.tsx b/graph/ui-components/src/lib/error-renderer.tsx index 3d5d1917329d3..b25664faa7c73 100644 --- a/graph/ui-components/src/lib/error-renderer.tsx +++ b/graph/ui-components/src/lib/error-renderer.tsx @@ -16,7 +16,7 @@ export function ErrorRenderer({ errors }: { errors: GraphError[] }) { : error.fileName; return (
- + {errorHeading} - {fileSpecifier} diff --git a/graph/ui-components/src/lib/modal.tsx b/graph/ui-components/src/lib/modal.tsx index 1cac5f2fbf1e1..5a2dfe65140e0 100644 --- a/graph/ui-components/src/lib/modal.tsx +++ b/graph/ui-components/src/lib/modal.tsx @@ -74,10 +74,10 @@ export const Modal = forwardRef( leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
Close modal
-
+
{children}
diff --git a/graph/ui-project-details/src/lib/project-details/project-details.tsx b/graph/ui-project-details/src/lib/project-details/project-details.tsx index 14197a5e0fee6..914041e85971c 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -86,7 +86,7 @@ export const ProjectDetails = ({ {onViewInProjectGraph ? (