Compare commits
3 Commits
patch-error
...
22.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 18dda8527f | |||
| 16ba3ad931 | |||
| 54ce2863d0 |
@@ -7,7 +7,7 @@ authors: ['Nicole Oliver', 'Miroslav Jonas', 'Victor Savkin']
|
||||
tags: [webinar]
|
||||
cover_image: /blog/images/2025-12-17/2025-December-webinar-card.avif
|
||||
time: 2pm ET/7pm UTC
|
||||
status: Upcoming
|
||||
status: Past - Gated
|
||||
registrationUrl: https://go.nx.dev/dec2025-webinar
|
||||
---
|
||||
|
||||
@@ -21,4 +21,4 @@ Whether you’re a longtime Nx user or considering it for your team, this sessio
|
||||
|
||||
Bah humbug to slow builds and CI bottlenecks—let’s build something better together.
|
||||
|
||||
{% call-to-action title="Register today!" url="https://go.nx.dev/dec2025-webinar" description="Save your spot" /%}
|
||||
{% call-to-action title="Download the recording!" url="https://go.nx.dev/dec2025-webinar" description="Sign up to gain access" /%}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCommand,
|
||||
tmpProjPath,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
import {
|
||||
addProjectReference,
|
||||
createDotNetProject,
|
||||
enableMultiTargeting,
|
||||
} from './utils/create-dotnet-project';
|
||||
|
||||
interface GraphDependency {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
describe('.NET Plugin - Dependency Graph', () => {
|
||||
beforeAll(() => {
|
||||
console.log('Creating new Nx workspace');
|
||||
newProject({ packages: [] });
|
||||
runCLI(`add @nx/dotnet`, { verbose: true });
|
||||
console.log('Nx workspace created');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupProject();
|
||||
console.log('Nx workspace cleaned up');
|
||||
});
|
||||
|
||||
describe('Multi-targeting projects', () => {
|
||||
beforeAll(() => {
|
||||
// Create a chain: MultiTargetLib -> SingleTargetLib -> BaseLib
|
||||
createDotNetProject({ name: 'BaseLib', type: 'classlib' });
|
||||
createDotNetProject({ name: 'SingleTargetLib', type: 'classlib' });
|
||||
createDotNetProject({ name: 'MultiTargetLib', type: 'classlib' });
|
||||
createDotNetProject({ name: 'ConsumerApp', type: 'console' });
|
||||
|
||||
// Set up the dependency chain
|
||||
addProjectReference('SingleTargetLib', 'BaseLib');
|
||||
addProjectReference('MultiTargetLib', 'SingleTargetLib');
|
||||
addProjectReference('ConsumerApp', 'MultiTargetLib');
|
||||
|
||||
// Enable multi-targeting on all libs in the chain and restore each to update assets
|
||||
enableMultiTargeting('BaseLib', ['net8.0', 'net9.0']);
|
||||
runCommand('dotnet restore BaseLib', { cwd: tmpProjPath() });
|
||||
|
||||
enableMultiTargeting('SingleTargetLib', ['net8.0', 'net9.0']);
|
||||
runCommand('dotnet restore SingleTargetLib', { cwd: tmpProjPath() });
|
||||
|
||||
enableMultiTargeting('MultiTargetLib', ['net8.0', 'net9.0']);
|
||||
runCommand('dotnet restore MultiTargetLib', { cwd: tmpProjPath() });
|
||||
});
|
||||
|
||||
it('should detect dependencies for multi-targeting projects', () => {
|
||||
runCLI('graph --file=multi-target-graph.json');
|
||||
|
||||
checkFilesExist('multi-target-graph.json');
|
||||
const { graph } = readJson('multi-target-graph.json');
|
||||
|
||||
// MultiTargetLib should have SingleTargetLib as a dependency
|
||||
const multiTargetDeps: GraphDependency[] =
|
||||
graph.dependencies['MultiTargetLib'] || [];
|
||||
expect(
|
||||
multiTargetDeps.some((dep) => dep.target === 'SingleTargetLib')
|
||||
).toBe(true);
|
||||
|
||||
// SingleTargetLib should have BaseLib as a dependency
|
||||
const singleTargetDeps: GraphDependency[] =
|
||||
graph.dependencies['SingleTargetLib'] || [];
|
||||
expect(singleTargetDeps.some((dep) => dep.target === 'BaseLib')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// ConsumerApp should have MultiTargetLib as a dependency
|
||||
const consumerDeps: GraphDependency[] =
|
||||
graph.dependencies['ConsumerApp'] || [];
|
||||
expect(consumerDeps.some((dep) => dep.target === 'MultiTargetLib')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should build multi-targeting projects with dependencies', () => {
|
||||
const output = runCLI('build ConsumerApp --verbose', { verbose: true });
|
||||
expect(output).toContain('Build succeeded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transitive dependencies', () => {
|
||||
beforeAll(() => {
|
||||
// Create a chain: AppProject -> MiddleLib -> LeafLib
|
||||
// We want to verify AppProject only shows MiddleLib as direct dep,
|
||||
// not LeafLib (which is a transitive dependency)
|
||||
createDotNetProject({ name: 'LeafLib', type: 'classlib' });
|
||||
createDotNetProject({ name: 'MiddleLib', type: 'classlib' });
|
||||
createDotNetProject({ name: 'AppProject', type: 'console' });
|
||||
|
||||
// Set up the dependency chain
|
||||
addProjectReference('MiddleLib', 'LeafLib');
|
||||
addProjectReference('AppProject', 'MiddleLib');
|
||||
});
|
||||
|
||||
it('should only show direct dependencies, not transitive ones', () => {
|
||||
runCLI('graph --file=transitive-graph.json');
|
||||
|
||||
checkFilesExist('transitive-graph.json');
|
||||
const { graph } = readJson('transitive-graph.json');
|
||||
|
||||
// AppProject should ONLY have MiddleLib as dependency
|
||||
const appDeps: GraphDependency[] = graph.dependencies['AppProject'] || [];
|
||||
const appProjectRefs = appDeps.filter(
|
||||
(dep) => dep.type === 'static' || dep.type === 'implicit'
|
||||
);
|
||||
|
||||
// Should have MiddleLib
|
||||
expect(appProjectRefs.some((dep) => dep.target === 'MiddleLib')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Should NOT have LeafLib (that's a transitive dependency)
|
||||
expect(appProjectRefs.some((dep) => dep.target === 'LeafLib')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
// MiddleLib should have LeafLib as dependency
|
||||
const middleDeps: GraphDependency[] =
|
||||
graph.dependencies['MiddleLib'] || [];
|
||||
expect(middleDeps.some((dep) => dep.target === 'LeafLib')).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly show transitive deps for multi-targeting projects', () => {
|
||||
// Enable multi-targeting to test the combined scenario
|
||||
enableMultiTargeting('LeafLib', ['net8.0', 'net9.0']);
|
||||
runCommand('dotnet restore LeafLib', { cwd: tmpProjPath() });
|
||||
|
||||
enableMultiTargeting('MiddleLib', ['net8.0', 'net9.0']);
|
||||
runCommand('dotnet restore MiddleLib', { cwd: tmpProjPath() });
|
||||
|
||||
runCLI('graph --file=transitive-multi-graph.json');
|
||||
|
||||
checkFilesExist('transitive-multi-graph.json');
|
||||
const { graph } = readJson('transitive-multi-graph.json');
|
||||
|
||||
// AppProject should still ONLY have MiddleLib as direct dependency
|
||||
const appDeps: GraphDependency[] = graph.dependencies['AppProject'] || [];
|
||||
const appProjectRefs = appDeps.filter(
|
||||
(dep) => dep.type === 'static' || dep.type === 'implicit'
|
||||
);
|
||||
|
||||
expect(appProjectRefs.some((dep) => dep.target === 'MiddleLib')).toBe(
|
||||
true
|
||||
);
|
||||
expect(appProjectRefs.some((dep) => dep.target === 'LeafLib')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { MouseEvent, ReactElement, useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
MegaphoneIcon,
|
||||
VideoCameraIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
// import { MouseEvent, ReactElement, useEffect, useState } from 'react';
|
||||
// import { motion } from 'framer-motion';
|
||||
// import {
|
||||
// MegaphoneIcon,
|
||||
// VideoCameraIcon,
|
||||
// XMarkIcon,
|
||||
// } from '@heroicons/react/24/outline';
|
||||
|
||||
export function WebinarNotifier(): ReactElement | null {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const localStorageKey = 'webinar-december-2025--notifier-closed';
|
||||
return null;
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const isClosedSession = localStorage.getItem(localStorageKey);
|
||||
if (isClosedSession === 'true') {
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, []);
|
||||
// const [isMounted, setIsMounted] = useState(false);
|
||||
// const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
// const localStorageKey = 'webinar-december-2025--notifier-closed';
|
||||
|
||||
const closeNotifier = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsVisible(false);
|
||||
localStorage.setItem(localStorageKey, 'true');
|
||||
};
|
||||
// useEffect(() => {
|
||||
// setIsMounted(true);
|
||||
// const isClosedSession = localStorage.getItem(localStorageKey);
|
||||
// if (isClosedSession === 'true') {
|
||||
// setIsVisible(false);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
if (!isMounted || !isVisible) return null;
|
||||
// const closeNotifier = (e: MouseEvent) => {
|
||||
// e.stopPropagation();
|
||||
// setIsVisible(false);
|
||||
// localStorage.setItem(localStorageKey, 'true');
|
||||
// };
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ y: '120%' }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '120%' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
mass: 1,
|
||||
}}
|
||||
className="fixed bottom-0 left-0 right-0 z-30 w-full overflow-hidden bg-slate-950 text-white shadow-lg md:bottom-4 md:left-auto md:right-4 md:w-[512px] md:rounded-lg"
|
||||
style={{ originY: 1 }}
|
||||
>
|
||||
<div className="relative p-4">
|
||||
<button
|
||||
onClick={closeNotifier}
|
||||
className="absolute right-2 top-2 flex h-9 w-9 cursor-pointer items-center justify-center !rounded-full bg-transparent p-1 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-white"
|
||||
>
|
||||
<XMarkIcon className="size-5" aria-hidden="true" />
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<div>
|
||||
<motion.h3
|
||||
layout="position"
|
||||
className="flex items-center gap-2 pr-8 text-lg font-semibold"
|
||||
>
|
||||
<MegaphoneIcon
|
||||
aria-hidden="true"
|
||||
className="size-8 flex-shrink-0"
|
||||
/>
|
||||
<span>Bah humbug to slow builds and CI bottlenecks!</span>
|
||||
</motion.h3>
|
||||
<motion.div key="live-event" className="mt-4 space-y-4">
|
||||
<p className="mb-2 text-sm">
|
||||
Join us for a special year-end webinar on Dec. 17th. We’ll take
|
||||
you on a journey through Nx’s past, present, and future to explore
|
||||
the evolution of Nx and what’s coming next.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-end gap-1 sm:gap-4">
|
||||
<a
|
||||
title="Signup"
|
||||
href="https://bit.ly/4q9sgzV"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-pink-600 px-2 py-2 text-sm font-semibold text-white no-underline transition hover:bg-pink-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:text-black/70 md:px-4"
|
||||
>
|
||||
<VideoCameraIcon aria-hidden="true" className="size-4" />
|
||||
<span>Sign Up Now</span>
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
// if (!isMounted || !isVisible) return null;
|
||||
|
||||
// return (
|
||||
// <motion.div
|
||||
// layout
|
||||
// initial={{ y: '120%' }}
|
||||
// animate={{ y: 0 }}
|
||||
// exit={{ y: '120%' }}
|
||||
// transition={{
|
||||
// type: 'spring',
|
||||
// stiffness: 300,
|
||||
// damping: 30,
|
||||
// mass: 1,
|
||||
// }}
|
||||
// className="fixed bottom-0 left-0 right-0 z-30 w-full overflow-hidden bg-slate-950 text-white shadow-lg md:bottom-4 md:left-auto md:right-4 md:w-[512px] md:rounded-lg"
|
||||
// style={{ originY: 1 }}
|
||||
// >
|
||||
// <div className="relative p-4">
|
||||
// <button
|
||||
// onClick={closeNotifier}
|
||||
// className="absolute right-2 top-2 flex h-9 w-9 cursor-pointer items-center justify-center !rounded-full bg-transparent p-1 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-white"
|
||||
// >
|
||||
// <XMarkIcon className="size-5" aria-hidden="true" />
|
||||
// <span className="sr-only">Close</span>
|
||||
// </button>
|
||||
// <div>
|
||||
// <motion.h3
|
||||
// layout="position"
|
||||
// className="flex items-center gap-2 pr-8 text-lg font-semibold"
|
||||
// >
|
||||
// <MegaphoneIcon
|
||||
// aria-hidden="true"
|
||||
// className="size-8 flex-shrink-0"
|
||||
// />
|
||||
// <span>Bah humbug to slow builds and CI bottlenecks!</span>
|
||||
// </motion.h3>
|
||||
// <motion.div key="live-event" className="mt-4 space-y-4">
|
||||
// <p className="mb-2 text-sm">
|
||||
// Join us for a special year-end webinar on Dec. 17th. We’ll take
|
||||
// you on a journey through Nx’s past, present, and future to explore
|
||||
// the evolution of Nx and what’s coming next.
|
||||
// </p>
|
||||
// <div className="flex flex-wrap items-center justify-end gap-1 sm:gap-4">
|
||||
// <a
|
||||
// title="Signup"
|
||||
// href="https://bit.ly/4q9sgzV"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// className="inline-flex items-center justify-center gap-2 rounded-lg bg-pink-600 px-2 py-2 text-sm font-semibold text-white no-underline transition hover:bg-pink-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:text-black/70 md:px-4"
|
||||
// >
|
||||
// <VideoCameraIcon aria-hidden="true" className="size-4" />
|
||||
// <span>Sign Up Now</span>
|
||||
// </a>
|
||||
// </div>
|
||||
// </motion.div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </motion.div>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from 'react';
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
// import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export const WebinarSection: React.FC = () => {
|
||||
return (
|
||||
<p>
|
||||
<a
|
||||
href="https://bit.ly/4q9sgzV"
|
||||
title="See live event in details"
|
||||
className="group/event-link inline-flex space-x-6"
|
||||
>
|
||||
<span className="rounded-full bg-blue-600/10 px-3 py-1 text-sm/6 font-semibold text-blue-600 ring-1 ring-inset ring-blue-600/10 dark:bg-cyan-600/10 dark:text-cyan-600 dark:ring-cyan-600/10">
|
||||
Live event
|
||||
</span>
|
||||
<span className="inline-flex items-center space-x-2 text-sm/6 font-medium">
|
||||
<span>Webinar on November 19th</span>
|
||||
<ChevronRightIcon
|
||||
aria-hidden="true"
|
||||
className="size-5 transform transition-all group-hover/event-link:translate-x-1"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
return null;
|
||||
|
||||
// return (
|
||||
// <p>
|
||||
// <a
|
||||
// href="https://bit.ly/4q9sgzV"
|
||||
// title="See live event in details"
|
||||
// className="group/event-link inline-flex space-x-6"
|
||||
// >
|
||||
// <span className="rounded-full bg-blue-600/10 px-3 py-1 text-sm/6 font-semibold text-blue-600 ring-1 ring-inset ring-blue-600/10 dark:bg-cyan-600/10 dark:text-cyan-600 dark:ring-cyan-600/10">
|
||||
// Live event
|
||||
// </span>
|
||||
// <span className="inline-flex items-center space-x-2 text-sm/6 font-medium">
|
||||
// <span>Webinar on November 19th</span>
|
||||
// <ChevronRightIcon
|
||||
// aria-hidden="true"
|
||||
// className="size-5 transform transition-all group-hover/event-link:translate-x-1"
|
||||
// />
|
||||
// </span>
|
||||
// </a>
|
||||
// </p>
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -59,40 +59,68 @@ public static class Analyzer
|
||||
var nodesByFile = new Dictionary<string, NxProjectGraphNode>();
|
||||
var referencesByRoot = new Dictionary<string, ReferencesInfo>();
|
||||
|
||||
using (var analyzeProjectsPerf = PerfLogger.Start($"analyze workspace > transform {projectGraph.ProjectNodes.Count} projects"))
|
||||
// Group nodes by project file path to handle multi-targeting projects.
|
||||
// Multi-targeting projects (using TargetFrameworks plural) create multiple nodes:
|
||||
// - An "outer build" with TargetFrameworks set but TargetFramework empty
|
||||
// - "Inner builds" for each target framework with TargetFramework set
|
||||
// We need to aggregate references from all builds and use an inner build for config.
|
||||
var nodesByPath = new Dictionary<string, List<ProjectGraphNode>>();
|
||||
foreach (var node in projectGraph.ProjectNodes)
|
||||
{
|
||||
foreach (var node in projectGraph.ProjectNodes)
|
||||
if (node.ProjectInstance?.FullPath is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var path = node.ProjectInstance.FullPath;
|
||||
if (!nodesByPath.TryGetValue(path, out var nodes))
|
||||
{
|
||||
nodes = new List<ProjectGraphNode>();
|
||||
nodesByPath[path] = nodes;
|
||||
}
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
using (var analyzeProjectsPerf = PerfLogger.Start($"analyze workspace > transform {nodesByPath.Count} projects"))
|
||||
{
|
||||
foreach (var kvp in nodesByPath)
|
||||
{
|
||||
var projectPath = kvp.Key;
|
||||
var nodes = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
if (node.ProjectInstance is null)
|
||||
// For multi-targeting projects, prefer an inner build (has TargetFramework set)
|
||||
// over the outer build (has TargetFrameworks but no TargetFramework).
|
||||
// Inner builds have the actual project references.
|
||||
var primaryNode = nodes.FirstOrDefault(n =>
|
||||
!string.IsNullOrEmpty(n.ProjectInstance?.GetPropertyValue("TargetFramework")))
|
||||
?? nodes.First();
|
||||
|
||||
if (primaryNode.ProjectInstance is null)
|
||||
{
|
||||
throw new InvalidOperationException("ProjectInstance is null.");
|
||||
}
|
||||
var projectPath = node.ProjectInstance.FullPath;
|
||||
if (string.IsNullOrEmpty(projectPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var projectRoot = ProjectUtilities.GetRelativeProjectRoot(projectPath, workspaceRoot);
|
||||
var relativeProjectFile = ProjectUtilities.GetRelativeProjectFile(projectPath, workspaceRoot);
|
||||
|
||||
// Collect package references
|
||||
var packageRefs = CollectPackageReferences(node.ProjectInstance!);
|
||||
// Collect package references from primary node
|
||||
var packageRefs = CollectPackageReferences(primaryNode.ProjectInstance!);
|
||||
|
||||
// Collect project references
|
||||
var projectRefs = CollectProjectReferences(node, projectPath, workspaceRoot);
|
||||
// Collect direct project references from the primary node's ProjectInstance.
|
||||
// Uses GetItems("ProjectReference") which returns only DIRECT references,
|
||||
// with glob patterns already evaluated by MSBuild during project loading.
|
||||
var projectRefs = CollectProjectReferences(primaryNode.ProjectInstance!, projectPath, workspaceRoot);
|
||||
|
||||
// Collect MSBuild properties
|
||||
var properties = CollectProperties(node.ProjectInstance!);
|
||||
// Collect MSBuild properties from primary node
|
||||
var properties = CollectProperties(primaryNode.ProjectInstance!);
|
||||
|
||||
// Determine project type
|
||||
var isTest = IsTestProject(properties, packageRefs);
|
||||
var isExe = IsExecutableProject(properties);
|
||||
|
||||
// Build targets
|
||||
var projectName = ProjectUtilities.GetProjectName(node.ProjectInstance);
|
||||
var projectName = ProjectUtilities.GetProjectName(primaryNode.ProjectInstance);
|
||||
var targets = TargetBuilder.BuildTargets(
|
||||
projectName,
|
||||
Path.GetFileName(projectPath),
|
||||
@@ -127,7 +155,7 @@ public static class Analyzer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error analyzing {node.ProjectInstance?.FullPath}: {ex.Message}");
|
||||
Console.Error.WriteLine($"Error analyzing {projectPath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,24 +183,32 @@ public static class Analyzer
|
||||
return packageRefs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects direct project references from the ProjectInstance.
|
||||
/// Uses GetItems("ProjectReference") to get only direct references defined in the project file.
|
||||
/// MSBuild evaluates glob patterns during project loading, so EvaluatedInclude contains
|
||||
/// resolved paths even when the original ProjectReference used globs.
|
||||
/// </summary>
|
||||
private static List<string> CollectProjectReferences(
|
||||
ProjectGraphNode project,
|
||||
ProjectInstance project,
|
||||
string projectPath,
|
||||
string workspaceRoot)
|
||||
{
|
||||
var projectRefs = new List<string>();
|
||||
var projectDir = Path.GetDirectoryName(projectPath)!;
|
||||
|
||||
foreach (var referencedProject in project.ProjectReferences)
|
||||
foreach (var item in project.GetItems("ProjectReference"))
|
||||
{
|
||||
var refPath = referencedProject.ProjectInstance?.FullPath;
|
||||
var refPath = item.EvaluatedInclude;
|
||||
if (string.IsNullOrEmpty(refPath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Project reference in {projectPath} is missing a valid path.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the path relative to the project directory
|
||||
var absoluteRefPath = Path.IsPathRooted(refPath)
|
||||
? refPath
|
||||
: Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectPath)!, refPath));
|
||||
: Path.GetFullPath(Path.Combine(projectDir, refPath));
|
||||
|
||||
var relativeRefRoot = ProjectUtilities.GetRelativeProjectRoot(absoluteRefPath, workspaceRoot);
|
||||
projectRefs.Add(relativeRefRoot);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { NxArgs } from '../utils/command-line-utils';
|
||||
import { isCI } from '../utils/is-ci';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export const ORIGINAL_TUI_ENV_VALUE = process.env.NX_TUI;
|
||||
|
||||
/**
|
||||
* @returns If tui is enabled
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
} from '../utils/sync-generators';
|
||||
import { workspaceRoot } from '../utils/workspace-root';
|
||||
import { createTaskGraph } from './create-task-graph';
|
||||
import { isTuiEnabled } from './is-tui-enabled';
|
||||
import { isTuiEnabled, ORIGINAL_TUI_ENV_VALUE } from './is-tui-enabled';
|
||||
import {
|
||||
CompositeLifeCycle,
|
||||
LifeCycle,
|
||||
@@ -94,8 +94,8 @@ async function getTerminalOutputLifeCycle(
|
||||
|
||||
const isRunOne = initiatingProject != null;
|
||||
|
||||
if (tasks.length === 1) {
|
||||
process.env.NX_TUI ??= 'false';
|
||||
if (tasks.length === 1 && !ORIGINAL_TUI_ENV_VALUE) {
|
||||
process.env.NX_TUI = 'false';
|
||||
}
|
||||
|
||||
if (isTuiEnabled()) {
|
||||
|
||||
Reference in New Issue
Block a user